mirror of
https://github.com/anotherhadi/github-recon.git
synced 2026-10-05 10:58:25 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a9483411a | ||
|
|
7f74f5d28f |
@@ -17,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
|
||||
|
||||
@@ -25,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
|
||||
|
||||
@@ -67,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
|
||||
|
||||
@@ -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" ];
|
||||
|
||||
|
||||
@@ -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
@@ -6,66 +6,87 @@ import (
|
||||
"github.com/google/go-github/v72/github"
|
||||
)
|
||||
|
||||
func (r Recon) Commits(username string) {
|
||||
PrintTitle("🐙 Commits")
|
||||
type CommitsResult struct {
|
||||
Name string
|
||||
Email string
|
||||
Occurences int
|
||||
FirstFoundIn string
|
||||
}
|
||||
|
||||
seenNames := make(map[string]struct{})
|
||||
seenEmails := make(map[string]struct{})
|
||||
var names, emails []string
|
||||
func (r Recon) Commits(username string) (response []CommitsResult) {
|
||||
r.PrintTitle("🐙 Commits")
|
||||
|
||||
collect := func(order string) error {
|
||||
opts := &github.SearchOptions{
|
||||
Sort: "author-date",
|
||||
Order: order,
|
||||
ListOptions: github.ListOptions{PerPage: 100},
|
||||
}
|
||||
results := make(map[string]CommitsResult)
|
||||
|
||||
collect := func(date string) error {
|
||||
for page := 1; page <= 10; page++ {
|
||||
opts.ListOptions.Page = page
|
||||
result, resp, err := r.client.Search.Commits(
|
||||
r.ctx,
|
||||
fmt.Sprintf("author:%s", username),
|
||||
opts,
|
||||
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, order, err)
|
||||
return fmt.Errorf("fetch page %d (%s): %w", page, date, 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)
|
||||
PrintInfo(
|
||||
"Name "+fmt.Sprint(len(names)),
|
||||
name,
|
||||
"from "+item.GetRepository().Owner.GetLogin()+"/"+item.GetRepository().
|
||||
GetName(),
|
||||
)
|
||||
name := item.Commit.GetAuthor().GetName()
|
||||
email := item.Commit.GetAuthor().GetEmail()
|
||||
if SkipResult(name, email) {
|
||||
// continue
|
||||
}
|
||||
if _, seen := seenEmails[email]; !seen {
|
||||
seenEmails[email] = struct{}{}
|
||||
emails = append(emails, email)
|
||||
PrintInfo(
|
||||
"Email "+fmt.Sprint(len(emails)),
|
||||
email,
|
||||
"from "+item.GetRepository().Owner.GetLogin()+"/"+item.GetRepository().
|
||||
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
|
||||
}
|
||||
|
||||
if err := collect("desc"); err != nil {
|
||||
r.logger.Error("Failed to fetch commits", "err", err, "order", "desc")
|
||||
// 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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -4,88 +4,151 @@ import (
|
||||
"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)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to fetch ssh keys", "err", err)
|
||||
} else if len(sshKeys) == 0 {
|
||||
PrintTitle("🔑 SSH Keys")
|
||||
r.logger.Info("No SSH Keys found\n")
|
||||
r.PrintTitle("🔑 SSH Keys")
|
||||
r.PrintInfo("INFO", "No SSH Keys found")
|
||||
} else {
|
||||
PrintTitle("🔑 SSH Keys")
|
||||
r.PrintTitle("🔑 SSH Keys")
|
||||
for i, key := range sshKeys {
|
||||
PrintInfo("Key n°", fmt.Sprintf("%d", i))
|
||||
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()
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to fetch user's gpg keys", "err", err)
|
||||
} else if len(gpgKeys) == 0 {
|
||||
PrintTitle("🗝️ GPG Keys")
|
||||
r.logger.Info("No GPG Keys found\n")
|
||||
r.PrintTitle("🗝️ GPG Keys")
|
||||
r.PrintInfo("INFO", "No GPG Keys found")
|
||||
} else {
|
||||
PrintTitle("🗝️ GPG Keys")
|
||||
r.PrintTitle("🗝️ GPG Keys")
|
||||
for i, key := range gpgKeys {
|
||||
PrintInfo("Key n°", fmt.Sprintf("%d", i))
|
||||
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 j, email := range key.Emails {
|
||||
PrintInfo(" Email n°", fmt.Sprintf("%d", j))
|
||||
PrintInfo(" Email", email.GetEmail())
|
||||
PrintInfo(" Verified", fmt.Sprintf("%t", email.GetVerified()))
|
||||
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{},
|
||||
}
|
||||
PrintInfo("Subkeys", fmt.Sprintf("%d", len(key.Subkeys)))
|
||||
for j, subkey := range key.Subkeys {
|
||||
PrintInfo(" Subkey n°", fmt.Sprintf("%d", j))
|
||||
PrintInfo(" Subkey ID", fmt.Sprintf("%d", subkey.GetID()))
|
||||
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())
|
||||
for _, email := range key.Emails {
|
||||
email := GPGKeyEmail{
|
||||
Email: email.GetEmail(),
|
||||
Verified: fmt.Sprintf("%t", email.GetVerified()),
|
||||
}
|
||||
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)
|
||||
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(
|
||||
r.ctx,
|
||||
username,
|
||||
@@ -94,18 +157,27 @@ func (r Recon) SshSigningKeys(username string) {
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to fetch user's ssh signing keys", "err", err)
|
||||
} else if len(signingKeys) == 0 {
|
||||
PrintTitle("📝 SSH Signing Keys")
|
||||
r.logger.Info("No SSH Signing Keys found\n")
|
||||
r.PrintTitle("📝 SSH Signing Keys")
|
||||
r.PrintInfo("INFO", "No SSH Signing Keys found")
|
||||
} else {
|
||||
PrintTitle("📝 SSH Signing Keys")
|
||||
r.PrintTitle("📝 SSH Signing Keys")
|
||||
for i, key := range signingKeys {
|
||||
PrintInfo("Key n°", fmt.Sprintf("%d", i))
|
||||
PrintInfo("ID", fmt.Sprintf("%d", key.GetID()))
|
||||
PrintInfo("Key", key.GetKey())
|
||||
PrintInfo("Title", key.GetTitle())
|
||||
PrintInfo("Created At", key.GetCreatedAt().String())
|
||||
fmt.Println()
|
||||
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
|
||||
}
|
||||
|
||||
+11
-1
@@ -11,12 +11,22 @@ 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) *Recon {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
+26
-18
@@ -4,31 +4,39 @@ import (
|
||||
"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)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to fetch organizations", "err", err)
|
||||
} else if len(orgs) == 0 {
|
||||
PrintTitle("🏢 Organizations")
|
||||
r.logger.Info("No Organizations found\n")
|
||||
r.PrintTitle("🏢 Organizations")
|
||||
r.PrintInfo("INFO", "No Organizations found")
|
||||
} else {
|
||||
PrintTitle("🏢 Organizations")
|
||||
r.PrintTitle("🏢 Organizations")
|
||||
for i, org := range orgs {
|
||||
PrintInfo("Orgs n°", fmt.Sprintf("%d", i))
|
||||
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()
|
||||
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
|
||||
}
|
||||
|
||||
+16
-15
@@ -5,20 +5,19 @@ import (
|
||||
"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")
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to fetch socials", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
type SocialAccount struct {
|
||||
Provider string `json:"provider"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
type SocialAccounts []SocialAccount
|
||||
|
||||
var socialAccounts SocialAccounts
|
||||
var socialAccounts []SocialResult
|
||||
err = json.Unmarshal(resp, &socialAccounts)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to unmarshal socials", "err", err)
|
||||
@@ -26,15 +25,17 @@ func (r Recon) Socials(username string) {
|
||||
}
|
||||
|
||||
if len(socialAccounts) == 0 {
|
||||
PrintTitle("🐥 Socials")
|
||||
PrintTitle("No Socials found\n")
|
||||
r.PrintTitle("🐥 Socials")
|
||||
r.PrintInfo("INFO", "No commits found")
|
||||
} else {
|
||||
PrintTitle("🐥 Socials")
|
||||
r.PrintTitle("🐥 Socials")
|
||||
for i, account := range socialAccounts {
|
||||
PrintInfo("Social n°", fmt.Sprintf("%d", i))
|
||||
PrintInfo("Provider", account.Provider)
|
||||
PrintInfo("URL", account.URL)
|
||||
fmt.Println()
|
||||
r.PrintInfo("Social n°", fmt.Sprintf("%d", i))
|
||||
r.PrintInfo("Provider", account.Provider)
|
||||
r.PrintInfo("URL", account.URL)
|
||||
}
|
||||
}
|
||||
r.PrintNewline()
|
||||
|
||||
return socialAccounts
|
||||
}
|
||||
|
||||
+75
-28
@@ -4,7 +4,32 @@ import (
|
||||
"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)
|
||||
if resp.StatusCode == 404 {
|
||||
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)
|
||||
}
|
||||
|
||||
PrintTitle("👤 User informations")
|
||||
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()
|
||||
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
|
||||
}
|
||||
|
||||
+54
-4
@@ -1,9 +1,11 @@
|
||||
package ghrecon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -22,9 +24,14 @@ 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 {
|
||||
@@ -83,12 +90,25 @@ func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) {
|
||||
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"))
|
||||
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" {
|
||||
return
|
||||
}
|
||||
@@ -111,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)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -2,42 +2,49 @@ 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")
|
||||
flag.Parse()
|
||||
|
||||
if username == "" {
|
||||
fmt.Println("Please provide a username with the --username flag")
|
||||
os.Exit(1)
|
||||
}
|
||||
err := ghrecon.ParseUsername(username)
|
||||
if err != nil {
|
||||
log.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.",
|
||||
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",
|
||||
)
|
||||
} else {
|
||||
client = client.WithAuthToken(token)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
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()
|
||||
|
||||
styles := log.DefaultStyles()
|
||||
styles.Levels[log.InfoLevel] = styles.Levels[log.InfoLevel].Foreground(ghrecon.Grey)
|
||||
@@ -47,18 +54,85 @@ func main() {
|
||||
})
|
||||
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(
|
||||
client,
|
||||
logger,
|
||||
ctx,
|
||||
silent,
|
||||
jsonFile,
|
||||
)
|
||||
|
||||
ghrecon.Header()
|
||||
r.User(username)
|
||||
r.Orgs(username)
|
||||
r.SshKeys(username)
|
||||
r.GpgKeys(username)
|
||||
r.SshSigningKeys(username)
|
||||
r.Socials(username)
|
||||
r.Commits(username)
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user