Compare commits

...
25 Commits
Author SHA1 Message Date
Hadi 0041c0c132 edit readme instructions & flags
Signed-off-by: Hadi <[email protected]>
2025-05-28 10:02:19 +02:00
Hadi d3fdfdcdc5 comments & sorting flags
Signed-off-by: Hadi <[email protected]>
2025-05-28 10:00:12 +02:00
Hadi 4f087cb7f0 ignore errors
Signed-off-by: Hadi <[email protected]>
2025-05-28 09:57:50 +02:00
Hadi befb546292 refactor main
Signed-off-by: Hadi <[email protected]>
2025-05-28 09:56:03 +02:00
Hadi 9a825bc7cc remove New func
Signed-off-by: Hadi <[email protected]>
2025-05-28 09:46:34 +02:00
Hadi 72b8635832 remove @ from username
Signed-off-by: Hadi <[email protected]>
2025-05-28 09:44:57 +02:00
Hadi 2ba8695674 edit flags sorting
Signed-off-by: Hadi <[email protected]>
2025-05-28 09:44:49 +02:00
Hadi f8eb7d58ba add flag normalization
Signed-off-by: Hadi <[email protected]>
2025-05-27 20:30:29 +02:00
Hadi 637d9811f2 change struct
Signed-off-by: Hadi <[email protected]>
2025-05-27 20:01:05 +02:00
Hadi c498e7bfdd typo
Signed-off-by: Hadi <[email protected]>
2025-05-27 16:12:01 +02:00
Hadi 7e2b6dd426 Cover your tracks!
Signed-off-by: Hadi <[email protected]>
2025-05-21 10:45:28 +02:00
Hadi 05b449afcd add --max-size & --refresh
Signed-off-by: Hadi <[email protected]>
2025-05-21 10:33:00 +02:00
Hadi b4392f972d edit close friends algo
Signed-off-by: Hadi <[email protected]>
2025-05-21 10:12:40 +02:00
Hadi e4cec2b07a Avoid double newline
Signed-off-by: Hadi <[email protected]>
2025-05-21 10:12:33 +02:00
Hadi 15458ab78e not anymore, now by year/half-year
Signed-off-by: Hadi <[email protected]>
2025-05-20 22:26:30 +02:00
Hadi deec50febf fix typo
Signed-off-by: Hadi <[email protected]>
2025-05-20 22:19:14 +02:00
Hadi de47073083 edit readme
Signed-off-by: Hadi <[email protected]>
2025-05-20 22:14:32 +02:00
Hadi a3f4b19382 add logo
Signed-off-by: Hadi <[email protected]>
2025-05-20 22:02:23 +02:00
Hadi afdd30d34b add assets
Signed-off-by: Hadi <[email protected]>
2025-05-20 22:00:15 +02:00
Hadi 58ac92bff8 rephrase
Signed-off-by: Hadi <[email protected]>
2025-05-20 17:00:38 +02:00
Hadi ce96d1f307 cleaner version
Signed-off-by: Hadi <[email protected]>
2025-05-20 15:42:49 +02:00
Hadi bd734f11af format
Signed-off-by: Hadi <[email protected]>
2025-05-20 15:32:20 +02:00
Hadi 1ff0e222c7 update flake
Signed-off-by: Hadi <[email protected]>
2025-05-20 15:30:08 +02:00
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
19 changed files with 1063 additions and 247 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

+10
View File
@@ -0,0 +1,10 @@
# Contributing
Everybody is invited and welcome to contribute to this repo. There is a lot to do... Check the issues!
The process is straight-forward.
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes. (but skip step 0 and 1)
- [Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) this repo.
- Write your changes (bug fixe, new feature, issues fix, ...).
- Create a Pull Request against the main branch.
+48 -9
View File
@@ -1,3 +1,9 @@
<div align="center">
<img src="https://raw.githubusercontent.com/anotherhadi/gh-recon/main/.github/assets/logo.png" width="120px" />
</div>
<br>
# GH-Recon # GH-Recon
<p> <p>
@@ -16,7 +22,11 @@ Fetches and aggregates public OSINT data for a GitHub user, leveraging Go and th
- List organizations and roles - List organizations and roles
- 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)
- Find close friends
- Find Github accounts using an email address
- Export results to JSON
- Deep scan option (clone repositories, regex search, analyze licenses, etc.)
## Disclaimer ## Disclaimer
@@ -25,18 +35,21 @@ 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
### With Go ### With Go
```bash ```bash
go get github.com/anotherhadi/gh-recon go install github.com/anotherhadi/gh-recon@latest
``` ```
### With Nix/NixOS ### With Nix/NixOS
<details>
<summary>Click to expand</summary>
**From anywhere (using the repo URL):** **From anywhere (using the repo URL):**
```bash ```bash
@@ -59,6 +72,8 @@ environment.systemPackages = with pkgs; [ # or home.packages
]; ];
``` ```
</details>
## Usage ## Usage
```bash ```bash
@@ -67,18 +82,42 @@ gh-recon --username TARGET_USER [--token YOUR_TOKEN]
### Flags ### Flags
- `--username`: GitHub username to inspect (required) ```txt
- `--token`: Personal Access Token (optional but recommended) -u, --username string GitHub username to analyze
-t, --token string GitHub personal access token (e.g. ghp_...)
-e, --email string Search accounts by email address
-d, --deep Enable deep scan (clone repos, regex search, analyse licenses, etc.)
--max-size int Limit the size of repositories to scan (in MB) (only for deep scan) (default 150)
--exclude-repo string Exclude repos from deep scan (comma-separated list, only for deep scan)
-r, --refresh Refresh the cache (only for deep scan)
-c, --only-commits Display only commits with author info
-s, --silent Suppress all non-essential output
-j, --json string Write results to specified JSON file
```
## Example ## Example
```bash ```bash
gh-recon --username anotherhadi --token ghp_ABC123... gh-recon --username anotherhadi --token ghp_ABC123...
gh-recon --email [email protected]
gh-recon --username anotherhadi --json output.json --deep
``` ```
## Todo ## Cover your tracks
Feel free to contribute! Here are some ideas: Understanding what information about you is publicly visible is the first step to managing your online presence. gh-recon can help you identify your own publicly available data on GitHub. Here’s how you can take steps to protect your privacy and security:
- Fetch names in License files - **Review your public profile**: Regularly check your GitHub profile and repositories to ensure that you are not unintentionally exposing sensitive information.
- Fetch emails in README files/comments - **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.
- **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.
You can also use a tool like [TruffleHog](github.com/trufflesecurity/trufflehog) to scan your repositories specifically for exposed secrets and tokens.
**Useful links:**
- [Blocking command line pushes that expose your personal email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/blocking-command-line-pushes-that-expose-your-personal-email-address)
- [No-reply email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address)
## Contributing
Feel free to contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
+2 -2
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.1";
ldflags = [ "-s" "-w" ]; ldflags = [ "-s" "-w" ];
@@ -24,7 +24,7 @@
src = ./.; src = ./.;
vendorHash = "sha256-CPk8B8FKEoN8qff6WV/iBf0eVjTBMVfJQvlVcti6dfM="; vendorHash = "sha256-S8IzmdiVvBtnQQl0AewGZ1yuitvrdnVQ/Jf2230g3Mg=";
meta = with pkgs.lib; { meta = with pkgs.lib; {
description = description =
+138
View File
@@ -0,0 +1,138 @@
package ghrecon
import (
"fmt"
"sort"
)
type CloseFriendsResult struct {
Login string
Score int
}
const (
maxFollowingForTarget = 50
maxFollowersForFollowing = 20
pointPerCriterion = 1
)
// 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 (+1 point)
// 3. The target's following follows the target (+1 point)
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.Error("Failed to fetch user's following list", "user", username, "err", err)
r.PrintNewline()
return
}
WaitForRateLimit(resp)
if len(following) >= maxFollowingForTarget {
r.PrintInfo(
"INFO",
fmt.Sprintf(
"%s follows %d or more users (%d). Skipping close friends check.",
username,
maxFollowingForTarget,
len(following),
),
)
r.PrintNewline()
return
}
if len(following) == 0 {
r.PrintInfo("INFO", fmt.Sprintf("%s is not following anyone.", username))
r.PrintNewline()
return
}
for _, userBeingFollowedByTarget := range following {
loginName := userBeingFollowedByTarget.GetLogin()
if loginName == "" {
r.Logger.Warn("User in following list has an empty login", "target_user", username)
continue
}
currentScore := 0
userDetails, userResp, userErr := r.Client.Users.Get(r.Ctx, loginName)
if userErr != nil {
r.Logger.Warn(
"Failed to fetch details for followed user",
"followed_user",
loginName,
"err",
userErr,
)
if userResp != nil {
WaitForRateLimit(userResp)
}
continue
}
WaitForRateLimit(userResp)
if userDetails.GetFollowers() < maxFollowersForFollowing {
currentScore += pointPerCriterion
}
followsTargetBack, checkErr := r.checkIfUserFollows(loginName, username)
if checkErr != nil {
} else if followsTargetBack {
currentScore += pointPerCriterion
}
if currentScore > 0 {
response = append(response, CloseFriendsResult{
Login: loginName,
Score: currentScore,
})
}
}
if len(response) == 0 {
r.PrintInfo(
"INFO",
fmt.Sprintf("No close friends found for %s based on the criteria.", username),
)
} else {
sort.Slice(response, func(i, j int) bool {
return response[i].Score > response[j].Score
})
for i, friend := range response {
r.PrintInfo(
fmt.Sprintf("Friend n°%d", i+1),
"@"+friend.Login,
"Score: "+fmt.Sprintf("%d", friend.Score),
)
}
}
r.PrintNewline()
return
}
// checkIfUserFollows checks if sourceUserLogin follows targetUserLogin.
func (r Recon) checkIfUserFollows(sourceUserLogin, targetUserLogin string) (bool, error) {
isFollowing, resp, err := r.Client.Users.IsFollowing(r.Ctx, sourceUserLogin, targetUserLogin)
if err != nil {
r.Logger.Warn("Error checking if user follows target",
"source_user_checking", sourceUserLogin,
"target_user_to_check", targetUserLogin,
"err", err)
if resp != nil {
WaitForRateLimit(resp)
}
return false, err
}
if resp != nil {
WaitForRateLimit(resp)
}
return isFollowing, nil
}
+62 -41
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 author-date:%s", username, date),
fmt.Sprintf("author:%s", username), &github.SearchOptions{
opts, 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
} }
+208
View File
@@ -0,0 +1,208 @@
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
Size int
}
func (r Recon) Deep(username, excludeRepos string, refresh bool) (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(),
Size: repo.GetSize(),
})
}
}
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
if folderExists(tmp_folder) {
if refresh {
r.PrintInfo("INFO", "Deleting existing folder "+tmp_folder)
err := os.RemoveAll(tmp_folder)
if err != nil {
r.PrintInfo("ERROR", "Failed to delete existing folder "+tmp_folder)
}
}
}
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
}
maxRepoSize := r.MaxRepoSize * 1024
if repo.Size > maxRepoSize {
r.PrintInfo(
"INFO",
"Skipping repository "+repo.Owner+"/"+repo.Name+" due to size", fmt.Sprintf(
"%d",
repo.Size/1024,
)+"MB > "+fmt.Sprintf(
"%d",
maxRepoSize/1024,
)+"MB",
)
continue
}
r.PrintInfo(
"Downloading",
repo.Owner+"/"+repo.Name,
fmt.Sprintf("%d", repo.Size/1024)+"MB",
)
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, "found in:"+strings.Join(email.FoundIn, ", "))
}
}
r.PrintNewline()
return
}
+98
View File
@@ -0,0 +1,98 @@
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")
}
r.PrintNewline()
return
}
+157 -74
View File
@@ -4,108 +4,191 @@ import (
"fmt" "fmt"
) )
func (r Recon) SshKeys(username string) { type SSHKeyResult struct {
sshKeys, resp, err := r.client.Users.ListKeys(r.ctx, username, nil) 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 { 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)
if i != len(sshKeys)-1 {
r.PrintNewline()
} }
} }
}
r.PrintNewline()
WaitForRateLimit(resp) WaitForRateLimit(resp)
return
} }
func (r Recon) GpgKeys(username string) { type GPGKeyEmail struct {
gpgKeys, resp, err := r.client.Users.ListGPGKeys(r.ctx, username, nil) 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 { 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)
if j != len(k.Emails)-1 {
r.PrintNewline()
} }
} }
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)
if j != len(k.Subkeys)-1 {
r.PrintNewline()
}
}
if i != len(gpgKeys)-1 {
r.PrintNewline()
}
}
}
r.PrintNewline()
WaitForRateLimit(resp) WaitForRateLimit(resp)
return
} }
func (r Recon) SshSigningKeys(username string) { type SSHSigningKeyResult struct {
signingKeys, resp, err := r.client.Users.ListSSHSigningKeys( ID string
r.ctx, Title string
CreatedAt string
Key string
}
func (r Recon) SshSigningKeys(username string) (response []SSHSigningKeyResult) {
signingKeys, resp, err := r.Client.Users.ListSSHSigningKeys(
r.Ctx,
username, username,
nil, nil,
) )
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)
if i != len(signingKeys)-1 {
r.PrintNewline()
}
response = append(response, k)
} }
} }
WaitForRateLimit(resp) WaitForRateLimit(resp)
r.PrintNewline()
return response
} }
+6 -11
View File
@@ -8,15 +8,10 @@ import (
) )
type Recon struct { 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 { MaxRepoSize int
return &Recon{
client: client,
logger: logger,
ctx: ctx,
}
} }
+30 -20
View File
@@ -4,31 +4,41 @@ import (
"fmt" "fmt"
) )
func (r Recon) Orgs(username string) { type OrgResult struct {
orgs, resp, err := r.client.Organizations.List(r.ctx, username, nil) 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 { 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())
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()
} }
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)
if i != len(orgs)-1 {
r.PrintNewline()
} }
response = append(response, o)
}
}
r.PrintNewline()
WaitForRateLimit(resp) WaitForRateLimit(resp)
return
} }
+19 -18
View File
@@ -5,36 +5,37 @@ import (
"fmt" "fmt"
) )
func (r Recon) Socials(username string) { type SocialResult struct {
resp, err := FetchGitHubAPI(r.client, "", "/users/"+username+"/social_accounts") 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 { 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)
return return
} }
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
} }
+78 -31
View File
@@ -4,42 +4,89 @@ import (
"fmt" "fmt"
) )
func (r Recon) User(username string) { type UserResult struct {
user, resp, err := r.client.Users.Get(r.ctx, username) 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 { if resp.StatusCode == 404 {
r.logger.Fatal("User not found") r.Logger.Fatal("User not found")
} }
if err != nil { if err != nil {
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
} }
+59 -5
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 {
@@ -59,7 +66,9 @@ func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("error executing request for %s: %w", url, err) return nil, fmt.Errorf("error executing request for %s: %w", url, err)
} }
defer resp.Body.Close() defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
bodyBytes, _ := io.ReadAll(resp.Body) bodyBytes, _ := io.ReadAll(resp.Body)
@@ -83,12 +92,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 +133,35 @@ 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 func() {
_ = 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=
+134 -41
View File
@@ -2,63 +2,156 @@ package main
import ( import (
"context" "context"
"flag"
"fmt"
"os" "os"
"strings"
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
var deep bool
var silent bool
var jsonFile string
var excludeRepos string
var maxRepoSize int
var refresh bool
// FLAGS
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(
&deep,
"deep",
"d",
false,
"Enable deep scan (clone repos, regex search, analyse licenses, etc.)",
)
flag.IntVar(
&maxRepoSize,
"max-size",
150,
"Limit the size of repositories to scan (in MB) (only for deep scan)",
)
flag.StringVar(
&excludeRepos,
"exclude-repo",
"",
"Exclude repos from deep scan (comma-separated list, only for deep scan)",
)
flag.BoolVarP(
&refresh,
"refresh",
"r",
false,
"Refresh the cache (only for deep scan)",
)
flag.BoolVarP(
&onlyCommitsLeak,
"only-commits",
"c",
false,
"Display only commits with author info",
)
flag.BoolVarP(&silent, "silent", "s", false, "Suppress all non-essential output")
flag.StringVarP(&jsonFile, "json", "j", "", "Write results to specified JSON file")
// FLAGS SETTINGS
flag.CommandLine.SetNormalizeFunc(wordSepNormalizeFunc)
flag.CommandLine.SortFlags = false
flag.Parse() flag.Parse()
if username == "" { // INITIALIZE RECON OBJECT
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) r := &ghrecon.Recon{
if token == "" { Client: github.NewClient(nil),
log.Info( Logger: log.NewWithOptions(os.Stderr, log.Options{
"It's recommended to set a Github token for better rate limits. You can set it using the --token flag.",
)
} else {
client = client.WithAuthToken(token)
}
ctx := context.Background()
styles := log.DefaultStyles()
styles.Levels[log.InfoLevel] = styles.Levels[log.InfoLevel].Foreground(ghrecon.Grey)
logger := log.NewWithOptions(os.Stderr, log.Options{
ReportCaller: false, ReportCaller: false,
ReportTimestamp: false, ReportTimestamp: false,
}) }),
logger.SetStyles(styles) Ctx: context.Background(),
Silent: silent,
JsonFile: jsonFile,
MaxRepoSize: maxRepoSize,
}
r := ghrecon.NewRecon( // CHECK FLAGS
client,
logger, if username == "" && fromEmail == "" {
ctx, r.Logger.Fatal(
"Please provide a username with the --username (-u) flag or an email with the --email (-e) flag",
) )
} else if username != "" {
username = strings.TrimPrefix(username, "@")
if err := ghrecon.ParseUsername(username); err != nil {
r.Logger.Fatal("Invalid username", "err", err)
}
}
ghrecon.Header() if token == "" {
r.User(username) r.PrintInfo(
r.Orgs(username) "INFO",
r.SshKeys(username) "It's recommended to set a Github token for better rate limits. You can set it using the --token (-t) flag.",
r.GpgKeys(username) )
r.SshSigningKeys(username) } else {
r.Socials(username) r.Client = r.Client.WithAuthToken(token)
r.Commits(username) }
// START
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, refresh)
}
r.WriteJson(results)
} }
+16
View File
@@ -0,0 +1,16 @@
package main
import (
"strings"
flag "github.com/spf13/pflag"
)
func wordSepNormalizeFunc(f *flag.FlagSet, name string) flag.NormalizedName {
from := []string{".", "_"}
to := "-"
for _, sep := range from {
name = strings.ReplaceAll(name, sep, to)
}
return flag.NormalizedName(name)
}