mirror of
https://github.com/anotherhadi/github-recon.git
synced 2026-10-05 10:58:25 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0041c0c132 | ||
|
|
d3fdfdcdc5 | ||
|
|
4f087cb7f0 | ||
|
|
befb546292 | ||
|
|
9a825bc7cc | ||
|
|
72b8635832 | ||
|
|
2ba8695674 | ||
|
|
f8eb7d58ba | ||
|
|
637d9811f2 | ||
|
|
c498e7bfdd | ||
|
|
7e2b6dd426 | ||
|
|
05b449afcd | ||
|
|
b4392f972d | ||
|
|
e4cec2b07a | ||
|
|
15458ab78e | ||
|
|
deec50febf | ||
|
|
de47073083 | ||
|
|
a3f4b19382 | ||
|
|
afdd30d34b | ||
|
|
58ac92bff8 | ||
|
|
ce96d1f307 | ||
|
|
bd734f11af | ||
|
|
1ff0e222c7 |
Binary file not shown.
|
After Width: | Height: | Size: 192 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
@@ -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.
|
||||
@@ -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
|
||||
|
||||
<p>
|
||||
@@ -16,9 +22,9 @@ Fetches and aggregates public OSINT data for a GitHub user, leveraging Go and th
|
||||
- List organizations and roles
|
||||
- Fetch SSH and GPG keys
|
||||
- Enumerate social accounts
|
||||
- Extract unique commit authors (name + email) in both chronological orders
|
||||
- Extract unique commit authors (name + email)
|
||||
- Find close friends
|
||||
- Search using an email address
|
||||
- Find Github accounts using an email address
|
||||
- Export results to JSON
|
||||
- Deep scan option (clone repositories, regex search, analyze licenses, etc.)
|
||||
|
||||
@@ -36,11 +42,14 @@ This tool is intended for educational purposes only. Use responsibly and ensure
|
||||
### With Go
|
||||
|
||||
```bash
|
||||
go get github.com/anotherhadi/gh-recon
|
||||
go install github.com/anotherhadi/gh-recon@latest
|
||||
```
|
||||
|
||||
### With Nix/NixOS
|
||||
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
**From anywhere (using the repo URL):**
|
||||
|
||||
```bash
|
||||
@@ -63,6 +72,8 @@ environment.systemPackages = with pkgs; [ # or home.packages
|
||||
];
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
@@ -71,37 +82,42 @@ gh-recon --username TARGET_USER [--token YOUR_TOKEN]
|
||||
|
||||
### Flags
|
||||
|
||||
- `--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
|
||||
-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
|
||||
|
||||
```bash
|
||||
gh-recon --username anotherhadi --token ghp_ABC123...
|
||||
gh-recon --email [email protected] --token ghp_ABC123...
|
||||
gh-recon --email [email protected]
|
||||
gh-recon --username anotherhadi --json output.json --deep
|
||||
```
|
||||
|
||||
## Todo
|
||||
## Cover your tracks
|
||||
|
||||
Feel free to contribute!
|
||||
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:
|
||||
|
||||
**Todo:**
|
||||
- **Review your public profile**: Regularly check your GitHub profile and repositories to ensure that you are not unintentionally exposing sensitive 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.
|
||||
- **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.
|
||||
|
||||
- Find and parse licenses
|
||||
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.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
(system: f system (import nixpkgs { inherit system; }));
|
||||
|
||||
pname = "gh-recon";
|
||||
version = "0.2.0";
|
||||
version = "0.2.1";
|
||||
|
||||
ldflags = [ "-s" "-w" ];
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
src = ./.;
|
||||
|
||||
vendorHash = "sha256-CPk8B8FKEoN8qff6WV/iBf0eVjTBMVfJQvlVcti6dfM=";
|
||||
vendorHash = "sha256-S8IzmdiVvBtnQQl0AewGZ1yuitvrdnVQ/Jf2230g3Mg=";
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description =
|
||||
|
||||
+110
-17
@@ -1,45 +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
|
||||
|
||||
// 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)
|
||||
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.Logger.Error("Failed to fetch user's following list", "user", username, "err", err)
|
||||
r.PrintNewline()
|
||||
return []CloseFriendsResult{}
|
||||
return
|
||||
}
|
||||
WaitForRateLimit(resp)
|
||||
for _, user := range following {
|
||||
followers, resp, err := r.client.Users.Get(r.ctx, user.GetLogin())
|
||||
WaitForRateLimit(resp)
|
||||
if err != nil {
|
||||
|
||||
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
|
||||
}
|
||||
if followers.GetFollowers() < 20 {
|
||||
|
||||
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: user.GetLogin(),
|
||||
Score: 1,
|
||||
Login: loginName,
|
||||
Score: currentScore,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, friend := range response {
|
||||
r.PrintInfo("Username", "@"+friend.Login)
|
||||
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
|
||||
}
|
||||
|
||||
+4
-4
@@ -20,8 +20,8 @@ func (r Recon) Commits(username string) (response []CommitsResult) {
|
||||
|
||||
collect := func(date string) error {
|
||||
for page := 1; page <= 10; page++ {
|
||||
result, resp, err := r.client.Search.Commits(
|
||||
r.ctx,
|
||||
result, resp, err := r.Client.Search.Commits(
|
||||
r.Ctx,
|
||||
fmt.Sprintf("author:%s author-date:%s", username, date),
|
||||
&github.SearchOptions{
|
||||
Sort: "author-date",
|
||||
@@ -40,7 +40,7 @@ func (r Recon) Commits(username string) (response []CommitsResult) {
|
||||
name := item.Commit.GetAuthor().GetName()
|
||||
email := item.Commit.GetAuthor().GetEmail()
|
||||
if SkipResult(name, email) {
|
||||
// continue
|
||||
continue
|
||||
}
|
||||
if _, seen := results[name+" - "+email]; !seen {
|
||||
author := CommitsResult{
|
||||
@@ -71,7 +71,7 @@ func (r Recon) Commits(username string) (response []CommitsResult) {
|
||||
">2026-01-01",
|
||||
} {
|
||||
if err := collect(date); err != nil {
|
||||
r.logger.Error("Failed to fetch commits", "err", err, "date", date)
|
||||
r.Logger.Error("Failed to fetch commits", "err", err, "date", date)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+38
-9
@@ -82,19 +82,20 @@ type DeepResult struct {
|
||||
Repository string
|
||||
Owner string
|
||||
Name string
|
||||
Size int
|
||||
}
|
||||
|
||||
func (r Recon) Deep(username, excludeRepos string) (response []DeepResult) {
|
||||
func (r Recon) Deep(username, excludeRepos string, refresh bool) (response []DeepResult) {
|
||||
excludeReposList := strings.Split(excludeRepos, ",")
|
||||
repos, resp, err := r.client.Repositories.ListByUser(
|
||||
r.ctx,
|
||||
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)
|
||||
r.Logger.Error("Failed to fetch repositories", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -107,6 +108,7 @@ func (r Recon) Deep(username, excludeRepos string) (response []DeepResult) {
|
||||
Repository: repo.GetCloneURL(),
|
||||
Owner: repo.GetOwner().GetLogin(),
|
||||
Name: repo.GetName(),
|
||||
Size: repo.GetSize(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -119,15 +121,43 @@ func (r Recon) Deep(username, excludeRepos string) (response []DeepResult) {
|
||||
}
|
||||
|
||||
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 repository",
|
||||
"Downloading",
|
||||
repo.Owner+"/"+repo.Name,
|
||||
fmt.Sprintf("%d", repo.Size/1024)+"MB",
|
||||
)
|
||||
|
||||
destination := tmp_folder + "/" + repo.Owner + "/" + repo.Name
|
||||
@@ -144,7 +174,7 @@ func (r Recon) Deep(username, excludeRepos string) (response []DeepResult) {
|
||||
)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
r.logger.Error(
|
||||
r.Logger.Error(
|
||||
"ERROR",
|
||||
"Failed to clone repository",
|
||||
"err",
|
||||
@@ -160,7 +190,7 @@ func (r Recon) Deep(username, excludeRepos string) (response []DeepResult) {
|
||||
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)
|
||||
r.Logger.Error("Failed to find emails in directory", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -169,8 +199,7 @@ func (r Recon) Deep(username, excludeRepos string) (response []DeepResult) {
|
||||
} else {
|
||||
r.PrintInfo("INFO", "Found emails:")
|
||||
for _, email := range results {
|
||||
r.PrintInfo("Email", email.Email)
|
||||
r.PrintInfo("Found in", tmp_folder, email.FoundIn...)
|
||||
r.PrintInfo("Email", email.Email, "found in:"+strings.Join(email.FoundIn, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -21,8 +21,8 @@ func (r Recon) Email(email string) (response []EmailResult) {
|
||||
|
||||
collect := func(date string) error {
|
||||
for page := 1; page <= 10; page++ {
|
||||
result, resp, err := r.client.Search.Commits(
|
||||
r.ctx,
|
||||
result, resp, err := r.Client.Search.Commits(
|
||||
r.Ctx,
|
||||
fmt.Sprintf("author-email:%s author-date:%s", email, date),
|
||||
&github.SearchOptions{
|
||||
Sort: "author-date",
|
||||
@@ -77,7 +77,7 @@ func (r Recon) Email(email string) (response []EmailResult) {
|
||||
">2026-01-01",
|
||||
} {
|
||||
if err := collect(date); err != nil {
|
||||
r.logger.Error("Failed to fetch commits", "err", err, "date", date)
|
||||
r.Logger.Error("Failed to fetch commits", "err", err, "date", date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,5 +92,7 @@ func (r Recon) Email(email string) (response []EmailResult) {
|
||||
if len(results) == 0 {
|
||||
r.PrintInfo("INFO", "No commits found")
|
||||
}
|
||||
|
||||
r.PrintNewline()
|
||||
return
|
||||
}
|
||||
|
||||
+22
-11
@@ -17,9 +17,9 @@ type SSHKeyResult struct {
|
||||
}
|
||||
|
||||
func (r Recon) SshKeys(username string) (response []SSHKeyResult) {
|
||||
sshKeys, resp, err := r.client.Users.ListKeys(r.ctx, username, nil)
|
||||
sshKeys, resp, err := r.Client.Users.ListKeys(r.Ctx, username, nil)
|
||||
if err != nil {
|
||||
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 {
|
||||
r.PrintTitle("🔑 SSH Keys")
|
||||
r.PrintInfo("INFO", "No SSH Keys found")
|
||||
@@ -48,7 +48,9 @@ func (r Recon) SshKeys(username string) (response []SSHKeyResult) {
|
||||
r.PrintInfo("Verified", k.Verified)
|
||||
r.PrintInfo("Last Used", k.LastUsed)
|
||||
r.PrintInfo("Added By", k.AddedBy)
|
||||
r.PrintNewline()
|
||||
if i != len(sshKeys)-1 {
|
||||
r.PrintNewline()
|
||||
}
|
||||
}
|
||||
}
|
||||
r.PrintNewline()
|
||||
@@ -72,9 +74,9 @@ type GPGKeyResult struct {
|
||||
}
|
||||
|
||||
func (r Recon) GpgKeys(username string) (response []GPGKeyResult) {
|
||||
gpgKeys, resp, err := r.client.Users.ListGPGKeys(r.ctx, username, nil)
|
||||
gpgKeys, resp, err := r.Client.Users.ListGPGKeys(r.Ctx, username, nil)
|
||||
if err != nil {
|
||||
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 {
|
||||
r.PrintTitle("🗝️ GPG Keys")
|
||||
r.PrintInfo("INFO", "No GPG Keys found")
|
||||
@@ -122,6 +124,9 @@ func (r Recon) GpgKeys(username string) (response []GPGKeyResult) {
|
||||
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 {
|
||||
@@ -131,9 +136,13 @@ func (r Recon) GpgKeys(username string) (response []GPGKeyResult) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
r.PrintNewline()
|
||||
@@ -149,13 +158,13 @@ type SSHSigningKeyResult struct {
|
||||
}
|
||||
|
||||
func (r Recon) SshSigningKeys(username string) (response []SSHSigningKeyResult) {
|
||||
signingKeys, resp, err := r.client.Users.ListSSHSigningKeys(
|
||||
r.ctx,
|
||||
signingKeys, resp, err := r.Client.Users.ListSSHSigningKeys(
|
||||
r.Ctx,
|
||||
username,
|
||||
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 {
|
||||
r.PrintTitle("📝 SSH Signing Keys")
|
||||
r.PrintInfo("INFO", "No SSH Signing Keys found")
|
||||
@@ -173,7 +182,9 @@ func (r Recon) SshSigningKeys(username string) (response []SSHSigningKeyResult)
|
||||
r.PrintInfo("Title", k.Title)
|
||||
r.PrintInfo("Created At", k.CreatedAt)
|
||||
r.PrintInfo("Key", k.Key)
|
||||
r.PrintNewline()
|
||||
if i != len(signingKeys)-1 {
|
||||
r.PrintNewline()
|
||||
}
|
||||
response = append(response, k)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-21
@@ -8,25 +8,10 @@ import (
|
||||
)
|
||||
|
||||
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,
|
||||
silent bool,
|
||||
jsonFile string,
|
||||
) *Recon {
|
||||
return &Recon{
|
||||
client: client,
|
||||
logger: logger,
|
||||
ctx: ctx,
|
||||
silent: silent,
|
||||
jsonFile: jsonFile,
|
||||
}
|
||||
Client *github.Client
|
||||
Logger *log.Logger
|
||||
Ctx context.Context
|
||||
Silent bool
|
||||
JsonFile string
|
||||
MaxRepoSize int
|
||||
}
|
||||
|
||||
+5
-3
@@ -12,9 +12,9 @@ type OrgResult struct {
|
||||
}
|
||||
|
||||
func (r Recon) Orgs(username string) (response []OrgResult) {
|
||||
orgs, resp, err := r.client.Organizations.List(r.ctx, username, nil)
|
||||
orgs, resp, err := r.Client.Organizations.List(r.Ctx, username, nil)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to fetch organizations", "err", err)
|
||||
r.Logger.Error("Failed to fetch organizations", "err", err)
|
||||
} else if len(orgs) == 0 {
|
||||
r.PrintTitle("🏢 Organizations")
|
||||
r.PrintInfo("INFO", "No Organizations found")
|
||||
@@ -32,7 +32,9 @@ func (r Recon) Orgs(username string) (response []OrgResult) {
|
||||
r.PrintInfo("ID", o.ID)
|
||||
r.PrintInfo("URL", o.URL)
|
||||
r.PrintInfo("Description", o.Description)
|
||||
r.PrintNewline()
|
||||
if i != len(orgs)-1 {
|
||||
r.PrintNewline()
|
||||
}
|
||||
response = append(response, o)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -11,16 +11,16 @@ type SocialResult struct {
|
||||
}
|
||||
|
||||
func (r Recon) Socials(username string) (response []SocialResult) {
|
||||
resp, err := FetchGitHubAPI(r.client, "", "/users/"+username+"/social_accounts")
|
||||
resp, err := FetchGitHubAPI(r.Client, "", "/users/"+username+"/social_accounts")
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to fetch socials", "err", err)
|
||||
r.Logger.Error("Failed to fetch socials", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
var socialAccounts []SocialResult
|
||||
err = json.Unmarshal(resp, &socialAccounts)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to unmarshal socials", "err", err)
|
||||
r.Logger.Error("Failed to unmarshal socials", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -30,12 +30,12 @@ type UserResult struct {
|
||||
}
|
||||
|
||||
func (r Recon) User(username string) (response UserResult) {
|
||||
user, resp, err := r.client.Users.Get(r.ctx, username)
|
||||
user, resp, err := r.Client.Users.Get(r.Ctx, username)
|
||||
if resp.StatusCode == 404 {
|
||||
r.logger.Fatal("User not found")
|
||||
r.Logger.Fatal("User not found")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
r.PrintTitle("👤 User informations")
|
||||
|
||||
+15
-11
@@ -25,7 +25,7 @@ var (
|
||||
)
|
||||
|
||||
func (r Recon) Header() {
|
||||
if r.silent {
|
||||
if r.Silent {
|
||||
return
|
||||
}
|
||||
asciiArt := " __ \n ___ _/ / _______ _______ ___ \n / _ `/ _ \\/ __/ -_) __/ _ \\/ _ \\\n \\_, /_//_/_/ \\__/\\__/\\___/_//_/\n/___/ "
|
||||
@@ -66,7 +66,9 @@ func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) {
|
||||
if err != nil {
|
||||
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 {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
@@ -91,14 +93,14 @@ func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (r Recon) PrintNewline() {
|
||||
if r.silent {
|
||||
if r.Silent {
|
||||
return
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func (r Recon) PrintTitle(title string) {
|
||||
if r.silent {
|
||||
if r.Silent {
|
||||
return
|
||||
}
|
||||
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7287fd"))
|
||||
@@ -106,7 +108,7 @@ func (r Recon) PrintTitle(title string) {
|
||||
}
|
||||
|
||||
func (r Recon) PrintInfo(key, value string, more ...string) {
|
||||
if r.silent {
|
||||
if r.Silent {
|
||||
return
|
||||
}
|
||||
if value == "" || value == "0001-01-01 00:00:00 +0000 UTC" {
|
||||
@@ -144,20 +146,22 @@ func SkipResult(name, email string) bool {
|
||||
}
|
||||
|
||||
func (r Recon) WriteJson(data any) {
|
||||
if r.jsonFile == "" {
|
||||
if r.JsonFile == "" {
|
||||
return
|
||||
}
|
||||
file, err := os.Create(r.jsonFile)
|
||||
file, err := os.Create(r.JsonFile)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to create JSON file", "err", err)
|
||||
r.Logger.Error("Failed to create JSON file", "err", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
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)
|
||||
r.Logger.Error("Failed to write to JSON file", "err", err)
|
||||
return
|
||||
}
|
||||
r.PrintInfo("INFO", "JSON file created successfully", "file", r.jsonFile)
|
||||
r.PrintInfo("INFO", "JSON file created successfully", "file", r.JsonFile)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
ghrecon "github.com/anotherhadi/gh-recon/gh-recon"
|
||||
"github.com/charmbracelet/log"
|
||||
@@ -19,16 +20,13 @@ func main() {
|
||||
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(
|
||||
&onlyCommitsLeak,
|
||||
"only-commits",
|
||||
"c",
|
||||
false,
|
||||
"Display only commits with author info",
|
||||
)
|
||||
flag.BoolVarP(
|
||||
&deep,
|
||||
"deep",
|
||||
@@ -36,60 +34,81 @@ func main() {
|
||||
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.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)",
|
||||
"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()
|
||||
|
||||
styles := log.DefaultStyles()
|
||||
styles.Levels[log.InfoLevel] = styles.Levels[log.InfoLevel].Foreground(ghrecon.Grey)
|
||||
logger := log.NewWithOptions(os.Stderr, log.Options{
|
||||
ReportCaller: false,
|
||||
ReportTimestamp: false,
|
||||
})
|
||||
logger.SetStyles(styles)
|
||||
// INITIALIZE RECON OBJECT
|
||||
|
||||
r := &ghrecon.Recon{
|
||||
Client: github.NewClient(nil),
|
||||
Logger: log.NewWithOptions(os.Stderr, log.Options{
|
||||
ReportCaller: false,
|
||||
ReportTimestamp: false,
|
||||
}),
|
||||
Ctx: context.Background(),
|
||||
Silent: silent,
|
||||
JsonFile: jsonFile,
|
||||
MaxRepoSize: maxRepoSize,
|
||||
}
|
||||
|
||||
// CHECK FLAGS
|
||||
|
||||
if username == "" && fromEmail == "" {
|
||||
logger.Error(
|
||||
r.Logger.Fatal(
|
||||
"Please provide a username with the --username (-u) flag or an email with the --email (-e) flag",
|
||||
)
|
||||
os.Exit(1)
|
||||
} else if username != "" {
|
||||
username = strings.TrimPrefix(username, "@")
|
||||
if err := ghrecon.ParseUsername(username); err != nil {
|
||||
logger.Error("Invalid username", "err", err)
|
||||
os.Exit(1)
|
||||
r.Logger.Fatal("Invalid username", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
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.",
|
||||
)
|
||||
}
|
||||
r.PrintInfo(
|
||||
"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)
|
||||
r.Client = r.Client.WithAuthToken(token)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
r := ghrecon.NewRecon(
|
||||
client,
|
||||
logger,
|
||||
ctx,
|
||||
silent,
|
||||
jsonFile,
|
||||
)
|
||||
// START
|
||||
|
||||
r.Header()
|
||||
|
||||
|
||||
if fromEmail != "" {
|
||||
emailsInfo := r.Email(fromEmail)
|
||||
r.WriteJson(
|
||||
@@ -120,18 +139,18 @@ func main() {
|
||||
commitsInfo := r.Commits(username)
|
||||
|
||||
results := map[string]any{
|
||||
"User": userInfo,
|
||||
"Orgs": orgsInfo,
|
||||
"SSHKeys": sshKeysInfo,
|
||||
"GPGKeys": gpgKeysInfo,
|
||||
"SSHSigningKeys": sshSigningKeysInfo,
|
||||
"Socials": socialsInfo,
|
||||
"Commits": commitsInfo,
|
||||
"CloseFriends": closeFriendsInfo,
|
||||
}
|
||||
"User": userInfo,
|
||||
"Orgs": orgsInfo,
|
||||
"SSHKeys": sshKeysInfo,
|
||||
"GPGKeys": gpgKeysInfo,
|
||||
"SSHSigningKeys": sshSigningKeysInfo,
|
||||
"Socials": socialsInfo,
|
||||
"Commits": commitsInfo,
|
||||
"CloseFriends": closeFriendsInfo,
|
||||
}
|
||||
|
||||
if deep {
|
||||
results["Deep"] = r.Deep(username, excludeRepos)
|
||||
results["Deep"] = r.Deep(username, excludeRepos, refresh)
|
||||
}
|
||||
|
||||
r.WriteJson(results)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user