mirror of
https://github.com/anotherhadi/github-recon.git
synced 2026-10-05 10:58:25 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df9fff8e97 | ||
|
|
926bf7239e | ||
|
|
5b79b6492b | ||
|
|
2137925043 | ||
|
|
274b393e53 | ||
|
|
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 |
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,4 +1,10 @@
|
|||||||
# GH-Recon
|
<div align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/anotherhadi/gh-recon/main/.github/assets/logo.png" width="120px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
# GH-Recon 🔍
|
||||||
|
|
||||||
<p>
|
<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://github.com/anotherhadi/gh-recon/releases"><img src="https://img.shields.io/github/release/anotherhadi/gh-recon.svg" alt="Latest Release"></a>
|
||||||
@@ -6,37 +12,45 @@
|
|||||||
<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>
|
<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>
|
</p>
|
||||||
|
|
||||||
## Project Overview
|
## 🧾 Project Overview
|
||||||
|
|
||||||
Fetches and aggregates public OSINT data for a GitHub user, leveraging Go and the GitHub API.
|
Retrieves and aggregates public OSINT data about a GitHub user using Go and the GitHub API.
|
||||||
|
Finds hidden emails in commit history, previous usernames, friends, other GitHub accounts, and more.
|
||||||
|
|
||||||
## Features
|
## 🚀 Features
|
||||||
|
|
||||||
- Retrieve basic user profile information (username, ID, avatar, bio, creation dates)
|
- Retrieve basic user profile information (username, ID, avatar, bio, creation dates)
|
||||||
- List organizations and roles
|
- List organizations and roles
|
||||||
- Fetch SSH and GPG keys
|
- Fetch SSH and GPG keys
|
||||||
- Enumerate social accounts
|
- Enumerate social accounts
|
||||||
- Extract unique commit authors (name + email) in both chronological orders
|
- Extract unique commit authors (name + email)
|
||||||
|
- Find close friends
|
||||||
|
- Find Github accounts using an email address
|
||||||
|
- Export results to JSON
|
||||||
|
- Deep scan option (clone repositories, regex search, analyze licenses, etc.)
|
||||||
|
|
||||||
## Disclaimer
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
This tool is intended for educational purposes only. Use responsibly and ensure you have permission to access the data you are querying.
|
This tool is intended for educational purposes only. Use responsibly and ensure you have permission to access the data you are querying.
|
||||||
|
|
||||||
## Prerequisites
|
## 📋 Prerequisites
|
||||||
|
|
||||||
- Go 1.18+
|
- Go 1.18+
|
||||||
- GitHub Personal Access Token (recommended for higher rate limits)
|
- GitHub Personal Access Token (recommended for higher rate limits): Create a GitHub API token with no permissions/no scope. This will be equivalent to public GitHub access, but it will allow access to use the GitHub Search API.
|
||||||
|
|
||||||
## Installation
|
## 📦 Installation
|
||||||
|
|
||||||
### With Go
|
### With Go
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go get github.com/anotherhadi/gh-recon
|
go install github.com/anotherhadi/gh-recon@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### With Nix/NixOS
|
### With Nix/NixOS
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click to expand</summary>
|
||||||
|
|
||||||
**From anywhere (using the repo URL):**
|
**From anywhere (using the repo URL):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -59,7 +73,9 @@ environment.systemPackages = with pkgs; [ # or home.packages
|
|||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
</details>
|
||||||
|
|
||||||
|
## 🧪 Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh-recon --username TARGET_USER [--token YOUR_TOKEN]
|
gh-recon --username TARGET_USER [--token YOUR_TOKEN]
|
||||||
@@ -67,18 +83,42 @@ gh-recon --username TARGET_USER [--token YOUR_TOKEN]
|
|||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
- `--username`: GitHub username to inspect (required)
|
```txt
|
||||||
- `--token`: Personal Access Token (optional but recommended)
|
-u, --username string GitHub username to analyze
|
||||||
|
-t, --token string GitHub personal access token (e.g. ghp_...)
|
||||||
|
-e, --email string Search accounts by email address
|
||||||
|
-d, --deep Enable deep scan (clone repos, regex search, analyse licenses, etc.)
|
||||||
|
--max-size int Limit the size of repositories to scan (in MB) (only for deep scan) (default 150)
|
||||||
|
--exclude-repo string Exclude repos from deep scan (comma-separated list, only for deep scan)
|
||||||
|
-r, --refresh Refresh the cache (only for deep scan)
|
||||||
|
-c, --only-commits Display only commits with author info
|
||||||
|
-s, --silent Suppress all non-essential output
|
||||||
|
-j, --json string Write results to specified JSON file
|
||||||
|
```
|
||||||
|
|
||||||
## Example
|
## 💡 Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh-recon --username anotherhadi --token ghp_ABC123...
|
gh-recon --username anotherhadi --token ghp_ABC123...
|
||||||
|
gh-recon --email [email protected]
|
||||||
|
gh-recon --username anotherhadi --json output.json --deep
|
||||||
```
|
```
|
||||||
|
|
||||||
## Todo
|
## 🕵️♂️ Cover your tracks
|
||||||
|
|
||||||
Feel free to contribute! Here are some ideas:
|
Understanding what information about you is publicly visible is the first step to managing your online presence. gh-recon can help you identify your own publicly available data on GitHub. Here’s how you can take steps to protect your privacy and security:
|
||||||
|
|
||||||
- Fetch names in License files
|
- **Review your public profile**: Regularly check your GitHub profile and repositories to ensure that you are not unintentionally exposing sensitive information.
|
||||||
- Fetch emails in README files/comments
|
- **Manage email exposure**: Use GitHub's settings to control which email addresses are visible on your profile and in commit history. You can also use a no-reply email address for commits. Delete/modify any sensitive information in your commit history.
|
||||||
|
- **Be Mindful of Repository Content**: Avoid including sensitive information in your repositories, such as API keys, passwords, emails or personal data. Use `.gitignore` to exclude files that contain sensitive information.
|
||||||
|
|
||||||
|
You can also use a tool like [TruffleHog](github.com/trufflesecurity/trufflehog) to scan your repositories specifically for exposed secrets and tokens.
|
||||||
|
|
||||||
|
**Useful links:**
|
||||||
|
|
||||||
|
- [Blocking command line pushes that expose your personal email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/blocking-command-line-pushes-that-expose-your-personal-email-address)
|
||||||
|
- [No-reply email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address)
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Feel free to contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
(system: f system (import nixpkgs { inherit system; }));
|
(system: f system (import nixpkgs { inherit system; }));
|
||||||
|
|
||||||
pname = "gh-recon";
|
pname = "gh-recon";
|
||||||
version = "0.1.0";
|
version = "0.2.1";
|
||||||
|
|
||||||
ldflags = [ "-s" "-w" ];
|
ldflags = [ "-s" "-w" ];
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
vendorHash = "sha256-CPk8B8FKEoN8qff6WV/iBf0eVjTBMVfJQvlVcti6dfM=";
|
vendorHash = "sha256-S8IzmdiVvBtnQQl0AewGZ1yuitvrdnVQ/Jf2230g3Mg=";
|
||||||
|
|
||||||
meta = with pkgs.lib; {
|
meta = with pkgs.lib; {
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package ghrecon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloseFriendsResult struct {
|
||||||
|
Login string
|
||||||
|
Score int
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxFollowingForTarget = 50
|
||||||
|
maxFollowersForFollowing = 20
|
||||||
|
pointPerCriterion = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// CloseFriends returns a list of close friends of the user
|
||||||
|
// To derive this, we check the following:
|
||||||
|
// 1. The target has less than 50 Following
|
||||||
|
// 2. The target's following has less than 20 followers (+1 point)
|
||||||
|
// 3. The target's following follows the target (+1 point)
|
||||||
|
func (r Recon) CloseFriends(username string) (response []CloseFriendsResult) {
|
||||||
|
r.PrintTitle("🧑🤝🧑 Close Friends")
|
||||||
|
|
||||||
|
following, resp, err := r.Client.Users.ListFollowing(r.Ctx, username, nil)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Error("Failed to fetch user's following list", "user", username, "err", err)
|
||||||
|
r.PrintNewline()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WaitForRateLimit(resp)
|
||||||
|
|
||||||
|
if len(following) >= maxFollowingForTarget {
|
||||||
|
r.PrintInfo(
|
||||||
|
"INFO",
|
||||||
|
fmt.Sprintf(
|
||||||
|
"%s follows %d or more users (%d). Skipping close friends check.",
|
||||||
|
username,
|
||||||
|
maxFollowingForTarget,
|
||||||
|
len(following),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
r.PrintNewline()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(following) == 0 {
|
||||||
|
r.PrintInfo("INFO", fmt.Sprintf("%s is not following anyone.", username))
|
||||||
|
r.PrintNewline()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, userBeingFollowedByTarget := range following {
|
||||||
|
loginName := userBeingFollowedByTarget.GetLogin()
|
||||||
|
if loginName == "" {
|
||||||
|
r.Logger.Warn("User in following list has an empty login", "target_user", username)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
currentScore := 0
|
||||||
|
|
||||||
|
userDetails, userResp, userErr := r.Client.Users.Get(r.Ctx, loginName)
|
||||||
|
if userErr != nil {
|
||||||
|
r.Logger.Warn(
|
||||||
|
"Failed to fetch details for followed user",
|
||||||
|
"followed_user",
|
||||||
|
loginName,
|
||||||
|
"err",
|
||||||
|
userErr,
|
||||||
|
)
|
||||||
|
if userResp != nil {
|
||||||
|
WaitForRateLimit(userResp)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
WaitForRateLimit(userResp)
|
||||||
|
|
||||||
|
if userDetails.GetFollowers() < maxFollowersForFollowing {
|
||||||
|
currentScore += pointPerCriterion
|
||||||
|
}
|
||||||
|
|
||||||
|
followsTargetBack, checkErr := r.checkIfUserFollows(loginName, username)
|
||||||
|
if checkErr != nil {
|
||||||
|
} else if followsTargetBack {
|
||||||
|
currentScore += pointPerCriterion
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentScore > 0 {
|
||||||
|
response = append(response, CloseFriendsResult{
|
||||||
|
Login: loginName,
|
||||||
|
Score: currentScore,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(response) == 0 {
|
||||||
|
r.PrintInfo(
|
||||||
|
"INFO",
|
||||||
|
fmt.Sprintf("No close friends found for %s based on the criteria.", username),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sort.Slice(response, func(i, j int) bool {
|
||||||
|
return response[i].Score > response[j].Score
|
||||||
|
})
|
||||||
|
for i, friend := range response {
|
||||||
|
r.PrintInfo(
|
||||||
|
fmt.Sprintf("Friend n°%d", i+1),
|
||||||
|
"@"+friend.Login,
|
||||||
|
"Score: "+fmt.Sprintf("%d", friend.Score),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.PrintNewline()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkIfUserFollows checks if sourceUserLogin follows targetUserLogin.
|
||||||
|
func (r Recon) checkIfUserFollows(sourceUserLogin, targetUserLogin string) (bool, error) {
|
||||||
|
isFollowing, resp, err := r.Client.Users.IsFollowing(r.Ctx, sourceUserLogin, targetUserLogin)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Warn("Error checking if user follows target",
|
||||||
|
"source_user_checking", sourceUserLogin,
|
||||||
|
"target_user_to_check", targetUserLogin,
|
||||||
|
"err", err)
|
||||||
|
if resp != nil {
|
||||||
|
WaitForRateLimit(resp)
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp != nil {
|
||||||
|
WaitForRateLimit(resp)
|
||||||
|
}
|
||||||
|
return isFollowing, nil
|
||||||
|
}
|
||||||
+62
-41
@@ -6,66 +6,87 @@ import (
|
|||||||
"github.com/google/go-github/v72/github"
|
"github.com/google/go-github/v72/github"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r Recon) Commits(username string) {
|
type CommitsResult struct {
|
||||||
PrintTitle("🐙 Commits")
|
Name string
|
||||||
|
Email string
|
||||||
|
Occurences int
|
||||||
|
FirstFoundIn string
|
||||||
|
}
|
||||||
|
|
||||||
seenNames := make(map[string]struct{})
|
func (r Recon) Commits(username string) (response []CommitsResult) {
|
||||||
seenEmails := make(map[string]struct{})
|
r.PrintTitle("🐙 Commits")
|
||||||
var names, emails []string
|
|
||||||
|
|
||||||
collect := func(order string) error {
|
results := make(map[string]CommitsResult)
|
||||||
opts := &github.SearchOptions{
|
|
||||||
Sort: "author-date",
|
collect := func(date string) error {
|
||||||
Order: order,
|
|
||||||
ListOptions: github.ListOptions{PerPage: 100},
|
|
||||||
}
|
|
||||||
for page := 1; page <= 10; page++ {
|
for page := 1; page <= 10; page++ {
|
||||||
opts.ListOptions.Page = page
|
result, resp, err := r.Client.Search.Commits(
|
||||||
result, resp, err := r.client.Search.Commits(
|
r.Ctx,
|
||||||
r.ctx,
|
fmt.Sprintf("author:%s author-date:%s", username, date),
|
||||||
fmt.Sprintf("author:%s", username),
|
&github.SearchOptions{
|
||||||
opts,
|
Sort: "author-date",
|
||||||
|
Order: "desc",
|
||||||
|
ListOptions: github.ListOptions{PerPage: 100, Page: page},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("fetch page %d (%s): %w", page, order, err)
|
return fmt.Errorf("fetch page %d (%s): %w", page, date, err)
|
||||||
}
|
}
|
||||||
WaitForRateLimit(resp)
|
WaitForRateLimit(resp)
|
||||||
if len(result.Commits) == 0 {
|
if len(result.Commits) == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
for _, item := range result.Commits {
|
for _, item := range result.Commits {
|
||||||
a := item.Commit.GetAuthor()
|
name := item.Commit.GetAuthor().GetName()
|
||||||
name := a.GetName()
|
email := item.Commit.GetAuthor().GetEmail()
|
||||||
email := a.GetEmail()
|
if SkipResult(name, email) {
|
||||||
if _, seen := seenNames[name]; !seen {
|
continue
|
||||||
seenNames[name] = struct{}{}
|
|
||||||
names = append(names, name)
|
|
||||||
PrintInfo(
|
|
||||||
"Name "+fmt.Sprint(len(names)),
|
|
||||||
name,
|
|
||||||
"from "+item.GetRepository().Owner.GetLogin()+"/"+item.GetRepository().
|
|
||||||
GetName(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if _, seen := seenEmails[email]; !seen {
|
if _, seen := results[name+" - "+email]; !seen {
|
||||||
seenEmails[email] = struct{}{}
|
author := CommitsResult{
|
||||||
emails = append(emails, email)
|
Name: name,
|
||||||
PrintInfo(
|
Email: email,
|
||||||
"Email "+fmt.Sprint(len(emails)),
|
Occurences: 1,
|
||||||
email,
|
FirstFoundIn: item.GetRepository().Owner.GetLogin() + "/" + item.GetRepository().
|
||||||
"from "+item.GetRepository().Owner.GetLogin()+"/"+item.GetRepository().
|
|
||||||
GetName(),
|
GetName(),
|
||||||
)
|
}
|
||||||
|
results[name+" - "+email] = author
|
||||||
|
} else {
|
||||||
|
result := results[name+" - "+email]
|
||||||
|
result.Occurences++
|
||||||
|
results[name+" - "+email] = result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := collect("desc"); err != nil {
|
// Range of dates to bypass the limit of 1000 results
|
||||||
r.logger.Error("Failed to fetch commits", "err", err, "order", "desc")
|
for _, date := range []string{
|
||||||
|
"<2023-01-01", "2023-01-01..2023-12-31",
|
||||||
|
"2024-01-01..2024-05-31",
|
||||||
|
"2024-06-01..2024-12-31",
|
||||||
|
"2025-01-01..2025-05-31",
|
||||||
|
"2025-06-01..2025-12-31",
|
||||||
|
">2026-01-01",
|
||||||
|
} {
|
||||||
|
if err := collect(date); err != nil {
|
||||||
|
r.Logger.Error("Failed to fetch commits", "err", err, "date", date)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := collect("asc"); err != nil {
|
|
||||||
r.logger.Error("Failed to fetch commits", "err", err, "order", "asc")
|
for _, result := range results {
|
||||||
|
r.PrintInfo(
|
||||||
|
"Author",
|
||||||
|
result.Name+" - "+result.Email,
|
||||||
|
"first from "+result.FirstFoundIn+" (x"+fmt.Sprint(result.Occurences)+")",
|
||||||
|
)
|
||||||
|
response = append(response, result)
|
||||||
}
|
}
|
||||||
|
if len(results) == 0 {
|
||||||
|
r.PrintInfo("INFO", "No commits found")
|
||||||
|
}
|
||||||
|
r.PrintNewline()
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
package ghrecon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/go-github/v72/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthorOccurrence struct {
|
||||||
|
Name string
|
||||||
|
Email string
|
||||||
|
FoundIn []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmailOccurrence struct {
|
||||||
|
Email string
|
||||||
|
FoundIn []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeepResult struct {
|
||||||
|
Repositories []Repositorie
|
||||||
|
Authors []AuthorOccurrence
|
||||||
|
Emails []EmailOccurrence
|
||||||
|
}
|
||||||
|
|
||||||
|
type Repositorie struct {
|
||||||
|
Repository string
|
||||||
|
Owner string
|
||||||
|
Name string
|
||||||
|
Size int
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
if strings.Contains(path, ".git/logs/") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Recon) Deep(username, excludeRepos string, refresh bool) (response DeepResult) {
|
||||||
|
repositories := []Repositorie{}
|
||||||
|
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 {
|
||||||
|
if slices.Contains(excludeReposList, repo.GetName()) ||
|
||||||
|
slices.Contains(excludeReposList, repo.GetOwner().GetLogin()+"/"+repo.GetName()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
maxRepoSize := r.MaxRepoSize * 1024
|
||||||
|
|
||||||
|
if repo.GetSize() > maxRepoSize {
|
||||||
|
r.PrintInfo(
|
||||||
|
"INFO",
|
||||||
|
"Skipping repository "+repo.GetOwner().GetLogin()+"/"+repo.GetName()+" due to size", fmt.Sprintf(
|
||||||
|
"%d",
|
||||||
|
repo.GetSize()/1024,
|
||||||
|
)+"MB > "+fmt.Sprintf(
|
||||||
|
"%d",
|
||||||
|
maxRepoSize/1024,
|
||||||
|
)+"MB",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories = append(repositories, Repositorie{
|
||||||
|
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 repositories {
|
||||||
|
destination := tmp_folder + "/" + repo.Owner + "/" + repo.Name
|
||||||
|
if folderExists(destination) {
|
||||||
|
r.PrintInfo("INFO", "Directory already downloaded, skipping "+repo.Owner+"/"+repo.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r.PrintInfo(
|
||||||
|
"Downloading",
|
||||||
|
repo.Owner+"/"+repo.Name,
|
||||||
|
fmt.Sprintf("%d", repo.Size/1024)+"MB",
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
authorOccurrences := []AuthorOccurrence{}
|
||||||
|
mapAuthorToIndex := make(map[string]int)
|
||||||
|
for _, repo := range repositories {
|
||||||
|
destination := tmp_folder + "/" + repo.Owner + "/" + repo.Name
|
||||||
|
if !folderExists(filepath.Join(destination, ".git")) {
|
||||||
|
r.Logger.Error(
|
||||||
|
"No .git directory found, cannot run git log.",
|
||||||
|
"repo",
|
||||||
|
repo.Owner+"/"+repo.Name,
|
||||||
|
"path",
|
||||||
|
destination,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
gitLogCmd := exec.Command("git", "log", "--all", "--format=%aN <%aE>")
|
||||||
|
gitLogCmd.Dir = destination
|
||||||
|
logOutput, logErr := gitLogCmd.Output()
|
||||||
|
|
||||||
|
if logErr != nil {
|
||||||
|
if exitErr, ok := logErr.(*exec.ExitError); ok {
|
||||||
|
r.Logger.Error("Failed to execute git log (ExitError)", "repo", repo.Owner+"/"+repo.Name, "stderr", string(exitErr.Stderr), "err", logErr)
|
||||||
|
} else {
|
||||||
|
r.Logger.Error("Failed to execute git log", "repo", repo.Owner+"/"+repo.Name, "err", logErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines := strings.Split(string(logOutput), "\n")
|
||||||
|
repoIdentifier := repo.Owner + "/" + repo.Name
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmedLine := strings.TrimSpace(line)
|
||||||
|
if trimmedLine == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if index, exists := mapAuthorToIndex[trimmedLine]; exists {
|
||||||
|
isRepoListed := false
|
||||||
|
for _, foundRepo := range authorOccurrences[index].FoundIn {
|
||||||
|
if foundRepo == repoIdentifier {
|
||||||
|
isRepoListed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isRepoListed {
|
||||||
|
authorOccurrences[index].FoundIn = append(authorOccurrences[index].FoundIn, repoIdentifier)
|
||||||
|
slices.Sort(authorOccurrences[index].FoundIn)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parts := strings.SplitN(trimmedLine, " <", 2)
|
||||||
|
var authorName, authorEmail string
|
||||||
|
if len(parts) == 2 {
|
||||||
|
authorName = parts[0]
|
||||||
|
authorEmail = strings.TrimSuffix(parts[1], ">")
|
||||||
|
} else if len(parts) == 1 {
|
||||||
|
authorName = "-"
|
||||||
|
authorEmail = strings.TrimPrefix(strings.TrimSuffix(parts[0], ">"), "<")
|
||||||
|
} else {
|
||||||
|
r.Logger.Error("Malformed author line from git log", "line", trimmedLine, "repo", repoIdentifier)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
authorOccurrences = append(authorOccurrences, AuthorOccurrence{
|
||||||
|
Name: authorName,
|
||||||
|
Email: authorEmail,
|
||||||
|
FoundIn: []string{repoIdentifier},
|
||||||
|
})
|
||||||
|
mapAuthorToIndex[trimmedLine] = len(authorOccurrences) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, author := range authorOccurrences {
|
||||||
|
r.PrintInfo(
|
||||||
|
"Author",
|
||||||
|
author.Name+" <"+author.Email+">",
|
||||||
|
"found in:"+strings.Join(author.FoundIn, ", "),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.PrintInfo("INFO", "Now searching for emails in cloned repositories, this may take a while...")
|
||||||
|
emails, err := findEmailsAndOccurrencesInDir(tmp_folder)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Error("Failed to find emails in directory", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(emails) == 0 {
|
||||||
|
r.PrintInfo("INFO", "No emails found")
|
||||||
|
} else {
|
||||||
|
r.PrintInfo("INFO", "Found emails:")
|
||||||
|
for _, email := range emails {
|
||||||
|
r.PrintInfo("Email", email.Email, "found in:"+strings.Join(email.FoundIn, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Repositories = repositories
|
||||||
|
response.Authors = authorOccurrences
|
||||||
|
response.Emails = emails
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
+157
-74
@@ -4,108 +4,191 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r Recon) SshKeys(username string) {
|
type SSHKeyResult struct {
|
||||||
sshKeys, resp, err := r.client.Users.ListKeys(r.ctx, username, nil)
|
ID string
|
||||||
|
Url string
|
||||||
|
Title string
|
||||||
|
CreatedAt string
|
||||||
|
Key string
|
||||||
|
ReadOnly string
|
||||||
|
Verified string
|
||||||
|
LastUsed string
|
||||||
|
AddedBy string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Recon) SshKeys(username string) (response []SSHKeyResult) {
|
||||||
|
sshKeys, resp, err := r.Client.Users.ListKeys(r.Ctx, username, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("Failed to fetch ssh keys", "err", err)
|
r.Logger.Error("Failed to fetch ssh keys", "err", err)
|
||||||
} else if len(sshKeys) == 0 {
|
} else if len(sshKeys) == 0 {
|
||||||
PrintTitle("🔑 SSH Keys")
|
r.PrintTitle("🔑 SSH Keys")
|
||||||
r.logger.Info("No SSH Keys found\n")
|
r.PrintInfo("INFO", "No SSH Keys found")
|
||||||
} else {
|
} else {
|
||||||
PrintTitle("🔑 SSH Keys")
|
r.PrintTitle("🔑 SSH Keys")
|
||||||
for i, key := range sshKeys {
|
for i, key := range sshKeys {
|
||||||
PrintInfo("Key n°", fmt.Sprintf("%d", i))
|
k := SSHKeyResult{
|
||||||
PrintInfo("ID", fmt.Sprintf("%d", key.GetID()))
|
ID: fmt.Sprintf("%d", key.GetID()),
|
||||||
PrintInfo("URL", key.GetURL())
|
Url: key.GetURL(),
|
||||||
PrintInfo("Title", key.GetTitle())
|
Title: key.GetTitle(),
|
||||||
PrintInfo("Created At", key.GetCreatedAt().String())
|
CreatedAt: key.GetCreatedAt().String(),
|
||||||
PrintInfo("Key", key.GetKey())
|
Key: key.GetKey(),
|
||||||
PrintInfo("Read Only", fmt.Sprintf("%t", key.GetReadOnly()))
|
ReadOnly: fmt.Sprintf("%t", key.GetReadOnly()),
|
||||||
PrintInfo("Verified", fmt.Sprintf("%t", key.GetVerified()))
|
Verified: fmt.Sprintf("%t", key.GetVerified()),
|
||||||
PrintInfo("Last Used", key.GetLastUsed().String())
|
LastUsed: key.GetLastUsed().String(),
|
||||||
PrintInfo("Added By", key.GetAddedBy())
|
AddedBy: key.GetAddedBy(),
|
||||||
fmt.Println()
|
}
|
||||||
|
response = append(response, k)
|
||||||
|
r.PrintInfo("Key n°", fmt.Sprintf("%d", i))
|
||||||
|
r.PrintInfo("ID", k.ID)
|
||||||
|
r.PrintInfo("URL", k.Url)
|
||||||
|
r.PrintInfo("Title", k.Title)
|
||||||
|
r.PrintInfo("Created At", k.CreatedAt)
|
||||||
|
r.PrintInfo("Key", k.Key)
|
||||||
|
r.PrintInfo("Read Only", k.ReadOnly)
|
||||||
|
r.PrintInfo("Verified", k.Verified)
|
||||||
|
r.PrintInfo("Last Used", k.LastUsed)
|
||||||
|
r.PrintInfo("Added By", k.AddedBy)
|
||||||
|
if i != len(sshKeys)-1 {
|
||||||
|
r.PrintNewline()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
r.PrintNewline()
|
||||||
WaitForRateLimit(resp)
|
WaitForRateLimit(resp)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Recon) GpgKeys(username string) {
|
type GPGKeyEmail struct {
|
||||||
gpgKeys, resp, err := r.client.Users.ListGPGKeys(r.ctx, username, nil)
|
Email string
|
||||||
|
Verified string
|
||||||
|
}
|
||||||
|
type GPGKeyResult struct {
|
||||||
|
ID string
|
||||||
|
KeyID string
|
||||||
|
PublicKey string
|
||||||
|
CreatedAt string
|
||||||
|
PrimaryKeyID string
|
||||||
|
RawKey string
|
||||||
|
Emails []GPGKeyEmail
|
||||||
|
Subkeys []GPGKeyResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Recon) GpgKeys(username string) (response []GPGKeyResult) {
|
||||||
|
gpgKeys, resp, err := r.Client.Users.ListGPGKeys(r.Ctx, username, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("Failed to fetch user's gpg keys", "err", err)
|
r.Logger.Error("Failed to fetch user's gpg keys", "err", err)
|
||||||
} else if len(gpgKeys) == 0 {
|
} else if len(gpgKeys) == 0 {
|
||||||
PrintTitle("🗝️ GPG Keys")
|
r.PrintTitle("🗝️ GPG Keys")
|
||||||
r.logger.Info("No GPG Keys found\n")
|
r.PrintInfo("INFO", "No GPG Keys found")
|
||||||
} else {
|
} else {
|
||||||
PrintTitle("🗝️ GPG Keys")
|
r.PrintTitle("🗝️ GPG Keys")
|
||||||
for i, key := range gpgKeys {
|
for i, key := range gpgKeys {
|
||||||
PrintInfo("Key n°", fmt.Sprintf("%d", i))
|
k := GPGKeyResult{
|
||||||
PrintInfo("ID", fmt.Sprintf("%d", key.GetID()))
|
ID: fmt.Sprintf("%d", key.GetID()),
|
||||||
PrintInfo("Key ID", key.GetKeyID())
|
KeyID: key.GetKeyID(),
|
||||||
PrintInfo("Public Key", key.GetPublicKey())
|
PublicKey: key.GetPublicKey(),
|
||||||
PrintInfo("Created At", key.GetCreatedAt().String())
|
CreatedAt: key.GetCreatedAt().String(),
|
||||||
PrintInfo("Expires At", key.GetExpiresAt().String())
|
PrimaryKeyID: fmt.Sprintf("%d", key.GetPrimaryKeyID()),
|
||||||
PrintInfo("Can Sign", fmt.Sprintf("%t", key.GetCanSign()))
|
RawKey: key.GetRawKey(),
|
||||||
PrintInfo("Can Encrypt Comms", fmt.Sprintf("%t", key.GetCanEncryptComms()))
|
Emails: []GPGKeyEmail{},
|
||||||
PrintInfo("Can Encrypt Storage", fmt.Sprintf("%t", key.GetCanEncryptStorage()))
|
Subkeys: []GPGKeyResult{},
|
||||||
PrintInfo("Can Certify", fmt.Sprintf("%t", key.GetCanCertify()))
|
|
||||||
PrintInfo("Primary Key ID", fmt.Sprintf("%d", key.GetPrimaryKeyID()))
|
|
||||||
PrintInfo("Raw Key", key.GetRawKey())
|
|
||||||
PrintInfo("Emails", fmt.Sprintf("%d", len(key.Emails)))
|
|
||||||
for j, email := range key.Emails {
|
|
||||||
PrintInfo(" Email n°", fmt.Sprintf("%d", j))
|
|
||||||
PrintInfo(" Email", email.GetEmail())
|
|
||||||
PrintInfo(" Verified", fmt.Sprintf("%t", email.GetVerified()))
|
|
||||||
}
|
}
|
||||||
PrintInfo("Subkeys", fmt.Sprintf("%d", len(key.Subkeys)))
|
for _, email := range key.Emails {
|
||||||
for j, subkey := range key.Subkeys {
|
email := GPGKeyEmail{
|
||||||
PrintInfo(" Subkey n°", fmt.Sprintf("%d", j))
|
Email: email.GetEmail(),
|
||||||
PrintInfo(" Subkey ID", fmt.Sprintf("%d", subkey.GetID()))
|
Verified: fmt.Sprintf("%t", email.GetVerified()),
|
||||||
PrintInfo(" Subkey Key ID", subkey.GetKeyID())
|
}
|
||||||
PrintInfo(" Subkey Created At", subkey.GetCreatedAt().String())
|
k.Emails = append(k.Emails, email)
|
||||||
PrintInfo(" Subkey Expires At", subkey.GetExpiresAt().String())
|
}
|
||||||
PrintInfo(" Subkey Can Sign", fmt.Sprintf("%t", subkey.GetCanSign()))
|
for _, subkey := range key.Subkeys {
|
||||||
PrintInfo(
|
subkey := GPGKeyResult{
|
||||||
" Subkey Can Encrypt Comms",
|
ID: fmt.Sprintf("%d", subkey.GetID()),
|
||||||
fmt.Sprintf("%t", subkey.GetCanEncryptComms()),
|
KeyID: subkey.GetKeyID(),
|
||||||
)
|
PublicKey: subkey.GetPublicKey(),
|
||||||
PrintInfo(
|
CreatedAt: subkey.GetCreatedAt().String(),
|
||||||
" Subkey Can Encrypt Storage",
|
PrimaryKeyID: fmt.Sprintf("%d", subkey.GetPrimaryKeyID()),
|
||||||
fmt.Sprintf("%t", subkey.GetCanEncryptStorage()),
|
RawKey: subkey.GetRawKey(),
|
||||||
)
|
}
|
||||||
PrintInfo(" Subkey Can Certify", fmt.Sprintf("%t", subkey.GetCanCertify()))
|
k.Subkeys = append(k.Subkeys, subkey)
|
||||||
PrintInfo(" Subkey Primary Key ID", fmt.Sprintf("%d", subkey.GetPrimaryKeyID()))
|
}
|
||||||
PrintInfo(" Subkey Raw Key", subkey.GetRawKey())
|
response = append(response, k)
|
||||||
PrintInfo(" Subkey Public Key", subkey.GetPublicKey())
|
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()
|
||||||
}
|
}
|
||||||
fmt.Println()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
r.PrintNewline()
|
||||||
WaitForRateLimit(resp)
|
WaitForRateLimit(resp)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Recon) SshSigningKeys(username string) {
|
type SSHSigningKeyResult struct {
|
||||||
signingKeys, resp, err := r.client.Users.ListSSHSigningKeys(
|
ID string
|
||||||
r.ctx,
|
Title string
|
||||||
|
CreatedAt string
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Recon) SshSigningKeys(username string) (response []SSHSigningKeyResult) {
|
||||||
|
signingKeys, resp, err := r.Client.Users.ListSSHSigningKeys(
|
||||||
|
r.Ctx,
|
||||||
username,
|
username,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("Failed to fetch user's ssh signing keys", "err", err)
|
r.Logger.Error("Failed to fetch user's ssh signing keys", "err", err)
|
||||||
} else if len(signingKeys) == 0 {
|
} else if len(signingKeys) == 0 {
|
||||||
PrintTitle("📝 SSH Signing Keys")
|
r.PrintTitle("📝 SSH Signing Keys")
|
||||||
r.logger.Info("No SSH Signing Keys found\n")
|
r.PrintInfo("INFO", "No SSH Signing Keys found")
|
||||||
} else {
|
} else {
|
||||||
PrintTitle("📝 SSH Signing Keys")
|
r.PrintTitle("📝 SSH Signing Keys")
|
||||||
for i, key := range signingKeys {
|
for i, key := range signingKeys {
|
||||||
PrintInfo("Key n°", fmt.Sprintf("%d", i))
|
k := SSHSigningKeyResult{
|
||||||
PrintInfo("ID", fmt.Sprintf("%d", key.GetID()))
|
ID: fmt.Sprintf("%d", key.GetID()),
|
||||||
PrintInfo("Key", key.GetKey())
|
Title: key.GetTitle(),
|
||||||
PrintInfo("Title", key.GetTitle())
|
CreatedAt: key.GetCreatedAt().String(),
|
||||||
PrintInfo("Created At", key.GetCreatedAt().String())
|
Key: key.GetKey(),
|
||||||
fmt.Println()
|
}
|
||||||
|
r.PrintInfo("Key n°", fmt.Sprintf("%d", i))
|
||||||
|
r.PrintInfo("ID", k.ID)
|
||||||
|
r.PrintInfo("Title", k.Title)
|
||||||
|
r.PrintInfo("Created At", k.CreatedAt)
|
||||||
|
r.PrintInfo("Key", k.Key)
|
||||||
|
if i != len(signingKeys)-1 {
|
||||||
|
r.PrintNewline()
|
||||||
|
}
|
||||||
|
response = append(response, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WaitForRateLimit(resp)
|
WaitForRateLimit(resp)
|
||||||
|
r.PrintNewline()
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-11
@@ -8,15 +8,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Recon struct {
|
type Recon struct {
|
||||||
client *github.Client
|
Client *github.Client
|
||||||
logger *log.Logger
|
Logger *log.Logger
|
||||||
ctx context.Context
|
Ctx context.Context
|
||||||
}
|
Silent bool
|
||||||
|
JsonFile string
|
||||||
func NewRecon(client *github.Client, logger *log.Logger, ctx context.Context) *Recon {
|
MaxRepoSize int
|
||||||
return &Recon{
|
|
||||||
client: client,
|
|
||||||
logger: logger,
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-20
@@ -4,31 +4,41 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r Recon) Orgs(username string) {
|
type OrgResult struct {
|
||||||
orgs, resp, err := r.client.Organizations.List(r.ctx, username, nil)
|
Login string
|
||||||
|
ID string
|
||||||
|
URL string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Recon) Orgs(username string) (response []OrgResult) {
|
||||||
|
orgs, resp, err := r.Client.Organizations.List(r.Ctx, username, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("Failed to fetch organizations", "err", err)
|
r.Logger.Error("Failed to fetch organizations", "err", err)
|
||||||
} else if len(orgs) == 0 {
|
} else if len(orgs) == 0 {
|
||||||
PrintTitle("🏢 Organizations")
|
r.PrintTitle("🏢 Organizations")
|
||||||
r.logger.Info("No Organizations found\n")
|
r.PrintInfo("INFO", "No Organizations found")
|
||||||
} else {
|
} else {
|
||||||
PrintTitle("🏢 Organizations")
|
r.PrintTitle("🏢 Organizations")
|
||||||
for i, org := range orgs {
|
for i, org := range orgs {
|
||||||
PrintInfo("Orgs n°", fmt.Sprintf("%d", i))
|
o := OrgResult{
|
||||||
PrintInfo("Login", org.GetLogin())
|
Login: org.GetLogin(),
|
||||||
PrintInfo("ID", fmt.Sprintf("%d", org.GetID()))
|
ID: fmt.Sprintf("%d", org.GetID()),
|
||||||
PrintInfo("Node ID", org.GetNodeID())
|
URL: org.GetURL(),
|
||||||
PrintInfo("URL", org.GetURL())
|
Description: org.GetDescription(),
|
||||||
PrintInfo("Repos URL", org.GetReposURL())
|
}
|
||||||
PrintInfo("Events URL", org.GetEventsURL())
|
r.PrintInfo("Organization n°", fmt.Sprintf("%d", i))
|
||||||
PrintInfo("Hooks URL", org.GetHooksURL())
|
r.PrintInfo("Login", o.Login)
|
||||||
PrintInfo("Issues URL", org.GetIssuesURL())
|
r.PrintInfo("ID", o.ID)
|
||||||
PrintInfo("Members URL", org.GetMembersURL())
|
r.PrintInfo("URL", o.URL)
|
||||||
PrintInfo("Public Members URL", org.GetPublicMembersURL())
|
r.PrintInfo("Description", o.Description)
|
||||||
PrintInfo("Avatar URL", org.GetAvatarURL())
|
if i != len(orgs)-1 {
|
||||||
PrintInfo("Description", org.GetDescription())
|
r.PrintNewline()
|
||||||
fmt.Println()
|
}
|
||||||
|
response = append(response, o)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
r.PrintNewline()
|
||||||
WaitForRateLimit(resp)
|
WaitForRateLimit(resp)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-18
@@ -5,36 +5,37 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r Recon) Socials(username string) {
|
type SocialResult struct {
|
||||||
resp, err := FetchGitHubAPI(r.client, "", "/users/"+username+"/social_accounts")
|
Provider string `json:"provider"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Recon) Socials(username string) (response []SocialResult) {
|
||||||
|
resp, err := FetchGitHubAPI(r.Client, "", "/users/"+username+"/social_accounts")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("Failed to fetch socials", "err", err)
|
r.Logger.Error("Failed to fetch socials", "err", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type SocialAccount struct {
|
var socialAccounts []SocialResult
|
||||||
Provider string `json:"provider"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
type SocialAccounts []SocialAccount
|
|
||||||
|
|
||||||
var socialAccounts SocialAccounts
|
|
||||||
err = json.Unmarshal(resp, &socialAccounts)
|
err = json.Unmarshal(resp, &socialAccounts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("Failed to unmarshal socials", "err", err)
|
r.Logger.Error("Failed to unmarshal socials", "err", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(socialAccounts) == 0 {
|
if len(socialAccounts) == 0 {
|
||||||
PrintTitle("🐥 Socials")
|
r.PrintTitle("🐥 Socials")
|
||||||
PrintTitle("No Socials found\n")
|
r.PrintInfo("INFO", "No commits found")
|
||||||
} else {
|
} else {
|
||||||
PrintTitle("🐥 Socials")
|
r.PrintTitle("🐥 Socials")
|
||||||
for i, account := range socialAccounts {
|
for i, account := range socialAccounts {
|
||||||
PrintInfo("Social n°", fmt.Sprintf("%d", i))
|
r.PrintInfo("Social n°", fmt.Sprintf("%d", i))
|
||||||
PrintInfo("Provider", account.Provider)
|
r.PrintInfo("Provider", account.Provider)
|
||||||
PrintInfo("URL", account.URL)
|
r.PrintInfo("URL", account.URL)
|
||||||
fmt.Println()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
r.PrintNewline()
|
||||||
|
|
||||||
|
return socialAccounts
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-31
@@ -4,42 +4,89 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r Recon) User(username string) {
|
type UserResult struct {
|
||||||
user, resp, err := r.client.Users.Get(r.ctx, username)
|
Username string
|
||||||
|
ID string
|
||||||
|
AvatarURL string
|
||||||
|
GravatarID string
|
||||||
|
Name string
|
||||||
|
Company string
|
||||||
|
Location string
|
||||||
|
Email string
|
||||||
|
Hireable string
|
||||||
|
Bio string
|
||||||
|
PublicRepos string
|
||||||
|
PublicGists string
|
||||||
|
Followers string
|
||||||
|
Following string
|
||||||
|
CreatedAt string
|
||||||
|
UpdatedAt string
|
||||||
|
SuspendedAt string
|
||||||
|
TotalPrivateRepos string
|
||||||
|
PrivateGists string
|
||||||
|
DiskUsage string
|
||||||
|
Collaborators string
|
||||||
|
Plan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Recon) User(username string) (response UserResult) {
|
||||||
|
user, resp, err := r.Client.Users.Get(r.Ctx, username)
|
||||||
if resp.StatusCode == 404 {
|
if resp.StatusCode == 404 {
|
||||||
r.logger.Fatal("User not found")
|
r.Logger.Fatal("User not found")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Fatal("Failed to fetch user's information", "err", err)
|
r.Logger.Fatal("Failed to fetch user's information", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
PrintTitle("👤 User informations")
|
r.PrintTitle("👤 User informations")
|
||||||
PrintInfo("Username", user.GetLogin())
|
u := UserResult{
|
||||||
PrintInfo("ID", fmt.Sprintf("%d", user.GetID()))
|
Username: user.GetLogin(),
|
||||||
PrintInfo("Avatar URL", user.GetAvatarURL())
|
ID: fmt.Sprintf("%d", user.GetID()),
|
||||||
PrintInfo("Gravatar ID", user.GetGravatarID())
|
AvatarURL: user.GetAvatarURL(),
|
||||||
PrintInfo("Name", user.GetName())
|
GravatarID: user.GetGravatarID(),
|
||||||
PrintInfo("Company", user.GetCompany())
|
Name: user.GetName(),
|
||||||
PrintInfo("Location", user.GetLocation())
|
Company: user.GetCompany(),
|
||||||
PrintInfo("Email", user.GetEmail())
|
Location: user.GetLocation(),
|
||||||
PrintInfo("Hireable", fmt.Sprintf("%t", user.GetHireable()))
|
Email: user.GetEmail(),
|
||||||
PrintInfo("Bio", user.GetBio())
|
Hireable: fmt.Sprintf("%t", user.GetHireable()),
|
||||||
PrintInfo("Public Repos", fmt.Sprintf("%d", user.GetPublicRepos()))
|
Bio: user.GetBio(),
|
||||||
PrintInfo("Public Gists", fmt.Sprintf("%d", user.GetPublicGists()))
|
PublicRepos: fmt.Sprintf("%d", user.GetPublicRepos()),
|
||||||
PrintInfo("Followers", fmt.Sprintf("%d", user.GetFollowers()))
|
PublicGists: fmt.Sprintf("%d", user.GetPublicGists()),
|
||||||
PrintInfo("Following", fmt.Sprintf("%d", user.GetFollowing()))
|
Followers: fmt.Sprintf("%d", user.GetFollowers()),
|
||||||
PrintInfo("Created At", user.GetCreatedAt().String())
|
Following: fmt.Sprintf("%d", user.GetFollowing()),
|
||||||
PrintInfo("Updated At", user.GetUpdatedAt().String())
|
CreatedAt: user.GetCreatedAt().String(),
|
||||||
PrintInfo("Suspended At", user.GetSuspendedAt().String())
|
UpdatedAt: user.GetUpdatedAt().String(),
|
||||||
PrintInfo("Type", user.GetType())
|
SuspendedAt: user.GetSuspendedAt().String(),
|
||||||
PrintInfo("Site Admin", fmt.Sprintf("%t", user.GetSiteAdmin()))
|
TotalPrivateRepos: fmt.Sprintf("%d", user.GetTotalPrivateRepos()),
|
||||||
PrintInfo("Total Private Repos", fmt.Sprintf("%d", user.GetTotalPrivateRepos()))
|
PrivateGists: fmt.Sprintf("%d", user.GetPrivateGists()),
|
||||||
PrintInfo("Owned Private Repos", fmt.Sprintf("%d", user.GetOwnedPrivateRepos()))
|
DiskUsage: fmt.Sprintf("%d", user.GetDiskUsage()),
|
||||||
PrintInfo("Private Gists", fmt.Sprintf("%d", user.GetPrivateGists()))
|
Collaborators: fmt.Sprintf("%d", user.GetCollaborators()),
|
||||||
PrintInfo("Disk Usage", fmt.Sprintf("%d", user.GetDiskUsage()))
|
Plan: user.GetPlan().GetName(),
|
||||||
PrintInfo("Collaborators", fmt.Sprintf("%d", user.GetCollaborators()))
|
}
|
||||||
PrintInfo("Plan", user.GetPlan().GetName())
|
r.PrintInfo("Username", u.Username)
|
||||||
fmt.Println()
|
r.PrintInfo("ID", u.ID)
|
||||||
|
r.PrintInfo("Avatar URL", u.AvatarURL)
|
||||||
|
r.PrintInfo("Gravatar ID", u.GravatarID)
|
||||||
|
r.PrintInfo("Name", u.Name)
|
||||||
|
r.PrintInfo("Company", u.Company)
|
||||||
|
r.PrintInfo("Location", u.Location)
|
||||||
|
r.PrintInfo("Email", u.Email)
|
||||||
|
r.PrintInfo("Hireable", u.Hireable)
|
||||||
|
r.PrintInfo("Bio", u.Bio)
|
||||||
|
r.PrintInfo("Public Repos", u.PublicRepos)
|
||||||
|
r.PrintInfo("Public Gists", u.PublicGists)
|
||||||
|
r.PrintInfo("Followers", u.Followers)
|
||||||
|
r.PrintInfo("Following", u.Following)
|
||||||
|
r.PrintInfo("Created At", u.CreatedAt)
|
||||||
|
r.PrintInfo("Updated At", u.UpdatedAt)
|
||||||
|
r.PrintInfo("Suspended At", u.SuspendedAt)
|
||||||
|
r.PrintInfo("Total Private Repos", u.TotalPrivateRepos)
|
||||||
|
r.PrintInfo("Private Gists", u.PrivateGists)
|
||||||
|
r.PrintInfo("Disk Usage", u.DiskUsage)
|
||||||
|
r.PrintInfo("Collaborators", u.Collaborators)
|
||||||
|
r.PrintInfo("Plan", u.Plan)
|
||||||
|
r.PrintNewline()
|
||||||
|
|
||||||
WaitForRateLimit(resp)
|
WaitForRateLimit(resp)
|
||||||
|
return u
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-5
@@ -1,9 +1,11 @@
|
|||||||
package ghrecon
|
package ghrecon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -22,9 +24,14 @@ var (
|
|||||||
RedStyle = lipgloss.NewStyle().Foreground(Red)
|
RedStyle = lipgloss.NewStyle().Foreground(Red)
|
||||||
)
|
)
|
||||||
|
|
||||||
func Header() {
|
func (r Recon) Header() {
|
||||||
|
if r.Silent {
|
||||||
|
return
|
||||||
|
}
|
||||||
asciiArt := " __ \n ___ _/ / _______ _______ ___ \n / _ `/ _ \\/ __/ -_) __/ _ \\/ _ \\\n \\_, /_//_/_/ \\__/\\__/\\___/_//_/\n/___/ "
|
asciiArt := " __ \n ___ _/ / _______ _______ ___ \n / _ `/ _ \\/ __/ -_) __/ _ \\/ _ \\\n \\_, /_//_/_/ \\__/\\__/\\___/_//_/\n/___/ "
|
||||||
fmt.Println(GreyStyle.Render(lipgloss.JoinVertical(lipgloss.Right, asciiArt, "@anotherhadi\n")))
|
fmt.Println(
|
||||||
|
GreyStyle.Render(lipgloss.JoinVertical(lipgloss.Right, asciiArt, "@anotherhadi\n")),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseUsername(username string) error {
|
func ParseUsername(username string) error {
|
||||||
@@ -59,7 +66,9 @@ func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error executing request for %s: %w", url, err)
|
return nil, fmt.Errorf("error executing request for %s: %w", url, err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
@@ -83,12 +92,25 @@ func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) {
|
|||||||
return bodyBytes, nil
|
return bodyBytes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrintTitle(title string) {
|
func (r Recon) PrintNewline() {
|
||||||
|
if r.Silent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Recon) PrintTitle(title string) {
|
||||||
|
if r.Silent {
|
||||||
|
return
|
||||||
|
}
|
||||||
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7287fd"))
|
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7287fd"))
|
||||||
fmt.Println(style.Render(title) + "\n")
|
fmt.Println(style.Render(title) + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrintInfo(key, value string, more ...string) {
|
func (r Recon) PrintInfo(key, value string, more ...string) {
|
||||||
|
if r.Silent {
|
||||||
|
return
|
||||||
|
}
|
||||||
if value == "" || value == "0001-01-01 00:00:00 +0000 UTC" {
|
if value == "" || value == "0001-01-01 00:00:00 +0000 UTC" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -111,3 +133,42 @@ func WaitForRateLimit(resp *github.Response) {
|
|||||||
time.Sleep(time.Until(resp.Rate.Reset.Time) + time.Second)
|
time.Sleep(time.Until(resp.Rate.Reset.Time) + time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SkipResult(name, email string) bool {
|
||||||
|
if name == "github-actions[bot]" || name == "github-actions" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if email == "github-actions[bot]@users.noreply.github.com" ||
|
||||||
|
email == "[email protected]" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Recon) WriteJson(data any) {
|
||||||
|
if r.JsonFile == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file, err := os.Create(r.JsonFile)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Error("Failed to create JSON file", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = file.Close()
|
||||||
|
}()
|
||||||
|
as_json, _ := json.MarshalIndent(data, "", "\t")
|
||||||
|
_, err = file.Write(as_json)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Error("Failed to write to JSON file", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.PrintInfo("INFO", "JSON file created successfully", "file", r.JsonFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func folderExists(path string) bool {
|
||||||
|
if stat, err := os.Stat(path); err == nil && stat.IsDir() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ require (
|
|||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/log v0.4.1
|
github.com/charmbracelet/log v0.4.1
|
||||||
github.com/google/go-github/v72 v72.0.0
|
github.com/google/go-github/v72 v72.0.0
|
||||||
|
github.com/spf13/pflag v1.0.6
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
|||||||
@@ -2,63 +2,156 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
ghrecon "github.com/anotherhadi/gh-recon/gh-recon"
|
ghrecon "github.com/anotherhadi/gh-recon/gh-recon"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/google/go-github/v72/github"
|
"github.com/google/go-github/v72/github"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var username string
|
var username string
|
||||||
var token string
|
var token string
|
||||||
flag.StringVar(&username, "username", "", "Target username")
|
var onlyCommitsLeak bool
|
||||||
flag.StringVar(&token, "token", "", "Github token")
|
var fromEmail string
|
||||||
|
var deep bool
|
||||||
|
var silent bool
|
||||||
|
var jsonFile string
|
||||||
|
var excludeRepos string
|
||||||
|
var maxRepoSize int
|
||||||
|
var refresh bool
|
||||||
|
|
||||||
|
// FLAGS
|
||||||
|
flag.StringVarP(&username, "username", "u", "", "GitHub username to analyze")
|
||||||
|
flag.StringVarP(&token, "token", "t", "", "GitHub personal access token (e.g. ghp_...)")
|
||||||
|
flag.StringVarP(&fromEmail, "email", "e", "", "Search accounts by email address")
|
||||||
|
flag.BoolVarP(
|
||||||
|
&deep,
|
||||||
|
"deep",
|
||||||
|
"d",
|
||||||
|
false,
|
||||||
|
"Enable deep scan (clone repos, regex search, analyse licenses, etc.)",
|
||||||
|
)
|
||||||
|
flag.IntVar(
|
||||||
|
&maxRepoSize,
|
||||||
|
"max-size",
|
||||||
|
150,
|
||||||
|
"Limit the size of repositories to scan (in MB) (only for deep scan)",
|
||||||
|
)
|
||||||
|
flag.StringVar(
|
||||||
|
&excludeRepos,
|
||||||
|
"exclude-repo",
|
||||||
|
"",
|
||||||
|
"Exclude repos from deep scan (comma-separated list, only for deep scan)",
|
||||||
|
)
|
||||||
|
flag.BoolVarP(
|
||||||
|
&refresh,
|
||||||
|
"refresh",
|
||||||
|
"r",
|
||||||
|
false,
|
||||||
|
"Refresh the cache (only for deep scan)",
|
||||||
|
)
|
||||||
|
flag.BoolVarP(
|
||||||
|
&onlyCommitsLeak,
|
||||||
|
"only-commits",
|
||||||
|
"c",
|
||||||
|
false,
|
||||||
|
"Display only commits with author info",
|
||||||
|
)
|
||||||
|
flag.BoolVarP(&silent, "silent", "s", false, "Suppress all non-essential output")
|
||||||
|
flag.StringVarP(&jsonFile, "json", "j", "", "Write results to specified JSON file")
|
||||||
|
|
||||||
|
// FLAGS SETTINGS
|
||||||
|
flag.CommandLine.SetNormalizeFunc(wordSepNormalizeFunc)
|
||||||
|
flag.CommandLine.SortFlags = false
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if username == "" {
|
// INITIALIZE RECON OBJECT
|
||||||
fmt.Println("Please provide a username with the --username flag")
|
|
||||||
os.Exit(1)
|
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 := ghrecon.ParseUsername(username)
|
|
||||||
if err != nil {
|
// CHECK FLAGS
|
||||||
log.Error("Invalid username", "err", err)
|
|
||||||
os.Exit(1)
|
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 == "" {
|
if token == "" {
|
||||||
log.Info(
|
r.PrintInfo(
|
||||||
"It's recommended to set a Github token for better rate limits. You can set it using the --token flag.",
|
"INFO",
|
||||||
|
"It's recommended to set a Github token for better rate limits. You can set it using the --token (-t) flag.",
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
client = client.WithAuthToken(token)
|
r.Client = r.Client.WithAuthToken(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
// START
|
||||||
|
|
||||||
styles := log.DefaultStyles()
|
r.Header()
|
||||||
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)
|
|
||||||
|
|
||||||
r := ghrecon.NewRecon(
|
if fromEmail != "" {
|
||||||
client,
|
emailsInfo := r.Email(fromEmail)
|
||||||
logger,
|
r.WriteJson(
|
||||||
ctx,
|
map[string]any{
|
||||||
)
|
"Authors": emailsInfo,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ghrecon.Header()
|
if onlyCommitsLeak {
|
||||||
r.User(username)
|
commitsInfo := r.Commits(username)
|
||||||
r.Orgs(username)
|
r.WriteJson(
|
||||||
r.SshKeys(username)
|
map[string]any{
|
||||||
r.GpgKeys(username)
|
"Authors": commitsInfo,
|
||||||
r.SshSigningKeys(username)
|
},
|
||||||
r.Socials(username)
|
)
|
||||||
r.Commits(username)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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