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 | ||
|
|
5a9483411a | ||
|
|
7f74f5d28f | ||
|
|
bd99649b4e | ||
|
|
be6a9fbc6b | ||
|
|
4b75b6f755 | ||
|
|
d02479748e | ||
|
|
9eee8e60be | ||
|
|
4a3a06fa30 |
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,5 +1,17 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/anotherhadi/gh-recon/main/.github/assets/logo.png" width="120px" />
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
# GH-Recon
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/anotherhadi/gh-recon/releases"><img src="https://img.shields.io/github/release/anotherhadi/gh-recon.svg" alt="Latest Release"></a>
|
||||
<a href="https://pkg.go.dev/github.com/anotherhadi/gh-recon?tab=doc"><img src="https://godoc.org/github.com/anotherhadi/gh-recon?status.svg" alt="GoDoc"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/anotherhadi/gh-recon"><img src="https://goreportcard.com/badge/github.com/anotherhadi/gh-recon" alt="GoReportCard"></a>
|
||||
</p>
|
||||
|
||||
## Project Overview
|
||||
|
||||
Fetches and aggregates public OSINT data for a GitHub user, leveraging Go and the GitHub API.
|
||||
@@ -10,7 +22,11 @@ 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
|
||||
- Find Github accounts using an email address
|
||||
- Export results to JSON
|
||||
- Deep scan option (clone repositories, regex search, analyze licenses, etc.)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
@@ -19,18 +35,21 @@ This tool is intended for educational purposes only. Use responsibly and ensure
|
||||
## Prerequisites
|
||||
|
||||
- Go 1.18+
|
||||
- GitHub Personal Access Token (recommended for higher rate limits)
|
||||
- GitHub Personal Access Token (recommended for higher rate limits): Create a GitHub API token with no permissions/no scope. This will be equivalent to public GitHub access, but it will allow access to use the GitHub Search API.
|
||||
|
||||
## Installation
|
||||
|
||||
### 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
|
||||
@@ -53,6 +72,8 @@ environment.systemPackages = with pkgs; [ # or home.packages
|
||||
];
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
@@ -61,18 +82,42 @@ gh-recon --username TARGET_USER [--token YOUR_TOKEN]
|
||||
|
||||
### Flags
|
||||
|
||||
- `--username`: GitHub username to inspect (required)
|
||||
- `--token`: Personal Access Token (optional but recommended)
|
||||
```txt
|
||||
-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]
|
||||
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
|
||||
- Fetch emails in README files/comments
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/go-github/v72/github"
|
||||
)
|
||||
|
||||
func commits(client *github.Client, ctx context.Context, username string) {
|
||||
// Commits
|
||||
fmt.Println(
|
||||
GreyStyle.Render("[")+GreenStyle.Render("+")+GreyStyle.Render("]"),
|
||||
GreyStyle.Render("Commits:\n"),
|
||||
)
|
||||
|
||||
seenNames := make(map[string]struct{})
|
||||
seenEmails := make(map[string]struct{})
|
||||
var names, emails []string
|
||||
|
||||
collect := func(order string) error {
|
||||
opts := &github.SearchOptions{
|
||||
Sort: "author-date",
|
||||
Order: order, // "desc" ou "asc"
|
||||
ListOptions: github.ListOptions{PerPage: 100},
|
||||
}
|
||||
for page := 1; page <= 10; page++ {
|
||||
opts.ListOptions.Page = page
|
||||
result, resp, err := client.Search.Commits(
|
||||
ctx,
|
||||
fmt.Sprintf("author:%s", username),
|
||||
opts,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch page %d (%s): %w", page, order, err)
|
||||
}
|
||||
WaitForRateLimit(resp)
|
||||
if len(result.Commits) == 0 {
|
||||
break
|
||||
}
|
||||
for _, item := range result.Commits {
|
||||
a := item.Commit.GetAuthor()
|
||||
name := a.GetName()
|
||||
email := a.GetEmail()
|
||||
if _, seen := seenNames[name]; !seen {
|
||||
seenNames[name] = struct{}{}
|
||||
names = append(names, name)
|
||||
}
|
||||
if _, seen := seenEmails[email]; !seen {
|
||||
seenEmails[email] = struct{}{}
|
||||
emails = append(emails, email)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := collect("desc"); err != nil {
|
||||
log.Error("Failed to fetch commits", "err", err)
|
||||
return
|
||||
}
|
||||
if err := collect("asc"); err != nil {
|
||||
log.Error("Failed to fetch commits", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
for i, name := range names {
|
||||
PrintInfo("Name "+fmt.Sprint(i+1), name)
|
||||
}
|
||||
for i, email := range emails {
|
||||
PrintInfo("Email "+fmt.Sprint(i+1), email)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
(system: f system (import nixpkgs { inherit system; }));
|
||||
|
||||
pname = "gh-recon";
|
||||
version = "0.1.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 =
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package ghrecon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-github/v72/github"
|
||||
)
|
||||
|
||||
type CommitsResult struct {
|
||||
Name string
|
||||
Email string
|
||||
Occurences int
|
||||
FirstFoundIn string
|
||||
}
|
||||
|
||||
func (r Recon) Commits(username string) (response []CommitsResult) {
|
||||
r.PrintTitle("🐙 Commits")
|
||||
|
||||
results := make(map[string]CommitsResult)
|
||||
|
||||
collect := func(date string) error {
|
||||
for page := 1; page <= 10; page++ {
|
||||
result, resp, err := r.Client.Search.Commits(
|
||||
r.Ctx,
|
||||
fmt.Sprintf("author:%s author-date:%s", username, date),
|
||||
&github.SearchOptions{
|
||||
Sort: "author-date",
|
||||
Order: "desc",
|
||||
ListOptions: github.ListOptions{PerPage: 100, Page: page},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch page %d (%s): %w", page, 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()
|
||||
if SkipResult(name, email) {
|
||||
continue
|
||||
}
|
||||
if _, seen := results[name+" - "+email]; !seen {
|
||||
author := CommitsResult{
|
||||
Name: name,
|
||||
Email: email,
|
||||
Occurences: 1,
|
||||
FirstFoundIn: item.GetRepository().Owner.GetLogin() + "/" + item.GetRepository().
|
||||
GetName(),
|
||||
}
|
||||
results[name+" - "+email] = author
|
||||
} else {
|
||||
result := results[name+" - "+email]
|
||||
result.Occurences++
|
||||
results[name+" - "+email] = result
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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,
|
||||
"first from "+result.FirstFoundIn+" (x"+fmt.Sprint(result.Occurences)+")",
|
||||
)
|
||||
response = append(response, result)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
r.PrintInfo("INFO", "No commits found")
|
||||
}
|
||||
r.PrintNewline()
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package ghrecon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type SSHKeyResult struct {
|
||||
ID string
|
||||
Url string
|
||||
Title string
|
||||
CreatedAt string
|
||||
Key string
|
||||
ReadOnly string
|
||||
Verified string
|
||||
LastUsed string
|
||||
AddedBy string
|
||||
}
|
||||
|
||||
func (r Recon) SshKeys(username string) (response []SSHKeyResult) {
|
||||
sshKeys, resp, err := r.Client.Users.ListKeys(r.Ctx, username, nil)
|
||||
if err != nil {
|
||||
r.Logger.Error("Failed to fetch ssh keys", "err", err)
|
||||
} else if len(sshKeys) == 0 {
|
||||
r.PrintTitle("🔑 SSH Keys")
|
||||
r.PrintInfo("INFO", "No SSH Keys found")
|
||||
} else {
|
||||
r.PrintTitle("🔑 SSH Keys")
|
||||
for i, key := range sshKeys {
|
||||
k := SSHKeyResult{
|
||||
ID: fmt.Sprintf("%d", key.GetID()),
|
||||
Url: key.GetURL(),
|
||||
Title: key.GetTitle(),
|
||||
CreatedAt: key.GetCreatedAt().String(),
|
||||
Key: key.GetKey(),
|
||||
ReadOnly: fmt.Sprintf("%t", key.GetReadOnly()),
|
||||
Verified: fmt.Sprintf("%t", key.GetVerified()),
|
||||
LastUsed: key.GetLastUsed().String(),
|
||||
AddedBy: key.GetAddedBy(),
|
||||
}
|
||||
response = append(response, k)
|
||||
r.PrintInfo("Key n°", fmt.Sprintf("%d", i))
|
||||
r.PrintInfo("ID", k.ID)
|
||||
r.PrintInfo("URL", k.Url)
|
||||
r.PrintInfo("Title", k.Title)
|
||||
r.PrintInfo("Created At", k.CreatedAt)
|
||||
r.PrintInfo("Key", k.Key)
|
||||
r.PrintInfo("Read Only", k.ReadOnly)
|
||||
r.PrintInfo("Verified", k.Verified)
|
||||
r.PrintInfo("Last Used", k.LastUsed)
|
||||
r.PrintInfo("Added By", k.AddedBy)
|
||||
if i != len(sshKeys)-1 {
|
||||
r.PrintNewline()
|
||||
}
|
||||
}
|
||||
}
|
||||
r.PrintNewline()
|
||||
WaitForRateLimit(resp)
|
||||
return
|
||||
}
|
||||
|
||||
type GPGKeyEmail struct {
|
||||
Email string
|
||||
Verified string
|
||||
}
|
||||
type GPGKeyResult struct {
|
||||
ID string
|
||||
KeyID string
|
||||
PublicKey string
|
||||
CreatedAt string
|
||||
PrimaryKeyID string
|
||||
RawKey string
|
||||
Emails []GPGKeyEmail
|
||||
Subkeys []GPGKeyResult
|
||||
}
|
||||
|
||||
func (r Recon) GpgKeys(username string) (response []GPGKeyResult) {
|
||||
gpgKeys, resp, err := r.Client.Users.ListGPGKeys(r.Ctx, username, nil)
|
||||
if err != nil {
|
||||
r.Logger.Error("Failed to fetch user's gpg keys", "err", err)
|
||||
} else if len(gpgKeys) == 0 {
|
||||
r.PrintTitle("🗝️ GPG Keys")
|
||||
r.PrintInfo("INFO", "No GPG Keys found")
|
||||
} else {
|
||||
r.PrintTitle("🗝️ GPG Keys")
|
||||
for i, key := range gpgKeys {
|
||||
k := GPGKeyResult{
|
||||
ID: fmt.Sprintf("%d", key.GetID()),
|
||||
KeyID: key.GetKeyID(),
|
||||
PublicKey: key.GetPublicKey(),
|
||||
CreatedAt: key.GetCreatedAt().String(),
|
||||
PrimaryKeyID: fmt.Sprintf("%d", key.GetPrimaryKeyID()),
|
||||
RawKey: key.GetRawKey(),
|
||||
Emails: []GPGKeyEmail{},
|
||||
Subkeys: []GPGKeyResult{},
|
||||
}
|
||||
for _, email := range key.Emails {
|
||||
email := GPGKeyEmail{
|
||||
Email: email.GetEmail(),
|
||||
Verified: fmt.Sprintf("%t", email.GetVerified()),
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
type SSHSigningKeyResult struct {
|
||||
ID string
|
||||
Title string
|
||||
CreatedAt string
|
||||
Key string
|
||||
}
|
||||
|
||||
func (r Recon) SshSigningKeys(username string) (response []SSHSigningKeyResult) {
|
||||
signingKeys, resp, err := r.Client.Users.ListSSHSigningKeys(
|
||||
r.Ctx,
|
||||
username,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
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")
|
||||
} else {
|
||||
r.PrintTitle("📝 SSH Signing Keys")
|
||||
for i, key := range signingKeys {
|
||||
k := SSHSigningKeyResult{
|
||||
ID: fmt.Sprintf("%d", key.GetID()),
|
||||
Title: key.GetTitle(),
|
||||
CreatedAt: key.GetCreatedAt().String(),
|
||||
Key: key.GetKey(),
|
||||
}
|
||||
r.PrintInfo("Key n°", fmt.Sprintf("%d", i))
|
||||
r.PrintInfo("ID", k.ID)
|
||||
r.PrintInfo("Title", k.Title)
|
||||
r.PrintInfo("Created At", k.CreatedAt)
|
||||
r.PrintInfo("Key", k.Key)
|
||||
if i != len(signingKeys)-1 {
|
||||
r.PrintNewline()
|
||||
}
|
||||
response = append(response, k)
|
||||
}
|
||||
}
|
||||
WaitForRateLimit(resp)
|
||||
r.PrintNewline()
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package ghrecon
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/go-github/v72/github"
|
||||
)
|
||||
|
||||
type Recon struct {
|
||||
Client *github.Client
|
||||
Logger *log.Logger
|
||||
Ctx context.Context
|
||||
Silent bool
|
||||
JsonFile string
|
||||
MaxRepoSize int
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package ghrecon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type OrgResult struct {
|
||||
Login string
|
||||
ID string
|
||||
URL string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (r Recon) Orgs(username string) (response []OrgResult) {
|
||||
orgs, resp, err := r.Client.Organizations.List(r.Ctx, username, nil)
|
||||
if err != nil {
|
||||
r.Logger.Error("Failed to fetch organizations", "err", err)
|
||||
} else if len(orgs) == 0 {
|
||||
r.PrintTitle("🏢 Organizations")
|
||||
r.PrintInfo("INFO", "No Organizations found")
|
||||
} else {
|
||||
r.PrintTitle("🏢 Organizations")
|
||||
for i, org := range orgs {
|
||||
o := OrgResult{
|
||||
Login: org.GetLogin(),
|
||||
ID: fmt.Sprintf("%d", org.GetID()),
|
||||
URL: org.GetURL(),
|
||||
Description: org.GetDescription(),
|
||||
}
|
||||
r.PrintInfo("Organization n°", fmt.Sprintf("%d", i))
|
||||
r.PrintInfo("Login", o.Login)
|
||||
r.PrintInfo("ID", o.ID)
|
||||
r.PrintInfo("URL", o.URL)
|
||||
r.PrintInfo("Description", o.Description)
|
||||
if i != len(orgs)-1 {
|
||||
r.PrintNewline()
|
||||
}
|
||||
response = append(response, o)
|
||||
}
|
||||
}
|
||||
r.PrintNewline()
|
||||
WaitForRateLimit(resp)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package ghrecon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type SocialResult struct {
|
||||
Provider string `json:"provider"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func (r Recon) Socials(username string) (response []SocialResult) {
|
||||
resp, err := FetchGitHubAPI(r.Client, "", "/users/"+username+"/social_accounts")
|
||||
if err != nil {
|
||||
r.Logger.Error("Failed to fetch socials", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
var socialAccounts []SocialResult
|
||||
err = json.Unmarshal(resp, &socialAccounts)
|
||||
if err != nil {
|
||||
r.Logger.Error("Failed to unmarshal socials", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(socialAccounts) == 0 {
|
||||
r.PrintTitle("🐥 Socials")
|
||||
r.PrintInfo("INFO", "No commits found")
|
||||
} else {
|
||||
r.PrintTitle("🐥 Socials")
|
||||
for i, account := range socialAccounts {
|
||||
r.PrintInfo("Social n°", fmt.Sprintf("%d", i))
|
||||
r.PrintInfo("Provider", account.Provider)
|
||||
r.PrintInfo("URL", account.URL)
|
||||
}
|
||||
}
|
||||
r.PrintNewline()
|
||||
|
||||
return socialAccounts
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package ghrecon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type UserResult struct {
|
||||
Username string
|
||||
ID string
|
||||
AvatarURL string
|
||||
GravatarID string
|
||||
Name string
|
||||
Company string
|
||||
Location string
|
||||
Email string
|
||||
Hireable string
|
||||
Bio string
|
||||
PublicRepos string
|
||||
PublicGists string
|
||||
Followers string
|
||||
Following string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
SuspendedAt string
|
||||
TotalPrivateRepos string
|
||||
PrivateGists string
|
||||
DiskUsage string
|
||||
Collaborators string
|
||||
Plan string
|
||||
}
|
||||
|
||||
func (r Recon) User(username string) (response UserResult) {
|
||||
user, resp, err := r.Client.Users.Get(r.Ctx, username)
|
||||
if resp.StatusCode == 404 {
|
||||
r.Logger.Fatal("User not found")
|
||||
}
|
||||
if err != nil {
|
||||
r.Logger.Fatal("Failed to fetch user's information", "err", err)
|
||||
}
|
||||
|
||||
r.PrintTitle("👤 User informations")
|
||||
u := UserResult{
|
||||
Username: user.GetLogin(),
|
||||
ID: fmt.Sprintf("%d", user.GetID()),
|
||||
AvatarURL: user.GetAvatarURL(),
|
||||
GravatarID: user.GetGravatarID(),
|
||||
Name: user.GetName(),
|
||||
Company: user.GetCompany(),
|
||||
Location: user.GetLocation(),
|
||||
Email: user.GetEmail(),
|
||||
Hireable: fmt.Sprintf("%t", user.GetHireable()),
|
||||
Bio: user.GetBio(),
|
||||
PublicRepos: fmt.Sprintf("%d", user.GetPublicRepos()),
|
||||
PublicGists: fmt.Sprintf("%d", user.GetPublicGists()),
|
||||
Followers: fmt.Sprintf("%d", user.GetFollowers()),
|
||||
Following: fmt.Sprintf("%d", user.GetFollowing()),
|
||||
CreatedAt: user.GetCreatedAt().String(),
|
||||
UpdatedAt: user.GetUpdatedAt().String(),
|
||||
SuspendedAt: user.GetSuspendedAt().String(),
|
||||
TotalPrivateRepos: fmt.Sprintf("%d", user.GetTotalPrivateRepos()),
|
||||
PrivateGists: fmt.Sprintf("%d", user.GetPrivateGists()),
|
||||
DiskUsage: fmt.Sprintf("%d", user.GetDiskUsage()),
|
||||
Collaborators: fmt.Sprintf("%d", user.GetCollaborators()),
|
||||
Plan: user.GetPlan().GetName(),
|
||||
}
|
||||
r.PrintInfo("Username", u.Username)
|
||||
r.PrintInfo("ID", u.ID)
|
||||
r.PrintInfo("Avatar URL", u.AvatarURL)
|
||||
r.PrintInfo("Gravatar ID", u.GravatarID)
|
||||
r.PrintInfo("Name", u.Name)
|
||||
r.PrintInfo("Company", u.Company)
|
||||
r.PrintInfo("Location", u.Location)
|
||||
r.PrintInfo("Email", u.Email)
|
||||
r.PrintInfo("Hireable", u.Hireable)
|
||||
r.PrintInfo("Bio", u.Bio)
|
||||
r.PrintInfo("Public Repos", u.PublicRepos)
|
||||
r.PrintInfo("Public Gists", u.PublicGists)
|
||||
r.PrintInfo("Followers", u.Followers)
|
||||
r.PrintInfo("Following", u.Following)
|
||||
r.PrintInfo("Created At", u.CreatedAt)
|
||||
r.PrintInfo("Updated At", u.UpdatedAt)
|
||||
r.PrintInfo("Suspended At", u.SuspendedAt)
|
||||
r.PrintInfo("Total Private Repos", u.TotalPrivateRepos)
|
||||
r.PrintInfo("Private Gists", u.PrivateGists)
|
||||
r.PrintInfo("Disk Usage", u.DiskUsage)
|
||||
r.PrintInfo("Collaborators", u.Collaborators)
|
||||
r.PrintInfo("Plan", u.Plan)
|
||||
r.PrintNewline()
|
||||
|
||||
WaitForRateLimit(resp)
|
||||
return u
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package ghrecon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/go-github/v72/github"
|
||||
)
|
||||
|
||||
var (
|
||||
Grey = lipgloss.Color("#7d7d7d")
|
||||
Green = lipgloss.Color("#a6e3a1")
|
||||
Red = lipgloss.Color("#f38ba8")
|
||||
|
||||
GreyStyle = lipgloss.NewStyle().Foreground(Grey)
|
||||
GreenStyle = lipgloss.NewStyle().Foreground(Green)
|
||||
RedStyle = lipgloss.NewStyle().Foreground(Red)
|
||||
)
|
||||
|
||||
func (r Recon) Header() {
|
||||
if r.Silent {
|
||||
return
|
||||
}
|
||||
asciiArt := " __ \n ___ _/ / _______ _______ ___ \n / _ `/ _ \\/ __/ -_) __/ _ \\/ _ \\\n \\_, /_//_/_/ \\__/\\__/\\___/_//_/\n/___/ "
|
||||
fmt.Println(
|
||||
GreyStyle.Render(lipgloss.JoinVertical(lipgloss.Right, asciiArt, "@anotherhadi\n")),
|
||||
)
|
||||
}
|
||||
|
||||
func ParseUsername(username string) error {
|
||||
if username == "" {
|
||||
return fmt.Errorf("username is required")
|
||||
}
|
||||
if strings.Contains(username, " ") {
|
||||
return fmt.Errorf("username cannot contain spaces")
|
||||
}
|
||||
if strings.Contains(username, "@") {
|
||||
return fmt.Errorf("username cannot contain @")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) {
|
||||
url := "https://api.github.com" + path
|
||||
userAgent := "GHRecon/1.0"
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating request for %s: %w", url, err)
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
resp, err := github.Client().Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error executing request for %s: %w", url, err)
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf(
|
||||
"request for %s failed with status %d: %s",
|
||||
url,
|
||||
resp.StatusCode,
|
||||
string(bodyBytes),
|
||||
)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"error reading response body for %s: %w",
|
||||
url,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return bodyBytes, nil
|
||||
}
|
||||
|
||||
func (r Recon) PrintNewline() {
|
||||
if r.Silent {
|
||||
return
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func (r Recon) PrintTitle(title string) {
|
||||
if r.Silent {
|
||||
return
|
||||
}
|
||||
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7287fd"))
|
||||
fmt.Println(style.Render(title) + "\n")
|
||||
}
|
||||
|
||||
func (r Recon) PrintInfo(key, value string, more ...string) {
|
||||
if r.Silent {
|
||||
return
|
||||
}
|
||||
if value == "" || value == "0001-01-01 00:00:00 +0000 UTC" {
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(key, "n°") {
|
||||
fmt.Printf(" %s %s", GreyStyle.Render(key), value)
|
||||
} else {
|
||||
fmt.Printf(" %s %s", GreyStyle.Render(key+":"), value)
|
||||
}
|
||||
if len(more) > 0 {
|
||||
fmt.Printf(" %s", GreyStyle.Render("("+strings.Join(more, ", ")+")"))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func WaitForRateLimit(resp *github.Response) {
|
||||
if resp.Rate.Remaining == 0 {
|
||||
log.Info(
|
||||
"Rate limit reached, waiting for reset... (time:" + resp.Rate.Reset.Time.String() + ")",
|
||||
)
|
||||
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)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ require (
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/charmbracelet/log v0.4.1
|
||||
github.com/google/go-github/v72 v72.0.0
|
||||
github.com/spf13/pflag v1.0.6
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -36,6 +36,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/go-github/v72/github"
|
||||
)
|
||||
|
||||
func keys(client *github.Client, ctx context.Context, username string) {
|
||||
sshKeys, resp, err := client.Users.ListKeys(ctx, username, nil)
|
||||
if err != nil {
|
||||
log.Error("Failed to fetch user's keys", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(sshKeys) == 0 {
|
||||
fmt.Println(
|
||||
GreyStyle.Render("[")+
|
||||
RedStyle.Render("x")+
|
||||
GreyStyle.Render("]"),
|
||||
GreyStyle.Render("No keys found for user "+username),
|
||||
)
|
||||
} else {
|
||||
fmt.Println(
|
||||
GreyStyle.Render("[")+
|
||||
GreenStyle.Render("+")+
|
||||
GreyStyle.Render("]"),
|
||||
GreyStyle.Render("Keys:\n"),
|
||||
)
|
||||
}
|
||||
for _, key := range sshKeys {
|
||||
PrintInfo("ID", fmt.Sprintf("%d", key.GetID()))
|
||||
PrintInfo("URL", key.GetURL())
|
||||
PrintInfo("Title", key.GetTitle())
|
||||
PrintInfo("Created At", key.GetCreatedAt().String())
|
||||
PrintInfo("Key", key.GetKey())
|
||||
PrintInfo("Read Only", fmt.Sprintf("%t", key.GetReadOnly()))
|
||||
PrintInfo("Verified", fmt.Sprintf("%t", key.GetVerified()))
|
||||
PrintInfo("Last Used", key.GetLastUsed().String())
|
||||
PrintInfo("Added By", key.GetAddedBy())
|
||||
fmt.Println()
|
||||
}
|
||||
WaitForRateLimit(resp)
|
||||
|
||||
gpgKeys, resp, err := client.Users.ListGPGKeys(ctx, username, nil)
|
||||
if err != nil {
|
||||
log.Error("Failed to fetch user's GPG keys", "err", err)
|
||||
}
|
||||
|
||||
if len(gpgKeys) == 0 {
|
||||
fmt.Println(
|
||||
GreyStyle.Render("[")+
|
||||
RedStyle.Render("x")+
|
||||
GreyStyle.Render("]"),
|
||||
GreyStyle.Render("No GPG keys found\n"),
|
||||
)
|
||||
} else {
|
||||
fmt.Println(
|
||||
GreyStyle.Render("[")+
|
||||
GreenStyle.Render("+")+
|
||||
GreyStyle.Render("]"),
|
||||
GreyStyle.Render("GPG Keys:\n"),
|
||||
)
|
||||
}
|
||||
for _, key := range gpgKeys {
|
||||
PrintInfo("ID", fmt.Sprintf("%d", key.GetID()))
|
||||
PrintInfo("Key ID", key.GetKeyID())
|
||||
PrintInfo("Public Key", key.GetPublicKey())
|
||||
PrintInfo("Created At", key.GetCreatedAt().String())
|
||||
PrintInfo("Expires At", key.GetExpiresAt().String())
|
||||
PrintInfo("Can Sign", fmt.Sprintf("%t", key.GetCanSign()))
|
||||
PrintInfo("Can Encrypt Comms", fmt.Sprintf("%t", key.GetCanEncryptComms()))
|
||||
PrintInfo("Can Encrypt Storage", fmt.Sprintf("%t", key.GetCanEncryptStorage()))
|
||||
PrintInfo("Can Certify", fmt.Sprintf("%t", key.GetCanCertify()))
|
||||
PrintInfo("Primary Key ID", fmt.Sprintf("%d", key.GetPrimaryKeyID()))
|
||||
PrintInfo("Raw Key", key.GetRawKey())
|
||||
PrintInfo("Emails", fmt.Sprintf("%d", len(key.Emails)))
|
||||
for _, email := range key.Emails {
|
||||
PrintInfo("\tEmail", email.GetEmail())
|
||||
PrintInfo("\tVerified", fmt.Sprintf("%t", email.GetVerified()))
|
||||
}
|
||||
PrintInfo("Subkeys", fmt.Sprintf("%d", len(key.Subkeys)))
|
||||
for _, subkey := range key.Subkeys {
|
||||
PrintInfo("\tSubkey ID", fmt.Sprintf("%d", subkey.GetID()))
|
||||
PrintInfo("\tSubkey Key ID", subkey.GetKeyID())
|
||||
PrintInfo("\tSubkey Created At", subkey.GetCreatedAt().String())
|
||||
PrintInfo("\tSubkey Expires At", subkey.GetExpiresAt().String())
|
||||
PrintInfo("\tSubkey Can Sign", fmt.Sprintf("%t", subkey.GetCanSign()))
|
||||
PrintInfo(
|
||||
"\tSubkey Can Encrypt Comms",
|
||||
fmt.Sprintf("%t", subkey.GetCanEncryptComms()),
|
||||
)
|
||||
PrintInfo(
|
||||
"\tSubkey Can Encrypt Storage",
|
||||
fmt.Sprintf("%t", subkey.GetCanEncryptStorage()),
|
||||
)
|
||||
PrintInfo("\tSubkey Can Certify", fmt.Sprintf("%t", subkey.GetCanCertify()))
|
||||
PrintInfo("\tSubkey Primary Key ID", fmt.Sprintf("%d", subkey.GetPrimaryKeyID()))
|
||||
PrintInfo("\tSubkey Raw Key", subkey.GetRawKey())
|
||||
PrintInfo("\tSubkey Public Key", subkey.GetPublicKey())
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
WaitForRateLimit(resp)
|
||||
|
||||
signingKeys, resp, err := client.Users.ListSSHSigningKeys(
|
||||
ctx,
|
||||
username,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error("Failed to fetch user's signing keys", "err", err)
|
||||
}
|
||||
if len(signingKeys) == 0 {
|
||||
fmt.Println(
|
||||
GreyStyle.Render("[")+
|
||||
RedStyle.Render("x")+
|
||||
GreyStyle.Render("]"),
|
||||
GreyStyle.Render("No signing keys found\n"),
|
||||
)
|
||||
} else {
|
||||
fmt.Println(
|
||||
GreyStyle.Render("[")+
|
||||
GreenStyle.Render("+")+
|
||||
GreyStyle.Render("]"),
|
||||
GreyStyle.Render("Signing Keys:\n"),
|
||||
)
|
||||
}
|
||||
for _, key := range signingKeys {
|
||||
PrintInfo("ID", fmt.Sprintf("%d", key.GetID()))
|
||||
PrintInfo("Key", key.GetKey())
|
||||
PrintInfo("Title", key.GetTitle())
|
||||
PrintInfo("Created At", key.GetCreatedAt().String())
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
WaitForRateLimit(resp)
|
||||
}
|
||||
@@ -2,46 +2,156 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
ghrecon "github.com/anotherhadi/gh-recon/gh-recon"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/go-github/v72/github"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var username string
|
||||
var token string
|
||||
flag.StringVar(&username, "username", "", "Target username")
|
||||
flag.StringVar(&token, "token", "", "Github token")
|
||||
var onlyCommitsLeak bool
|
||||
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()
|
||||
|
||||
if username == "" {
|
||||
fmt.Println("Please provide a username with the --username flag")
|
||||
os.Exit(1)
|
||||
// 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,
|
||||
}
|
||||
err := parseUsername(username)
|
||||
if err != nil {
|
||||
log.Error("Invalid username", "err", err)
|
||||
os.Exit(1)
|
||||
|
||||
// CHECK FLAGS
|
||||
|
||||
if username == "" && fromEmail == "" {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
client := github.NewClient(nil)
|
||||
if token == "" {
|
||||
log.Info(
|
||||
"It's recommended to set a Github token for better rate limits. You can set it using the --token flag.",
|
||||
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()
|
||||
// START
|
||||
|
||||
header()
|
||||
userInfo(client, ctx, username)
|
||||
orgs(client, ctx, username)
|
||||
keys(client, ctx, username)
|
||||
socials(client, username)
|
||||
commits(client, ctx, username)
|
||||
r.Header()
|
||||
|
||||
if fromEmail != "" {
|
||||
emailsInfo := r.Email(fromEmail)
|
||||
r.WriteJson(
|
||||
map[string]any{
|
||||
"Authors": emailsInfo,
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if onlyCommitsLeak {
|
||||
commitsInfo := r.Commits(username)
|
||||
r.WriteJson(
|
||||
map[string]any{
|
||||
"Authors": commitsInfo,
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := r.User(username)
|
||||
orgsInfo := r.Orgs(username)
|
||||
sshKeysInfo := r.SshKeys(username)
|
||||
gpgKeysInfo := r.GpgKeys(username)
|
||||
sshSigningKeysInfo := r.SshSigningKeys(username)
|
||||
socialsInfo := r.Socials(username)
|
||||
closeFriendsInfo := r.CloseFriends(username)
|
||||
commitsInfo := r.Commits(username)
|
||||
|
||||
results := map[string]any{
|
||||
"User": userInfo,
|
||||
"Orgs": orgsInfo,
|
||||
"SSHKeys": sshKeysInfo,
|
||||
"GPGKeys": gpgKeysInfo,
|
||||
"SSHSigningKeys": sshSigningKeysInfo,
|
||||
"Socials": socialsInfo,
|
||||
"Commits": commitsInfo,
|
||||
"CloseFriends": closeFriendsInfo,
|
||||
}
|
||||
|
||||
if deep {
|
||||
results["Deep"] = r.Deep(username, excludeRepos, refresh)
|
||||
}
|
||||
|
||||
r.WriteJson(results)
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/go-github/v72/github"
|
||||
)
|
||||
|
||||
func orgs(client *github.Client, ctx context.Context, username string) {
|
||||
orgs, resp, err := client.Organizations.List(ctx, username, nil)
|
||||
if err != nil {
|
||||
log.Error("Failed to fetch user's organizations", "err", err)
|
||||
}
|
||||
if len(orgs) == 0 {
|
||||
fmt.Println(
|
||||
GreyStyle.Render("[")+
|
||||
RedStyle.Render("x")+
|
||||
GreyStyle.Render("]"),
|
||||
GreyStyle.Render("No organizations found for user "+username),
|
||||
)
|
||||
} else {
|
||||
fmt.Println(
|
||||
GreyStyle.Render("[")+
|
||||
GreenStyle.Render("+")+
|
||||
GreyStyle.Render("]"),
|
||||
GreyStyle.Render("Organizations:\n"),
|
||||
)
|
||||
}
|
||||
for _, org := range orgs {
|
||||
PrintInfo("Login", org.GetLogin())
|
||||
PrintInfo("ID", fmt.Sprintf("%d", org.GetID()))
|
||||
PrintInfo("Node ID", org.GetNodeID())
|
||||
PrintInfo("URL", org.GetURL())
|
||||
PrintInfo("Repos URL", org.GetReposURL())
|
||||
PrintInfo("Events URL", org.GetEventsURL())
|
||||
PrintInfo("Hooks URL", org.GetHooksURL())
|
||||
PrintInfo("Issues URL", org.GetIssuesURL())
|
||||
PrintInfo("Members URL", org.GetMembersURL())
|
||||
PrintInfo("Public Members URL", org.GetPublicMembersURL())
|
||||
PrintInfo("Avatar URL", org.GetAvatarURL())
|
||||
PrintInfo("Description", org.GetDescription())
|
||||
fmt.Println()
|
||||
}
|
||||
WaitForRateLimit(resp)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/go-github/v72/github"
|
||||
)
|
||||
|
||||
func socials(client *github.Client, username string) {
|
||||
resp, err := FetchGitHubAPI(client, "", "/users/"+username+"/social_accounts")
|
||||
if err != nil {
|
||||
log.Error("Failed to fetch user's social media accounts", "err", err)
|
||||
}
|
||||
|
||||
type SocialAccount struct {
|
||||
Provider string `json:"provider"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
type SocialAccounts []SocialAccount
|
||||
|
||||
var socialAccounts SocialAccounts
|
||||
err = json.Unmarshal(resp, &socialAccounts)
|
||||
if err != nil {
|
||||
log.Error("Failed to unmarshal social media accounts", "err", err)
|
||||
}
|
||||
if len(socialAccounts) == 0 {
|
||||
fmt.Println(
|
||||
GreyStyle.Render("[")+
|
||||
RedStyle.Render("x")+
|
||||
GreyStyle.Render("]"),
|
||||
GreyStyle.Render("No social media accounts found\n"),
|
||||
)
|
||||
} else {
|
||||
fmt.Println(
|
||||
GreyStyle.Render("[")+
|
||||
GreenStyle.Render("+")+
|
||||
GreyStyle.Render("]"),
|
||||
GreyStyle.Render("Social Media Accounts:\n"),
|
||||
)
|
||||
}
|
||||
for _, account := range socialAccounts {
|
||||
PrintInfo("Provider", account.Provider)
|
||||
PrintInfo("URL", account.URL)
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/go-github/v72/github"
|
||||
)
|
||||
|
||||
func userInfo(client *github.Client, ctx context.Context, username string) {
|
||||
fmt.Println(
|
||||
GreyStyle.Render("[")+
|
||||
GreenStyle.Render("-")+
|
||||
GreyStyle.Render("]"),
|
||||
GreyStyle.Render("Fetching user info...\n"),
|
||||
)
|
||||
|
||||
user, resp, err := client.Users.Get(ctx, username)
|
||||
if resp.StatusCode == 404 {
|
||||
fmt.Println(
|
||||
GreyStyle.Render("[")+
|
||||
RedStyle.Render("x")+
|
||||
GreyStyle.Render("]"),
|
||||
GreyStyle.Render("Error:"),
|
||||
RedStyle.Render("User not found"),
|
||||
)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Failed to fetch user's information", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
PrintInfo("Username", user.GetLogin())
|
||||
PrintInfo("ID", fmt.Sprintf("%d", user.GetID()))
|
||||
PrintInfo("Avatar URL", user.GetAvatarURL())
|
||||
PrintInfo("Gravatar ID", user.GetGravatarID())
|
||||
PrintInfo("Name", user.GetName())
|
||||
PrintInfo("Company", user.GetCompany())
|
||||
PrintInfo("Location", user.GetLocation())
|
||||
PrintInfo("Email", user.GetEmail())
|
||||
PrintInfo("Hireable", fmt.Sprintf("%t", user.GetHireable()))
|
||||
PrintInfo("Bio", user.GetBio())
|
||||
PrintInfo("Public Repos", fmt.Sprintf("%d", user.GetPublicRepos()))
|
||||
PrintInfo("Public Gists", fmt.Sprintf("%d", user.GetPublicGists()))
|
||||
PrintInfo("Followers", fmt.Sprintf("%d", user.GetFollowers()))
|
||||
PrintInfo("Following", fmt.Sprintf("%d", user.GetFollowing()))
|
||||
PrintInfo("Created At", user.GetCreatedAt().String())
|
||||
PrintInfo("Updated At", user.GetUpdatedAt().String())
|
||||
PrintInfo("Suspended At", user.GetSuspendedAt().String())
|
||||
PrintInfo("Type", user.GetType())
|
||||
PrintInfo("Site Admin", fmt.Sprintf("%t", user.GetSiteAdmin()))
|
||||
PrintInfo("Total Private Repos", fmt.Sprintf("%d", user.GetTotalPrivateRepos()))
|
||||
PrintInfo("Owned Private Repos", fmt.Sprintf("%d", user.GetOwnedPrivateRepos()))
|
||||
PrintInfo("Private Gists", fmt.Sprintf("%d", user.GetPrivateGists()))
|
||||
PrintInfo("Disk Usage", fmt.Sprintf("%d", user.GetDiskUsage()))
|
||||
PrintInfo("Collaborators", fmt.Sprintf("%d", user.GetCollaborators()))
|
||||
PrintInfo("Plan", user.GetPlan().GetName())
|
||||
fmt.Println()
|
||||
|
||||
WaitForRateLimit(resp)
|
||||
}
|
||||
@@ -1,100 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/go-github/v72/github"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var (
|
||||
Grey = lipgloss.Color("#7d7d7d")
|
||||
Green = lipgloss.Color("#a6e3a1")
|
||||
Red = lipgloss.Color("#f38ba8")
|
||||
|
||||
GreyStyle = lipgloss.NewStyle().Foreground(Grey)
|
||||
GreenStyle = lipgloss.NewStyle().Foreground(Green)
|
||||
RedStyle = lipgloss.NewStyle().Foreground(Red)
|
||||
)
|
||||
|
||||
func header() {
|
||||
asciiArt := " __ \n ___ _/ / _______ _______ ___ \n / _ `/ _ \\/ __/ -_) __/ _ \\/ _ \\\n \\_, /_//_/_/ \\__/\\__/\\___/_//_/\n/___/ "
|
||||
fmt.Println(GreyStyle.Render(lipgloss.JoinVertical(lipgloss.Right, asciiArt, "@anotherhadi\n")))
|
||||
}
|
||||
|
||||
func parseUsername(username string) error {
|
||||
if username == "" {
|
||||
return fmt.Errorf("username is required")
|
||||
}
|
||||
if strings.Contains(username, " ") {
|
||||
return fmt.Errorf("username cannot contain spaces")
|
||||
}
|
||||
if strings.Contains(username, "@") {
|
||||
return fmt.Errorf("username cannot contain @")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) {
|
||||
url := "https://api.github.com" + path
|
||||
userAgent := "GHRecon/1.0"
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating request for %s: %w", url, err)
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
resp, err := github.Client().Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error executing request for %s: %w", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf(
|
||||
"request for %s failed with status %d: %s",
|
||||
url,
|
||||
resp.StatusCode,
|
||||
string(bodyBytes),
|
||||
)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"error reading response body for %s: %w",
|
||||
url,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return bodyBytes, nil
|
||||
}
|
||||
|
||||
func PrintInfo(key, value string) {
|
||||
if value == "" || value == "0" || value == "0001-01-01 00:00:00 +0000 UTC" {
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s %s\n", GreyStyle.Render(key+":"), GreenStyle.Render(value))
|
||||
}
|
||||
|
||||
func WaitForRateLimit(resp *github.Response) {
|
||||
if resp.Rate.Remaining == 0 {
|
||||
log.Info(
|
||||
"Rate limit reached, waiting for reset... (time:" + resp.Rate.Reset.Time.String() + ")",
|
||||
)
|
||||
time.Sleep(time.Until(resp.Rate.Reset.Time) + time.Second)
|
||||
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