Compare commits

...
23 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
17 changed files with 346 additions and 159 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.
+40 -24
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
<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.
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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(
"Downloading repository",
"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
@@ -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
View File
@@ -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
}
+20 -9
View File
@@ -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,9 +48,11 @@ 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)
if i != len(sshKeys)-1 {
r.PrintNewline()
}
}
}
r.PrintNewline()
WaitForRateLimit(resp)
return
@@ -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,11 +136,15 @@ 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()
WaitForRateLimit(resp)
return
@@ -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)
if i != len(signingKeys)-1 {
r.PrintNewline()
}
response = append(response, k)
}
}
+6 -21
View File
@@ -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
}
+4 -2
View File
@@ -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)
if i != len(orgs)-1 {
r.PrintNewline()
}
response = append(response, o)
}
}
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+54 -35
View File
@@ -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{
// INITIALIZE RECON OBJECT
r := &ghrecon.Recon{
Client: github.NewClient(nil),
Logger: log.NewWithOptions(os.Stderr, log.Options{
ReportCaller: false,
ReportTimestamp: false,
})
logger.SetStyles(styles)
}),
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(
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(
@@ -131,7 +150,7 @@ func main() {
}
if deep {
results["Deep"] = r.Deep(username, excludeRepos)
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)
}