mirror of
https://github.com/anotherhadi/github-recon.git
synced 2026-10-05 19:08:24 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53dd8ea2d5 | ||
|
|
832e996708 | ||
|
|
8528fe5732 | ||
|
|
85ee3a6333 | ||
|
|
cb694d896d | ||
|
|
1dfa0c1620 | ||
|
|
ef0240fd65 | ||
|
|
d88bc6ae1e | ||
|
|
c460e5c24f | ||
|
|
e572d1326a | ||
|
|
0017d649d3 | ||
|
|
c163fb4ecd | ||
|
|
325397dfef | ||
|
|
dab0ba2b38 | ||
|
|
98769561c3 | ||
|
|
c76953882c | ||
|
|
94fd83ecce | ||
|
|
2d01c1639f | ||
|
|
66030c1c16 | ||
|
|
bb8f34055d | ||
|
|
26a1cfdd37 | ||
|
|
3064cdbf7a | ||
|
|
52f9f1f3e7 | ||
|
|
c7a1689084 | ||
|
|
fd79420749 | ||
|
|
ebf7dd529f | ||
|
|
c84b2b0458 | ||
|
|
76e8eee4dc | ||
|
|
c67901c9ef | ||
|
|
40ff397df7 | ||
|
|
a17caa6233 | ||
|
|
9b914e14ba | ||
|
|
b0c7ce633e | ||
|
|
beec1a873c | ||
|
|
d5781169b7 | ||
|
|
ecb9180283 | ||
|
|
8e2715e0ef | ||
|
|
7b8cb1654f | ||
|
|
426d10b2fe | ||
|
|
df6c8bca11 | ||
|
|
39b72bc444 | ||
|
|
586c91103a | ||
|
|
00b63b4bc3 | ||
|
|
56bc018128 | ||
|
|
f1a13065fb | ||
|
|
7037ec0864 | ||
|
|
6b11b26678 | ||
|
|
6229445251 | ||
|
|
c1d732e03a | ||
|
|
7b9c781281 | ||
|
|
522e0671e1 | ||
|
|
862cf3659c | ||
|
|
af4046d234 |
Binary file not shown.
|
Before Width: | Height: | Size: 192 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 9.3 KiB |
+7
-3
@@ -1,10 +1,14 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Everybody is invited and welcome to contribute to this repo. There is a lot to do... Check the issues!
|
Everybody is invited and welcome to contribute to this repo. There is a lot to
|
||||||
|
do... Check the issues!
|
||||||
|
|
||||||
The process is straight-forward.
|
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)
|
- Read
|
||||||
- [Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) this repo.
|
[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, ...).
|
- Write your changes (bug fixe, new feature, issues fix, ...).
|
||||||
- Create a Pull Request against the main branch.
|
- Create a Pull Request against the main branch.
|
||||||
|
|||||||
@@ -1,48 +1,78 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://raw.githubusercontent.com/anotherhadi/gh-recon/main/.github/assets/logo.png" width="120px" />
|
<img src="https://raw.githubusercontent.com/anotherhadi/github-recon/main/.github/assets/logo.png" width="120px" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
# GH-Recon
|
# Github-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/github-recon/releases"><img src="https://img.shields.io/github/release/anotherhadi/github-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://pkg.go.dev/github.com/anotherhadi/github-recon?tab=doc"><img src="https://godoc.org/github.com/anotherhadi/github-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>
|
<a href="https://goreportcard.com/report/github.com/anotherhadi/github-recon"><img src="https://goreportcard.com/badge/github.com/anotherhadi/github-recon" alt="GoReportCard"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Project Overview
|
- [🧾 Project Overview](#-project-overview)
|
||||||
|
- [🚀 Features](#-features)
|
||||||
|
- [⚠️ Disclaimer](#%EF%B8%8F-disclaimer)
|
||||||
|
- [📦 Installation](#-installation)
|
||||||
|
- [With Go](#with-go)
|
||||||
|
- [With Nix/NixOS](#with-nixnixos)
|
||||||
|
- [🧪 Usage](#-usage)
|
||||||
|
- [Flags](#flags)
|
||||||
|
- [Token](#token)
|
||||||
|
- [How does the email spoofing work?](#how-does-the-email-spoofing-work)
|
||||||
|
- [💡 Examples](#-examples)
|
||||||
|
- [🕵️♂️ Cover your tracks](#%EF%B8%8F%EF%B8%8F-cover-your-tracks)
|
||||||
|
- [🤝 Contributing](#-contributing)
|
||||||
|
- [🙏 Credits](#-credits)
|
||||||
|
|
||||||
Fetches and aggregates public OSINT data for a GitHub user, leveraging Go and the GitHub API.
|
## 🧾 Project Overview
|
||||||
|
|
||||||
## Features
|
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.
|
||||||
|
|
||||||
- Retrieve basic user profile information (username, ID, avatar, bio, creation dates)
|
<details>
|
||||||
|
<summary>Screenshot</summary>
|
||||||
|
<img src="https://raw.githubusercontent.com/anotherhadi/github-recon/main/.github/assets/example.png" alt="example screenshot">
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
- Export results to JSON
|
||||||
|
|
||||||
|
**From usernames:**
|
||||||
|
|
||||||
|
- Retrieve basic user profile information (username, ID, avatar, bio, creation
|
||||||
|
date)
|
||||||
|
- Display avatars directly in the terminal
|
||||||
- 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)
|
- Extract unique commit authors (name + email)
|
||||||
- Find close friends
|
- Find close friends
|
||||||
- Find Github accounts using an email address
|
- Deep scan option (clone repositories, run regex searches, analyze licenses,
|
||||||
- Export results to JSON
|
etc.)
|
||||||
- Deep scan option (clone repositories, regex search, analyze licenses, etc.)
|
- Use Levenshtein distance for matching usernames and emails
|
||||||
|
- TruffleHog integration to find secrets
|
||||||
|
|
||||||
## Disclaimer
|
**From emails:**
|
||||||
|
|
||||||
This tool is intended for educational purposes only. Use responsibly and ensure you have permission to access the data you are querying.
|
- Search for a specific email across all GitHub commits
|
||||||
|
- Spoof an email to discover the associated user account
|
||||||
|
|
||||||
## Prerequisites
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
- Go 1.18+
|
This tool is intended for educational purposes only. Use responsibly and ensure
|
||||||
- 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.
|
you have permission to access the data you are querying.
|
||||||
|
|
||||||
## Installation
|
## 📦 Installation
|
||||||
|
|
||||||
### With Go
|
### With Go
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install github.com/anotherhadi/gh-recon@latest
|
go install github.com/anotherhadi/github-recon@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### With Nix/NixOS
|
### With Nix/NixOS
|
||||||
@@ -53,7 +83,7 @@ go install github.com/anotherhadi/gh-recon@latest
|
|||||||
**From anywhere (using the repo URL):**
|
**From anywhere (using the repo URL):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix run github:anotherhadi/gh-recon -- --username TARGET_USER [--token YOUR_TOKEN]
|
nix run github:anotherhadi/github-recon -- [--flags value] target_username_or_email
|
||||||
```
|
```
|
||||||
|
|
||||||
**Permanent Installation:**
|
**Permanent Installation:**
|
||||||
@@ -62,62 +92,133 @@ nix run github:anotherhadi/gh-recon -- --username TARGET_USER [--token YOUR_TOKE
|
|||||||
# add the flake to your flake.nix
|
# add the flake to your flake.nix
|
||||||
{
|
{
|
||||||
inputs = {
|
inputs = {
|
||||||
gh-recon.url = "github:anotherhadi/gh-recon";
|
github-recon.url = "github:anotherhadi/github-recon";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
# then add it to your packages
|
# then add it to your packages
|
||||||
environment.systemPackages = with pkgs; [ # or home.packages
|
environment.systemPackages = with pkgs; [ # or home.packages
|
||||||
gh-recon
|
inputs.github-recon.defaultPackage.${pkgs.system}
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Usage
|
## 🧪 Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh-recon --username TARGET_USER [--token YOUR_TOKEN]
|
github-recon [--flags value] target_username_or_email
|
||||||
```
|
```
|
||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
-u, --username string GitHub username to analyze
|
-t, --token string Github personal access token (e.g. ghp_aaa...). Can also be set via GITHUB_RECON_TOKEN environment variable. You also need to set the token in $HOME/.config/github-recon/env file if you want to use this tool without passing the token every time. (default "null")
|
||||||
-t, --token string GitHub personal access token (e.g. ghp_...)
|
-d, --deepscan Enable deep scan (clone repos, regex search, analyse licenses, etc.)
|
||||||
-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)
|
--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)
|
-e, --exclude-repo strings Exclude repos from deep scan (comma-separated list, only for deep scan)
|
||||||
-r, --refresh Refresh the cache (only for deep scan)
|
-r, --refresh Refresh the cache (only for deep scan)
|
||||||
-c, --only-commits Display only commits with author info
|
-s, --show-source Show where the information (authors, emails, etc) were found (only for deep scan)
|
||||||
-s, --silent Suppress all non-essential output
|
-m, --max-distance int Maximum Levenshtein distance for matching usernames & emails (only for deep scan) (default 20)
|
||||||
-j, --json string Write results to specified JSON file
|
--trufflehog Run trufflehog on cloned repositories (only for deep scan) (default true)
|
||||||
|
-S, --silent Suppress all non-essential output
|
||||||
|
--spoof-email Spoof email (only for email mode) (default true)
|
||||||
|
-a, --print-avatar Show the avatar in the output
|
||||||
|
-j, --json string Write results to specified JSON file
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example
|
### Token
|
||||||
|
|
||||||
|
For the best experience, provide a **GitHub Personal Access Token**. Without a
|
||||||
|
token, you will quickly hit the **rate limit** and have to wait.
|
||||||
|
|
||||||
|
- For **basic usage**, you can create a token **without any permissions**.
|
||||||
|
- For the **email spoofing feature**, you need to add the **`repo`** and
|
||||||
|
**`delete_repo`** permissions.
|
||||||
|
|
||||||
|
You can set the token in multiple ways:
|
||||||
|
|
||||||
|
- **Command-line flag**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
github-recon -t "ghp_xxx..."
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Environment variable**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GITHUB_RECON_TOKEN=ghp_xxx...
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Config file**: Create the file `~/.config/github-recon/env` and add:
|
||||||
|
|
||||||
|
```env
|
||||||
|
GITHUB_RECON_TOKEN=ghp_xxx...
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> For safety, it is recommended to create the Personal Access Token on a
|
||||||
|
> **separate GitHub account** rather than your main account. This way, if
|
||||||
|
> anything goes wrong, your primary account remains safe.
|
||||||
|
|
||||||
|
### How does the email spoofing work?
|
||||||
|
|
||||||
|
Here’s the process:
|
||||||
|
|
||||||
|
1. Create a new repository.
|
||||||
|
2. Make a commit using the **target's email** as the author.
|
||||||
|
3. Push the commit to GitHub.
|
||||||
|
4. Observe which GitHub account the commit is linked to. This method **always
|
||||||
|
works**, but it only reveals the account if the email is set as the user’s
|
||||||
|
**primary email**.
|
||||||
|
|
||||||
|
All of these steps are handled **automatically by the tool**, so you just need
|
||||||
|
to provide the target email.
|
||||||
|
|
||||||
|
## 💡 Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gh-recon --username anotherhadi --token ghp_ABC123...
|
github-recon anotherhadi --token ghp_ABC123...
|
||||||
gh-recon --email [email protected]
|
github-recon [email protected] # Find github accounts by email
|
||||||
gh-recon --username anotherhadi --json output.json --deep
|
github-recon anotherhadi --json output.json --deepscan # Clone the repo and search for leaked email
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cover your tracks
|
## 🕵️♂️ Cover your tracks
|
||||||
|
|
||||||
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:
|
Understanding what information about you is publicly visible is the first step
|
||||||
|
to managing your online presence. github-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:
|
||||||
|
|
||||||
- **Review your public profile**: Regularly check your GitHub profile and repositories to ensure that you are not unintentionally exposing sensitive information.
|
- **Review your public profile**: Regularly check your GitHub profile and
|
||||||
- **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.
|
repositories to ensure that you are not unintentionally exposing sensitive
|
||||||
- **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.
|
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, and an
|
||||||
|
[alias email](https://proton.me/support/addresses-and-aliases) for your
|
||||||
|
account. 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.
|
You can also use a tool like [TruffleHog](github.com/trufflesecurity/trufflehog)
|
||||||
|
to scan your repositories specifically for exposed secrets and tokens.
|
||||||
|
|
||||||
**Useful links:**
|
**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)
|
- [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)
|
- [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
|
## 🤝 Contributing
|
||||||
|
|
||||||
Feel free to contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
Feel free to contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
||||||
|
|
||||||
|
## 🙏 Credits
|
||||||
|
|
||||||
|
Some features and ideas in this project were inspired by the following tools:
|
||||||
|
|
||||||
|
- [gitrecon](https://github.com/GONZOsint/gitrecon) by GONZOsint
|
||||||
|
- [gitfive](https://github.com/mxrch/gitfive) by mxrch
|
||||||
|
|
||||||
|
Big thanks to their authors for sharing their work with the community.
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeJson(s github_recon_settings.Settings, data any) {
|
||||||
|
if s.JsonOutput == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file, err := os.Create(s.JsonOutput)
|
||||||
|
if err != nil {
|
||||||
|
s.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 {
|
||||||
|
s.Logger.Error("Failed to write to JSON file", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Logger.Info("JSON output written to file", "file", s.JsonOutput)
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
recon_email "github.com/anotherhadi/github-recon/github-recon/email"
|
||||||
|
recon_username "github.com/anotherhadi/github-recon/github-recon/username"
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/anotherhadi/github-recon/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
settings, err := github_recon_settings.GetSettings()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.Silent {
|
||||||
|
utils.Header()
|
||||||
|
|
||||||
|
utils.PrintStruct(settings, struct {
|
||||||
|
Target string
|
||||||
|
TargetType string
|
||||||
|
}{
|
||||||
|
Target: settings.Target,
|
||||||
|
TargetType: string(settings.TargetType),
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.TargetType == github_recon_settings.TargetUsername {
|
||||||
|
result, err := recon_username.Username(settings)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
writeJson(settings, result)
|
||||||
|
} else {
|
||||||
|
result := recon_email.Email(settings)
|
||||||
|
writeJson(settings, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1746663147,
|
"lastModified": 1758427187,
|
||||||
"narHash": "sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ+TCkTRpRc=",
|
"narHash": "sha256-pHpxZ/IyCwoTQPtFIAG2QaxuSm8jWzrzBGjwQZIttJc=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "dda3dcd3fe03e991015e9a74b22d35950f264a54",
|
"rev": "554be6495561ff07b6c724047bdd7e0716aa7b46",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -1,35 +1,40 @@
|
|||||||
{
|
{
|
||||||
description =
|
description = "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.";
|
||||||
"GH-Recon: Fetches and aggregates public OSINT data for a GitHub user, leveraging Go and the GitHub API.";
|
|
||||||
|
|
||||||
inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; };
|
inputs = {nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";};
|
||||||
|
|
||||||
outputs = { self, nixpkgs }:
|
outputs = {
|
||||||
let
|
self,
|
||||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
|
nixpkgs,
|
||||||
|
}: let
|
||||||
|
supportedSystems = ["x86_64-linux" "aarch64-linux"];
|
||||||
|
|
||||||
forAllSystems = f:
|
forAllSystems = f:
|
||||||
nixpkgs.lib.genAttrs supportedSystems
|
nixpkgs.lib.genAttrs supportedSystems
|
||||||
(system: f system (import nixpkgs { inherit system; }));
|
(system: f system (import nixpkgs {inherit system;}));
|
||||||
|
|
||||||
pname = "gh-recon";
|
pname = "github-recon";
|
||||||
version = "0.2.1";
|
version = "1.5.3";
|
||||||
|
|
||||||
ldflags = [ "-s" "-w" ];
|
|
||||||
|
|
||||||
|
ldflags = ["-s" "-w"];
|
||||||
in {
|
in {
|
||||||
packages = forAllSystems (system: pkgs: {
|
packages = forAllSystems (system: pkgs: {
|
||||||
"${pname}" = pkgs.buildGoModule {
|
"${pname}" = pkgs.buildGoModule {
|
||||||
inherit pname version ldflags;
|
inherit pname version ldflags;
|
||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
subPackages = ["cmd"];
|
||||||
|
outputs = ["out"];
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
cp $GOPATH/bin/cmd $out/bin/github-recon
|
||||||
|
'';
|
||||||
|
|
||||||
vendorHash = "sha256-S8IzmdiVvBtnQQl0AewGZ1yuitvrdnVQ/Jf2230g3Mg=";
|
vendorHash = "sha256-16mRhQyoyY3M98cWBURsEvvwVT6aR3JWk8YfAFNfm/A=";
|
||||||
|
|
||||||
meta = with pkgs.lib; {
|
meta = with pkgs.lib; {
|
||||||
description =
|
description = "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.";
|
||||||
"Fetches and aggregates public OSINT data for a GitHub user.";
|
homepage = "https://github.com/anotherhadi/github-recon";
|
||||||
homepage = "https://github.com/anotherhadi/gh-recon";
|
|
||||||
platforms = platforms.unix;
|
platforms = platforms.unix;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
package github_recon
|
||||||
@@ -1,29 +1,32 @@
|
|||||||
package ghrecon
|
package recon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/anotherhadi/github-recon/utils"
|
||||||
"github.com/google/go-github/v72/github"
|
"github.com/google/go-github/v72/github"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EmailResult struct {
|
type CommitsResult []CommitResult
|
||||||
|
|
||||||
|
type CommitResult struct {
|
||||||
Name string
|
Name string
|
||||||
Email string
|
Email string
|
||||||
Username string
|
Username string
|
||||||
Occurences int
|
Occurrences int
|
||||||
FirstFoundIn string
|
FirstFoundIn string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Recon) Email(email string) (response []EmailResult) {
|
func Commits(s github_recon_settings.Settings) (response CommitsResult) {
|
||||||
r.PrintTitle("✉️ Email")
|
results := make(map[string]CommitResult)
|
||||||
|
|
||||||
results := make(map[string]EmailResult)
|
|
||||||
|
|
||||||
collect := func(date string) error {
|
collect := func(date string) error {
|
||||||
for page := 1; page <= 10; page++ {
|
for page := 1; page <= 10; page++ {
|
||||||
result, resp, err := r.Client.Search.Commits(
|
result, resp, err := s.Client.Search.Commits(
|
||||||
r.Ctx,
|
s.Ctx,
|
||||||
fmt.Sprintf("author-email:%s author-date:%s", email, date),
|
fmt.Sprintf("author-email:%s author-date:%s", s.Target, date),
|
||||||
&github.SearchOptions{
|
&github.SearchOptions{
|
||||||
Sort: "author-date",
|
Sort: "author-date",
|
||||||
Order: "desc",
|
Order: "desc",
|
||||||
@@ -33,7 +36,7 @@ func (r Recon) Email(email string) (response []EmailResult) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("fetch page %d (%s): %w", page, date, err)
|
return fmt.Errorf("fetch page %d (%s): %w", page, date, err)
|
||||||
}
|
}
|
||||||
WaitForRateLimit(resp)
|
utils.WaitForRateLimit(s, resp)
|
||||||
if len(result.Commits) == 0 {
|
if len(result.Commits) == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -44,23 +47,23 @@ func (r Recon) Email(email string) (response []EmailResult) {
|
|||||||
if login == "" {
|
if login == "" {
|
||||||
login = "Unknown"
|
login = "Unknown"
|
||||||
}
|
}
|
||||||
if SkipResult(name, email) {
|
if utils.SkipResult(name, email) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, seen := results[name+" - "+email+" - "+login]; !seen {
|
if _, seen := results[strings.ToLower(name)+" - "+strings.ToLower(email)+" - "+strings.ToLower(login)]; !seen {
|
||||||
author := EmailResult{
|
author := CommitResult{
|
||||||
Name: name,
|
Name: name,
|
||||||
Email: email,
|
Email: email,
|
||||||
Username: login,
|
Username: login,
|
||||||
Occurences: 1,
|
Occurrences: 1,
|
||||||
FirstFoundIn: item.GetRepository().Owner.GetLogin() + "/" + item.GetRepository().
|
FirstFoundIn: item.GetRepository().Owner.GetLogin() + "/" + item.GetRepository().
|
||||||
GetName(),
|
GetName(),
|
||||||
}
|
}
|
||||||
results[name+" - "+email+" - "+login] = author
|
results[strings.ToLower(name)+" - "+strings.ToLower(email)+" - "+strings.ToLower(login)] = author
|
||||||
} else {
|
} else {
|
||||||
result := results[name+" - "+email+" - "+login]
|
result := results[strings.ToLower(name)+" - "+strings.ToLower(email)+" - "+strings.ToLower(login)]
|
||||||
result.Occurences++
|
result.Occurrences++
|
||||||
results[name+" - "+email+" - "+login] = result
|
results[strings.ToLower(name)+" - "+strings.ToLower(email)+" - "+strings.ToLower(login)] = result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,22 +80,13 @@ func (r Recon) Email(email string) (response []EmailResult) {
|
|||||||
">2026-01-01",
|
">2026-01-01",
|
||||||
} {
|
} {
|
||||||
if err := collect(date); err != nil {
|
if err := collect(date); err != nil {
|
||||||
r.Logger.Error("Failed to fetch commits", "err", err, "date", date)
|
s.Logger.Error("Failed to fetch commits", "err", err, "date", date)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, result := range results {
|
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)
|
response = append(response, result)
|
||||||
}
|
}
|
||||||
if len(results) == 0 {
|
|
||||||
r.PrintInfo("INFO", "No commits found")
|
|
||||||
}
|
|
||||||
|
|
||||||
r.PrintNewline()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package recon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/anotherhadi/github-recon/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmailResult struct {
|
||||||
|
DateTime string
|
||||||
|
Target string
|
||||||
|
TargetType github_recon_settings.TargetType
|
||||||
|
|
||||||
|
Commits CommitsResult
|
||||||
|
Spoofing *SpoofingResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func Email(settings github_recon_settings.Settings) EmailResult {
|
||||||
|
result := EmailResult{
|
||||||
|
Target: settings.Target,
|
||||||
|
TargetType: settings.TargetType,
|
||||||
|
DateTime: time.Now().String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.PrintTitle(settings.Silent, "👤 Commits author")
|
||||||
|
result.Commits = Commits(settings)
|
||||||
|
utils.PrintStruct(settings, result.Commits, 0)
|
||||||
|
|
||||||
|
if settings.SpoofEmail {
|
||||||
|
if settings.Token == "null" {
|
||||||
|
settings.Logger.Warn("Skipping email spoofing test, please provide a Github token")
|
||||||
|
} else {
|
||||||
|
utils.PrintTitle(settings.Silent, "🎭 Spoofing test")
|
||||||
|
result.Spoofing = Spoofing(settings)
|
||||||
|
if result.Spoofing != nil && result.Spoofing.AvatarURL != "" {
|
||||||
|
utils.PrintAvatar(settings, result.Spoofing.AvatarURL)
|
||||||
|
}
|
||||||
|
utils.PrintStruct(settings, result.Spoofing, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package recon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/anotherhadi/github-recon/utils"
|
||||||
|
"github.com/google/go-github/v72/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpoofingResult struct {
|
||||||
|
Username string
|
||||||
|
Name string
|
||||||
|
Email string
|
||||||
|
Url string
|
||||||
|
AvatarURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomString(n int) string {
|
||||||
|
letters := []rune("abcdefghijklmnopqrstuvwxyz")
|
||||||
|
|
||||||
|
b := make([]rune, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letters[rand.Intn(len(letters))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Spoofing(s github_recon_settings.Settings) (response *SpoofingResult) {
|
||||||
|
response = &SpoofingResult{}
|
||||||
|
name := "gh-recon-spoofing-" + RandomString(8)
|
||||||
|
private := true
|
||||||
|
autoInit := true
|
||||||
|
repo, resp, err := s.Client.Repositories.Create(s.Ctx, "", &github.Repository{
|
||||||
|
Name: &name,
|
||||||
|
Private: &private,
|
||||||
|
AutoInit: &autoInit,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Error while creating repo", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_, err = s.Client.Repositories.Delete(s.Ctx, repo.Owner.GetLogin(), name)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Error while deleting repo", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
branch := repo.GetDefaultBranch()
|
||||||
|
if branch == "" {
|
||||||
|
branch = "main"
|
||||||
|
}
|
||||||
|
refName := "heads/" + branch
|
||||||
|
|
||||||
|
authorName := "GITHUB-RECON-SPOOFING"
|
||||||
|
authorEmail := s.Target
|
||||||
|
author := &github.CommitAuthor{
|
||||||
|
Name: &authorName,
|
||||||
|
Email: &authorEmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, resp, err := s.Client.Git.GetRef(s.Ctx, repo.Owner.GetLogin(), name, refName)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Error while getting ref", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
|
||||||
|
parentCommit, resp, err := s.Client.Git.GetCommit(s.Ctx, repo.Owner.GetLogin(), name, ref.GetObject().GetSHA())
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Error while getting parent commit", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
|
||||||
|
commitMessage := "Spoofed empty commit"
|
||||||
|
commit := &github.Commit{
|
||||||
|
Author: author,
|
||||||
|
Message: &commitMessage,
|
||||||
|
Tree: &github.Tree{SHA: parentCommit.Tree.SHA},
|
||||||
|
Parents: []*github.Commit{parentCommit},
|
||||||
|
}
|
||||||
|
|
||||||
|
newCommit, resp, err := s.Client.Git.CreateCommit(s.Ctx, repo.Owner.GetLogin(), name, commit, nil)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Error while creating spoofed empty commit", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
|
||||||
|
ref.Object.SHA = newCommit.SHA
|
||||||
|
_, resp, err = s.Client.Git.UpdateRef(s.Ctx, repo.Owner.GetLogin(), name, ref, false)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Error while updating ref to spoofed commit", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
|
||||||
|
const maxRetries = 5
|
||||||
|
const retryDelay = 2 * time.Second
|
||||||
|
for i := 0; i < maxRetries; i++ {
|
||||||
|
commits, _, err := s.Client.Repositories.ListCommits(s.Ctx, repo.Owner.GetLogin(), name, nil)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Error while listing commits", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(commits) > 1 {
|
||||||
|
last := commits[0]
|
||||||
|
response.Username = last.GetAuthor().GetLogin()
|
||||||
|
response.Name = last.GetAuthor().GetName()
|
||||||
|
response.Email = last.GetAuthor().GetEmail()
|
||||||
|
response.Url = last.GetAuthor().GetHTMLURL()
|
||||||
|
response.AvatarURL = last.GetAuthor().GetAvatarURL()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Logger.Info("Only one commit found, retrying...", "attempt", i+1)
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Username == "" && response.Name == "" && response.Email == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package recon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/anotherhadi/github-recon/utils"
|
||||||
|
"github.com/google/go-github/v72/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloseFriendsResult []CloseFriendResult
|
||||||
|
|
||||||
|
type CloseFriendResult struct {
|
||||||
|
Name string
|
||||||
|
Username string
|
||||||
|
Orgs []string
|
||||||
|
Company string
|
||||||
|
Location string
|
||||||
|
Score int
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxTargetFollowing = 50
|
||||||
|
maxFollowersForCandidate = 20
|
||||||
|
pointsPerCondition = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// CloseFriends returns a list of "close friends" for the target user.
|
||||||
|
// A candidate is considered closer if:
|
||||||
|
// 1. The target follows fewer than 50 people.
|
||||||
|
// 2. The candidate has fewer than 20 followers (+1 point).
|
||||||
|
// 3. The candidate follows the target back (+1 point).
|
||||||
|
// 4. The candidate shares at least one organization with the target (+1 point).
|
||||||
|
func CloseFriends(s github_recon_settings.Settings) (results CloseFriendsResult) {
|
||||||
|
targetFollowing, resp, err := s.Client.Users.ListFollowing(s.Ctx, s.Target, &github.ListOptions{PerPage: 100})
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Failed to fetch target's following list", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
|
||||||
|
targetOrgs, err := getOrgs(s, s.Target)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Failed to fetch target's organizations", "err", err)
|
||||||
|
targetOrgs = []*github.Organization{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(targetFollowing) > maxTargetFollowing {
|
||||||
|
s.Logger.Info("Skipping close friends check",
|
||||||
|
"reason",
|
||||||
|
fmt.Sprintf("Target follows %d or more users (limit: %d)", len(targetFollowing), maxTargetFollowing),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(targetFollowing) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, candidate := range targetFollowing {
|
||||||
|
candidateLogin := candidate.GetLogin()
|
||||||
|
if candidateLogin == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
score := 0
|
||||||
|
|
||||||
|
candidateDetails, userResp, err := s.Client.Users.Get(s.Ctx, candidateLogin)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Warn("Failed to fetch details for candidate",
|
||||||
|
"candidate", candidateLogin,
|
||||||
|
"err", err,
|
||||||
|
)
|
||||||
|
if userResp != nil {
|
||||||
|
utils.WaitForRateLimit(s, userResp)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
utils.WaitForRateLimit(s, userResp)
|
||||||
|
|
||||||
|
// Condition: candidate has few followers
|
||||||
|
if candidateDetails.GetFollowers() < maxFollowersForCandidate {
|
||||||
|
score += pointsPerCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition: candidate follows target back
|
||||||
|
followsBack, err := checkIfUserFollows(s, candidateLogin, s.Target)
|
||||||
|
if err == nil && followsBack {
|
||||||
|
score += pointsPerCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition: same organization
|
||||||
|
candidateOrgs, _ := getOrgs(s, candidateLogin)
|
||||||
|
if isInSameOrg(targetOrgs, candidateOrgs) {
|
||||||
|
score += pointsPerCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add candidate if they matched at least one condition
|
||||||
|
if score > 0 {
|
||||||
|
results = append(results, CloseFriendResult{
|
||||||
|
Name: candidateDetails.GetName(),
|
||||||
|
Username: candidateLogin,
|
||||||
|
Orgs: candidateOrgsToNames(candidateOrgs),
|
||||||
|
Score: score,
|
||||||
|
Location: candidateDetails.GetLocation(),
|
||||||
|
Company: candidateDetails.GetCompany(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) > 0 {
|
||||||
|
sort.Slice(results, func(i, j int) bool {
|
||||||
|
return results[i].Score > results[j].Score
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkIfUserFollows checks if sourceUser follows targetUser.
|
||||||
|
func checkIfUserFollows(s github_recon_settings.Settings, sourceUser, targetUser string) (bool, error) {
|
||||||
|
isFollowing, resp, err := s.Client.Users.IsFollowing(s.Ctx, sourceUser, targetUser)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Warn("Error checking if user follows target",
|
||||||
|
"source", sourceUser,
|
||||||
|
"target", targetUser,
|
||||||
|
"err", err,
|
||||||
|
)
|
||||||
|
if resp != nil {
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if resp != nil {
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
}
|
||||||
|
return isFollowing, nil
|
||||||
|
}
|
||||||
|
func candidateOrgsToNames(orgs []*github.Organization) []string {
|
||||||
|
var orgNames []string
|
||||||
|
for _, org := range orgs {
|
||||||
|
if org.GetLogin() != "" {
|
||||||
|
orgNames = append(orgNames, org.GetLogin())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return orgNames
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOrgs(s github_recon_settings.Settings, user string) ([]*github.Organization, error) {
|
||||||
|
orgs, resp, err := s.Client.Organizations.List(s.Ctx, user, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("failed to fetch organizations for user")
|
||||||
|
}
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
return orgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInSameOrg(orgsA, orgsB []*github.Organization) bool {
|
||||||
|
for _, orgA := range orgsA {
|
||||||
|
for _, orgB := range orgsB {
|
||||||
|
if orgA.GetLogin() == orgB.GetLogin() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package recon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/anotherhadi/github-recon/utils"
|
||||||
|
"github.com/google/go-github/v72/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommitsResult []CommitResult
|
||||||
|
|
||||||
|
type CommitResult struct {
|
||||||
|
Name string
|
||||||
|
Email string
|
||||||
|
Occurrences int
|
||||||
|
FirstFoundIn string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Commits(s github_recon_settings.Settings) (response CommitsResult) {
|
||||||
|
results := make(map[string]CommitResult)
|
||||||
|
|
||||||
|
collect := func(date string) error {
|
||||||
|
for page := 1; page <= 10; page++ {
|
||||||
|
result, resp, err := s.Client.Search.Commits(
|
||||||
|
s.Ctx,
|
||||||
|
fmt.Sprintf("author:%s author-date:%s", s.Target, 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)
|
||||||
|
}
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
if len(result.Commits) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, item := range result.Commits {
|
||||||
|
emails := []string{
|
||||||
|
item.Commit.GetAuthor().GetEmail(),
|
||||||
|
item.Commit.GetCommitter().GetEmail(),
|
||||||
|
item.GetAuthor().GetEmail(),
|
||||||
|
item.GetCommitter().GetEmail(),
|
||||||
|
}
|
||||||
|
|
||||||
|
names := []string{
|
||||||
|
item.Commit.GetAuthor().GetName(),
|
||||||
|
item.Commit.GetCommitter().GetName(),
|
||||||
|
item.GetAuthor().GetName(),
|
||||||
|
item.GetCommitter().GetName(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range names {
|
||||||
|
name := names[i]
|
||||||
|
email := emails[i]
|
||||||
|
if utils.SkipResult(name, email) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if name == "" || email == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, seen := results[strings.ToLower(name)+" - "+strings.ToLower(email)]; !seen {
|
||||||
|
author := CommitResult{
|
||||||
|
Name: name,
|
||||||
|
Email: email,
|
||||||
|
Occurrences: 1,
|
||||||
|
FirstFoundIn: item.GetRepository().Owner.GetLogin() + "/" + item.GetRepository().
|
||||||
|
GetName(),
|
||||||
|
}
|
||||||
|
results[strings.ToLower(name)+" - "+strings.ToLower(email)] = author
|
||||||
|
} else if i == 0 {
|
||||||
|
result := results[strings.ToLower(name)+" - "+strings.ToLower(email)]
|
||||||
|
result.Occurrences++
|
||||||
|
results[strings.ToLower(name)+" - "+strings.ToLower(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 {
|
||||||
|
s.Logger.Error("Failed to fetch commits", "err", err, "date", date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range results {
|
||||||
|
response = append(response, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
package recon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/anotherhadi/github-recon/utils"
|
||||||
|
"github.com/google/go-github/v72/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Authors []Author
|
||||||
|
|
||||||
|
type Author struct {
|
||||||
|
Name string
|
||||||
|
Levenshtein int
|
||||||
|
Email string
|
||||||
|
FoundIn []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Emails []Email
|
||||||
|
|
||||||
|
type Email struct {
|
||||||
|
Email string
|
||||||
|
Levenshtein int
|
||||||
|
FoundIn []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Secrets []Secret
|
||||||
|
|
||||||
|
type Secret struct {
|
||||||
|
Repositorie string
|
||||||
|
Raw map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeepScanResult struct {
|
||||||
|
Authors Authors
|
||||||
|
Emails Emails
|
||||||
|
Secrets Secrets
|
||||||
|
}
|
||||||
|
|
||||||
|
type Repositorie struct {
|
||||||
|
Repository string
|
||||||
|
Owner string
|
||||||
|
Name string
|
||||||
|
Size int
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeepScan(s github_recon_settings.Settings) (response DeepScanResult) {
|
||||||
|
repositories := []Repositorie{}
|
||||||
|
repos, resp, err := s.Client.Repositories.ListByUser(
|
||||||
|
s.Ctx,
|
||||||
|
s.Target,
|
||||||
|
&github.RepositoryListByUserOptions{
|
||||||
|
Type: "all",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Failed to fetch repositories", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, repo := range repos {
|
||||||
|
if slices.Contains(s.ExcludedRepos, repo.GetName()) ||
|
||||||
|
slices.Contains(s.ExcludedRepos, repo.GetOwner().GetLogin()+"/"+repo.GetName()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
maxRepoSize := s.MaxRepoSize * 1024
|
||||||
|
|
||||||
|
if repo.GetSize() > maxRepoSize {
|
||||||
|
s.Logger.Info("Skipping repository due to size", "repo", repo.GetOwner().GetLogin()+"/"+repo.GetName(), "size_MB", repo.GetSize()/1024, "max_size_MB", maxRepoSize/1024)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories = append(repositories, Repositorie{
|
||||||
|
Repository: repo.GetHTMLURL(),
|
||||||
|
Owner: repo.GetOwner().GetLogin(),
|
||||||
|
Name: repo.GetName(),
|
||||||
|
Size: repo.GetSize(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
|
||||||
|
cmd := exec.Command("git", "--version")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
s.Logger.Error("Git is not installed", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp_folder := "/tmp/ghrecon-" + s.Target
|
||||||
|
|
||||||
|
if utils.DoesFolderExists(tmp_folder) {
|
||||||
|
if s.Refresh {
|
||||||
|
s.Logger.Info("Deleting existing folder", "path", tmp_folder)
|
||||||
|
err := os.RemoveAll(tmp_folder)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Failed to delete existing folder", "path", tmp_folder, "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, repo := range repositories {
|
||||||
|
destination := tmp_folder + "/" + repo.Owner + "/" + repo.Name
|
||||||
|
if utils.DoesFolderExists(destination) {
|
||||||
|
s.Logger.Info("Directory already downloaded, skipping", "repo", repo.Owner+"/"+repo.Name, "path", destination)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Logger.Info("Cloning repository", "repo", repo.Owner+"/"+repo.Name, "path", destination, "size_MB", repo.Size/1024)
|
||||||
|
|
||||||
|
cmd := exec.Command(
|
||||||
|
"git",
|
||||||
|
"clone",
|
||||||
|
repo.Repository,
|
||||||
|
destination,
|
||||||
|
)
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error(
|
||||||
|
"ERROR",
|
||||||
|
"Failed to clone repository",
|
||||||
|
"err",
|
||||||
|
err,
|
||||||
|
"repo",
|
||||||
|
repo.Repository,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Logger.Info("Cloned all repositories", "path", tmp_folder)
|
||||||
|
|
||||||
|
if len(repositories) == 0 {
|
||||||
|
s.Logger.Info("No repositories found for the user, skipping deep scan.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorOccurrences := Authors{}
|
||||||
|
mapAuthorToIndex := make(map[string]int)
|
||||||
|
for _, repo := range repositories {
|
||||||
|
destination := tmp_folder + "/" + repo.Owner + "/" + repo.Name
|
||||||
|
if !utils.DoesFolderExists(filepath.Join(destination, ".git")) {
|
||||||
|
s.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 {
|
||||||
|
s.Logger.Error("Failed to execute git log (ExitError)", "repo", repo.Owner+"/"+repo.Name, "stderr", string(exitErr.Stderr), "err", logErr)
|
||||||
|
} else {
|
||||||
|
s.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 {
|
||||||
|
s.Logger.Error("Malformed author line from git log", "line", trimmedLine, "repo", repoIdentifier)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
authorOccurrences = append(authorOccurrences, Author{
|
||||||
|
Name: authorName,
|
||||||
|
Email: authorEmail,
|
||||||
|
FoundIn: []string{repoIdentifier},
|
||||||
|
Levenshtein: levenshteinDistanceAuthor(s.Target, authorName, authorEmail),
|
||||||
|
})
|
||||||
|
mapAuthorToIndex[trimmedLine] = len(authorOccurrences) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.SortFunc(authorOccurrences, func(a, b Author) int {
|
||||||
|
if a.Levenshtein != b.Levenshtein {
|
||||||
|
return a.Levenshtein - b.Levenshtein
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
|
||||||
|
authors := Authors{}
|
||||||
|
for _, author := range authorOccurrences {
|
||||||
|
if author.Levenshtein > s.MaxDistance {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if utils.SkipResult(author.Name, author.Email) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
authors = append(authors, author)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Logger.Info("Searching for emails in cloned repositories", "path", tmp_folder)
|
||||||
|
emailsFound, err := findEmailsAndOccurrencesInDir(tmp_folder, s.Target)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Failed to find emails in directory", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slices.SortFunc(emailsFound, func(a, b Email) int {
|
||||||
|
if a.Levenshtein != b.Levenshtein {
|
||||||
|
return a.Levenshtein - b.Levenshtein
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
|
||||||
|
emails := Emails{}
|
||||||
|
for _, email := range emailsFound {
|
||||||
|
if email.Levenshtein > s.MaxDistance {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
emails = append(emails, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Authors = authors
|
||||||
|
response.Emails = emails
|
||||||
|
|
||||||
|
s.Logger.Info("Searching for secrets in cloned repositories", "path", tmp_folder)
|
||||||
|
if s.Trufflehog {
|
||||||
|
cmd := exec.Command("trufflehog", "--version")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
s.Logger.Warn("Trufflehog is not installed, skipping secret scanning.")
|
||||||
|
} else {
|
||||||
|
secrets, err := truffleHog(tmp_folder)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Failed to run trufflehog", "err", err)
|
||||||
|
} else {
|
||||||
|
response.Secrets = secrets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func truffleHog(tmpFolder string) (Secrets, error) {
|
||||||
|
allSecrets := Secrets{}
|
||||||
|
|
||||||
|
directories, err := os.ReadDir(tmpFolder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read tmp folder: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range directories {
|
||||||
|
if !dir.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
innerPath := filepath.Join(tmpFolder, dir.Name())
|
||||||
|
innerDirectories, err := os.ReadDir(innerPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read inner tmp folder: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, innerDir := range innerDirectories {
|
||||||
|
if !innerDir.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
repoPath := filepath.Join(innerPath, innerDir.Name())
|
||||||
|
cmd := exec.Command("trufflehog", "git", "file://"+repoPath, "--json", "--log-level=-1", "--results=verified")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
if exitErr.ExitCode() > 1 {
|
||||||
|
return nil, fmt.Errorf("failed to execute trufflehog (ExitError): %s", string(exitErr.Stderr))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("failed to execute trufflehog: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(strings.NewReader(string(output)))
|
||||||
|
for decoder.More() {
|
||||||
|
var result map[string]any
|
||||||
|
if err := decoder.Decode(&result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse trufflehog output: %w", err)
|
||||||
|
}
|
||||||
|
allSecrets = append(allSecrets, Secret{
|
||||||
|
Repositorie: dir.Name() + "/" + innerDir.Name(),
|
||||||
|
Raw: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allSecrets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findEmailsAndOccurrencesInDir(rootPath string, username string) (Emails, 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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if d.Type().IsRegular() {
|
||||||
|
if strings.Contains(path, ".git/logs/") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentFileEmails := emailRegex.FindAllString(string(content), -1)
|
||||||
|
if len(currentFileEmails) > 0 {
|
||||||
|
relativePath, errRel := filepath.Rel(normalizedRootPath, path)
|
||||||
|
if errRel != nil {
|
||||||
|
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 Emails
|
||||||
|
for email, pathSet := range emailLocations {
|
||||||
|
var paths []string
|
||||||
|
for path := range pathSet {
|
||||||
|
paths = append(paths, path)
|
||||||
|
}
|
||||||
|
results = append(results, Email{
|
||||||
|
Email: email, FoundIn: paths,
|
||||||
|
Levenshtein: utils.LevenshteinDistance(username, strings.SplitN(email, "@", 2)[0]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func levenshteinDistanceAuthor(target, name, email string) int {
|
||||||
|
if strings.Contains(email, "@") {
|
||||||
|
email = strings.SplitN(email, "@", 2)[0]
|
||||||
|
}
|
||||||
|
return slices.Min([]int{utils.LevenshteinDistance(target, name), utils.LevenshteinDistance(target, email)})
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package recon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/anotherhadi/github-recon/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SshKeysResult []SshKeyResult
|
||||||
|
|
||||||
|
type SshKeyResult struct {
|
||||||
|
Url string
|
||||||
|
Title string
|
||||||
|
CreatedAt string
|
||||||
|
Key string
|
||||||
|
ReadOnly string
|
||||||
|
Verified string
|
||||||
|
LastUsed string
|
||||||
|
AddedBy string
|
||||||
|
}
|
||||||
|
|
||||||
|
func SshKeys(s github_recon_settings.Settings) (response SshKeysResult) {
|
||||||
|
sshKeys, resp, err := s.Client.Users.ListKeys(s.Ctx, s.Target, nil)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Failed to fetch ssh keys", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range sshKeys {
|
||||||
|
k := SshKeyResult{
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type GpgKeyEmail struct {
|
||||||
|
Email string
|
||||||
|
Verified string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GpgKeysResult []GpgKeyResult
|
||||||
|
|
||||||
|
type GpgKeyResult struct {
|
||||||
|
KeyID string
|
||||||
|
PublicKey string
|
||||||
|
CreatedAt string
|
||||||
|
PrimaryKeyID string
|
||||||
|
RawKey string
|
||||||
|
Emails []GpgKeyEmail
|
||||||
|
Subkeys []GpgKeyResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func GpgKeys(s github_recon_settings.Settings) (response GpgKeysResult) {
|
||||||
|
gpgKeys, resp, err := s.Client.Users.ListGPGKeys(s.Ctx, s.Target, nil)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Failed to fetch user's gpg keys", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range gpgKeys {
|
||||||
|
k := GpgKeyResult{
|
||||||
|
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{
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type SshSigningKeysResult []SshSigningKeyResult
|
||||||
|
|
||||||
|
type SshSigningKeyResult struct {
|
||||||
|
Title string
|
||||||
|
CreatedAt string
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func SshSigningKeys(s github_recon_settings.Settings) (response SshSigningKeysResult) {
|
||||||
|
signingKeys, resp, err := s.Client.Users.ListSSHSigningKeys(
|
||||||
|
s.Ctx,
|
||||||
|
s.Target,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Failed to fetch user's ssh signing keys", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range signingKeys {
|
||||||
|
k := SshSigningKeyResult{
|
||||||
|
Title: key.GetTitle(),
|
||||||
|
CreatedAt: key.GetCreatedAt().String(),
|
||||||
|
Key: key.GetKey(),
|
||||||
|
}
|
||||||
|
response = append(response, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package recon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/anotherhadi/github-recon/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UsernameResult struct {
|
||||||
|
DateTime string // Now
|
||||||
|
Target string
|
||||||
|
TargetType github_recon_settings.TargetType
|
||||||
|
|
||||||
|
User UserResult
|
||||||
|
Socials SocialsResult
|
||||||
|
Orgs OrgsResult
|
||||||
|
|
||||||
|
SshKeys SshKeysResult
|
||||||
|
SshSigningKeys SshSigningKeysResult
|
||||||
|
GpgKeys GpgKeysResult
|
||||||
|
|
||||||
|
CloseFriends CloseFriendsResult
|
||||||
|
|
||||||
|
Commits CommitsResult
|
||||||
|
|
||||||
|
DeepScan DeepScanResult
|
||||||
|
}
|
||||||
|
|
||||||
|
func Username(settings github_recon_settings.Settings) (result UsernameResult, err error) {
|
||||||
|
result = UsernameResult{
|
||||||
|
Target: settings.Target,
|
||||||
|
TargetType: settings.TargetType,
|
||||||
|
DateTime: time.Now().String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.PrintTitle(settings.Silent, "👤 User informations")
|
||||||
|
result.User, err = User(settings)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.User == (UserResult{}) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.PrintAvatar(settings, result.User.AvatarURL)
|
||||||
|
utils.PrintStruct(settings, result.User, 0)
|
||||||
|
|
||||||
|
utils.PrintTitle(settings.Silent, "🐥 Socials")
|
||||||
|
result.Socials = Socials(settings)
|
||||||
|
utils.PrintStruct(settings, result.Socials, 0)
|
||||||
|
|
||||||
|
utils.PrintTitle(settings.Silent, "🏢 Organizations")
|
||||||
|
result.Orgs = Orgs(settings)
|
||||||
|
utils.PrintStruct(settings, result.Orgs, 0)
|
||||||
|
|
||||||
|
utils.PrintTitle(settings.Silent, "🔑 SSH Keys")
|
||||||
|
result.SshKeys = SshKeys(settings)
|
||||||
|
utils.PrintStruct(settings, result.SshKeys, 0)
|
||||||
|
|
||||||
|
utils.PrintTitle(settings.Silent, "🖋️ SSH Signing Keys")
|
||||||
|
result.SshSigningKeys = SshSigningKeys(settings)
|
||||||
|
utils.PrintStruct(settings, result.SshSigningKeys, 0)
|
||||||
|
|
||||||
|
utils.PrintTitle(settings.Silent, "🔐 GPG Keys")
|
||||||
|
result.GpgKeys = GpgKeys(settings)
|
||||||
|
utils.PrintStruct(settings, result.GpgKeys, 0)
|
||||||
|
|
||||||
|
utils.PrintTitle(settings.Silent, "🤝 Close Friends")
|
||||||
|
result.CloseFriends = CloseFriends(settings)
|
||||||
|
utils.PrintStruct(settings, result.CloseFriends, 0)
|
||||||
|
|
||||||
|
utils.PrintTitle(settings.Silent, "📝 Commits")
|
||||||
|
result.Commits = Commits(settings)
|
||||||
|
utils.PrintStruct(settings, result.Commits, 0)
|
||||||
|
|
||||||
|
if settings.DeepScan {
|
||||||
|
utils.PrintTitle(settings.Silent, "🔍 Deep Scan")
|
||||||
|
result.DeepScan = DeepScan(settings)
|
||||||
|
utils.PrintStruct(settings, result.DeepScan, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package recon
|
||||||
|
|
||||||
|
import (
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/anotherhadi/github-recon/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrgsResult []OrgResult
|
||||||
|
|
||||||
|
type OrgResult struct {
|
||||||
|
Name string
|
||||||
|
URL string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Orgs(s github_recon_settings.Settings) (response OrgsResult) {
|
||||||
|
orgs, resp, err := s.Client.Organizations.List(s.Ctx, s.Target, nil)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Failed to fetch organizations", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, org := range orgs {
|
||||||
|
o := OrgResult{
|
||||||
|
Name: org.GetLogin(),
|
||||||
|
URL: org.GetHTMLURL(),
|
||||||
|
Description: org.GetDescription(),
|
||||||
|
}
|
||||||
|
response = append(response, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.WaitForRateLimit(s, resp)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package recon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/anotherhadi/github-recon/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type socialResultInput struct {
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SocialsResult []socialResult
|
||||||
|
|
||||||
|
type socialResult struct {
|
||||||
|
Provider string
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Socials(s github_recon_settings.Settings) (response SocialsResult) {
|
||||||
|
resp, err := utils.FetchGitHubAPI(s.Client, "", "/users/"+s.Target+"/social_accounts")
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Failed to fetch socials", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var socialAccounts []socialResultInput
|
||||||
|
err = json.Unmarshal(resp, &socialAccounts)
|
||||||
|
if err != nil {
|
||||||
|
s.Logger.Error("Failed to unmarshal socials", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
socials := []socialResult{}
|
||||||
|
for _, account := range socialAccounts {
|
||||||
|
socials = append(socials, socialResult{
|
||||||
|
URL: account.URL,
|
||||||
|
Provider: account.Provider,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return socials
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
package ghrecon
|
package recon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/anotherhadi/github-recon/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserResult struct {
|
type UserResult struct {
|
||||||
Username string
|
Username string
|
||||||
ID string
|
|
||||||
AvatarURL string
|
AvatarURL string
|
||||||
GravatarID string
|
GravatarID string
|
||||||
Name string
|
Name string
|
||||||
@@ -29,19 +31,17 @@ type UserResult struct {
|
|||||||
Plan string
|
Plan string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Recon) User(username string) (response UserResult) {
|
func User(s github_recon_settings.Settings) (response UserResult, err error) {
|
||||||
user, resp, err := r.Client.Users.Get(r.Ctx, username)
|
user, resp, err := s.Client.Users.Get(s.Ctx, s.Target)
|
||||||
if resp.StatusCode == 404 {
|
if resp.StatusCode == 404 {
|
||||||
r.Logger.Fatal("User not found")
|
return UserResult{}, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Logger.Fatal("Failed to fetch user's information", "err", err)
|
return UserResult{}, fmt.Errorf("failed to fetch user's information")
|
||||||
}
|
}
|
||||||
|
|
||||||
r.PrintTitle("👤 User informations")
|
|
||||||
u := UserResult{
|
u := UserResult{
|
||||||
Username: user.GetLogin(),
|
Username: user.GetLogin(),
|
||||||
ID: fmt.Sprintf("%d", user.GetID()),
|
|
||||||
AvatarURL: user.GetAvatarURL(),
|
AvatarURL: user.GetAvatarURL(),
|
||||||
GravatarID: user.GetGravatarID(),
|
GravatarID: user.GetGravatarID(),
|
||||||
Name: user.GetName(),
|
Name: user.GetName(),
|
||||||
@@ -63,30 +63,7 @@ func (r Recon) User(username string) (response UserResult) {
|
|||||||
Collaborators: fmt.Sprintf("%d", user.GetCollaborators()),
|
Collaborators: fmt.Sprintf("%d", user.GetCollaborators()),
|
||||||
Plan: user.GetPlan().GetName(),
|
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)
|
utils.WaitForRateLimit(s, resp)
|
||||||
return u
|
return u, nil
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
module github.com/anotherhadi/gh-recon
|
module github.com/anotherhadi/github-recon
|
||||||
|
|
||||||
go 1.24.2
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
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.2
|
||||||
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
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/saran13raj/go-pixels v0.0.0-20250629121333-58b240a3ae51
|
||||||
|
github.com/spf13/pflag v1.0.7
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -24,5 +26,6 @@ require (
|
|||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||||
|
golang.org/x/image v0.38.0 // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4p
|
|||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk=
|
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I=
|
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
@@ -23,6 +23,8 @@ github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi65
|
|||||||
github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg=
|
github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg=
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
@@ -36,14 +38,18 @@ 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/saran13raj/go-pixels v0.0.0-20250629121333-58b240a3ae51 h1:H/XUfYcLxI3CBmDlgBpnOeTntRgqWvIoUXnqhCF5a0s=
|
||||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/saran13raj/go-pixels v0.0.0-20250629121333-58b240a3ae51/go.mod h1:sqhdZVLvqzTEBtmZBuTnFDUW0Lsryw2X2/wrLgqLEYg=
|
||||||
|
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||||
|
github.com/spf13/pflag v1.0.7/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=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
|
||||||
|
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"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
|
|
||||||
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()
|
|
||||||
|
|
||||||
// INITIALIZE RECON OBJECT
|
|
||||||
|
|
||||||
r := &ghrecon.Recon{
|
|
||||||
Client: github.NewClient(nil),
|
|
||||||
Logger: log.NewWithOptions(os.Stderr, log.Options{
|
|
||||||
ReportCaller: false,
|
|
||||||
ReportTimestamp: false,
|
|
||||||
}),
|
|
||||||
Ctx: context.Background(),
|
|
||||||
Silent: silent,
|
|
||||||
JsonFile: jsonFile,
|
|
||||||
MaxRepoSize: maxRepoSize,
|
|
||||||
}
|
|
||||||
|
|
||||||
// CHECK FLAGS
|
|
||||||
|
|
||||||
if username == "" && fromEmail == "" {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if token == "" {
|
|
||||||
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 {
|
|
||||||
r.Client = r.Client.WithAuthToken(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// START
|
|
||||||
|
|
||||||
r.Header()
|
|
||||||
|
|
||||||
if fromEmail != "" {
|
|
||||||
emailsInfo := r.Email(fromEmail)
|
|
||||||
r.WriteJson(
|
|
||||||
map[string]any{
|
|
||||||
"Authors": emailsInfo,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if onlyCommitsLeak {
|
|
||||||
commitsInfo := r.Commits(username)
|
|
||||||
r.WriteJson(
|
|
||||||
map[string]any{
|
|
||||||
"Authors": commitsInfo,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userInfo := r.User(username)
|
|
||||||
orgsInfo := r.Orgs(username)
|
|
||||||
sshKeysInfo := r.SshKeys(username)
|
|
||||||
gpgKeysInfo := r.GpgKeys(username)
|
|
||||||
sshSigningKeysInfo := r.SshSigningKeys(username)
|
|
||||||
socialsInfo := r.Socials(username)
|
|
||||||
closeFriendsInfo := r.CloseFriends(username)
|
|
||||||
commitsInfo := r.Commits(username)
|
|
||||||
|
|
||||||
results := map[string]any{
|
|
||||||
"User": userInfo,
|
|
||||||
"Orgs": orgsInfo,
|
|
||||||
"SSHKeys": sshKeysInfo,
|
|
||||||
"GPGKeys": gpgKeysInfo,
|
|
||||||
"SSHSigningKeys": sshSigningKeysInfo,
|
|
||||||
"Socials": socialsInfo,
|
|
||||||
"Commits": commitsInfo,
|
|
||||||
"CloseFriends": closeFriendsInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
if deep {
|
|
||||||
results["Deep"] = r.Deep(username, excludeRepos, refresh)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.WriteJson(results)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package github_recon_settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/google/go-github/v72/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TargetType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TargetUsername TargetType = "Username"
|
||||||
|
TargetEmail TargetType = "Email"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
Token string
|
||||||
|
Target string
|
||||||
|
TargetType TargetType
|
||||||
|
ShowSource bool
|
||||||
|
Refresh bool
|
||||||
|
MaxRepoSize int
|
||||||
|
ExcludedRepos []string
|
||||||
|
JsonOutput string
|
||||||
|
Silent bool
|
||||||
|
DeepScan bool
|
||||||
|
MaxDistance int
|
||||||
|
PrintAvatar bool
|
||||||
|
SpoofEmail bool
|
||||||
|
Trufflehog bool
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
Client *github.Client
|
||||||
|
Logger *log.Logger
|
||||||
|
Ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDefaultSettings() Settings {
|
||||||
|
return Settings{
|
||||||
|
Token: "null",
|
||||||
|
Target: "",
|
||||||
|
TargetType: TargetUsername,
|
||||||
|
ShowSource: false,
|
||||||
|
Refresh: false,
|
||||||
|
MaxRepoSize: 150,
|
||||||
|
ExcludedRepos: []string{},
|
||||||
|
JsonOutput: "",
|
||||||
|
Silent: false,
|
||||||
|
DeepScan: false,
|
||||||
|
MaxDistance: 20,
|
||||||
|
PrintAvatar: true,
|
||||||
|
SpoofEmail: true,
|
||||||
|
Trufflehog: true,
|
||||||
|
|
||||||
|
Client: github.NewClient(nil),
|
||||||
|
Logger: log.NewWithOptions(os.Stderr, log.Options{
|
||||||
|
ReportCaller: false,
|
||||||
|
ReportTimestamp: false,
|
||||||
|
}),
|
||||||
|
Ctx: context.WithValue(context.Background(), github.SleepUntilPrimaryRateLimitResetWhenRateLimited, true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSettings() (settings Settings, err error) {
|
||||||
|
settings = GetDefaultSettings()
|
||||||
|
//// Flag settings
|
||||||
|
flag.Usage = func() {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage:\n", os.Args[0])
|
||||||
|
fmt.Fprintf(os.Stderr, "github-recon [flags] <target username or email>\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "Flags:\n")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.CommandLine.SetNormalizeFunc(wordSepNormalizeFunc)
|
||||||
|
flag.CommandLine.SortFlags = false
|
||||||
|
|
||||||
|
//// Flags
|
||||||
|
flag.StringVarP(&settings.Token, "token", "t", settings.Token, "Github personal access token (e.g. ghp_aaa...). Can also be set via GITHUB_RECON_TOKEN environment variable. You also need to set the token in $HOME/.config/github-recon/env file if you want to use this tool without passing the token every time.")
|
||||||
|
|
||||||
|
// DeepScan
|
||||||
|
flag.BoolVarP(&settings.DeepScan, "deepscan", "d", settings.DeepScan, "Enable deep scan (clone repos, regex search, analyse licenses, etc.)")
|
||||||
|
flag.IntVar(
|
||||||
|
&settings.MaxRepoSize,
|
||||||
|
"max-size",
|
||||||
|
settings.MaxRepoSize,
|
||||||
|
"Limit the size of repositories to scan (in MB) (only for deep scan)",
|
||||||
|
)
|
||||||
|
flag.StringSliceVarP(
|
||||||
|
&settings.ExcludedRepos,
|
||||||
|
"exclude-repo",
|
||||||
|
"e",
|
||||||
|
settings.ExcludedRepos,
|
||||||
|
"Exclude repos from deep scan (comma-separated list, only for deep scan)",
|
||||||
|
)
|
||||||
|
flag.BoolVarP(
|
||||||
|
&settings.Refresh,
|
||||||
|
"refresh",
|
||||||
|
"r",
|
||||||
|
settings.Refresh,
|
||||||
|
"Refresh the cache (only for deep scan)",
|
||||||
|
)
|
||||||
|
flag.BoolVarP(
|
||||||
|
&settings.ShowSource,
|
||||||
|
"show-source",
|
||||||
|
"s",
|
||||||
|
settings.ShowSource,
|
||||||
|
"Show where the information (authors, emails, etc) were found (only for deep scan)",
|
||||||
|
)
|
||||||
|
flag.IntVarP(
|
||||||
|
&settings.MaxDistance,
|
||||||
|
"max-distance",
|
||||||
|
"m",
|
||||||
|
settings.MaxDistance,
|
||||||
|
"Maximum Levenshtein distance for matching usernames & emails (only for deep scan)",
|
||||||
|
)
|
||||||
|
flag.BoolVar(
|
||||||
|
&settings.Trufflehog,
|
||||||
|
"trufflehog",
|
||||||
|
settings.Trufflehog,
|
||||||
|
"Run trufflehog on cloned repositories (only for deep scan)",
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.BoolVarP(&settings.Silent, "silent", "S", settings.Silent, "Suppress all non-essential output")
|
||||||
|
flag.BoolVarP(&settings.SpoofEmail, "spoof-email", "", settings.SpoofEmail, "Spoof email (only for email mode)")
|
||||||
|
flag.BoolVarP(&settings.PrintAvatar, "print-avatar", "a", settings.PrintAvatar, "Show the avatar in the output")
|
||||||
|
flag.StringVarP(&settings.JsonOutput, "json", "j", settings.JsonOutput, "Write results to specified JSON file")
|
||||||
|
|
||||||
|
//// Parse
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
//// Tail
|
||||||
|
nonFlagArgs := flag.Args()
|
||||||
|
if len(nonFlagArgs) > 1 {
|
||||||
|
settings.Logger.Error("Please provide only one target (username or email)")
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
} else if len(nonFlagArgs) == 0 {
|
||||||
|
settings.Logger.Error("Please provide a target (username or email)")
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.Target = flag.Arg(0)
|
||||||
|
settings.Target = strings.TrimPrefix(settings.Target, "@") // Remove the @ of the username
|
||||||
|
|
||||||
|
if strings.Contains(settings.Target, " ") {
|
||||||
|
err = fmt.Errorf("target cannot contain spaces")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(settings.Target, "@") {
|
||||||
|
settings.TargetType = TargetEmail
|
||||||
|
} else {
|
||||||
|
settings.TargetType = TargetUsername
|
||||||
|
}
|
||||||
|
|
||||||
|
// If token is not set via flag, get it from env
|
||||||
|
if settings.Token == "null" || settings.Token == "" {
|
||||||
|
settings.Token = GetToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.Token == "null" || settings.Token == "" {
|
||||||
|
settings.Logger.Warn("No Github token provided. You might hit the rate limit. Check the help menu for more information.")
|
||||||
|
} else {
|
||||||
|
settings.Client = settings.Client.WithAuthToken(settings.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package github_recon_settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetToken retrieves the GitHub token from the environment variable or config file
|
||||||
|
func GetToken() string {
|
||||||
|
token := os.Getenv("GITHUB_RECON_TOKEN")
|
||||||
|
if token != "" {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the $HOME/.config/github-recon/env file for this variable
|
||||||
|
homedir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
godotenv.Load(filepath.Join(homedir, ".config/github-recon/env"))
|
||||||
|
token = os.Getenv("GITHUB_RECON_TOKEN")
|
||||||
|
if token != "" {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
gopixels "github.com/saran13raj/go-pixels"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
grey = lipgloss.Color("#7d7d7d")
|
||||||
|
green = lipgloss.Color("#a6e3a1")
|
||||||
|
blue = lipgloss.Color("#7287fd")
|
||||||
|
|
||||||
|
greyStyle = lipgloss.NewStyle().Foreground(grey)
|
||||||
|
greenStyle = lipgloss.NewStyle().Foreground(green)
|
||||||
|
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(blue)
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrintStruct(settings github_recon_settings.Settings, s any, indent int) {
|
||||||
|
if settings.Silent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := strings.Repeat(" ", indent)
|
||||||
|
|
||||||
|
v := reflect.ValueOf(s)
|
||||||
|
if !v.IsValid() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t := reflect.TypeOf(s)
|
||||||
|
|
||||||
|
for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
|
||||||
|
if v.IsNil() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
t = v.Type()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
if v.NumField() == 0 {
|
||||||
|
fmt.Println(prefix + greyStyle.Render("No data found"))
|
||||||
|
fmt.Println("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
printed := 0
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
field := t.Field(i).Name
|
||||||
|
value := v.Field(i)
|
||||||
|
if !value.CanInterface() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !value.IsValid() || (value.Kind() == reflect.String && value.String() == "") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if value.Kind() == reflect.String && value.String() == "0001-01-01 00:00:00 +0000 UTC" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (field == "FirstFoundIn" || field == "FoundIn") && !settings.ShowSource {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
printed++
|
||||||
|
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Struct, reflect.Slice, reflect.Array, reflect.Ptr, reflect.Map, reflect.Interface:
|
||||||
|
fmt.Println(prefix + greyStyle.Render(field+":"))
|
||||||
|
PrintStruct(settings, value.Interface(), indent+1)
|
||||||
|
|
||||||
|
case reflect.String:
|
||||||
|
fmt.Printf("%s%s %s\n", prefix, greyStyle.Render(field+":"), greenStyle.Render(fmt.Sprintf("%q", value.Interface())))
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Printf("%s%s %s\n", prefix, greyStyle.Render(field+":"), greenStyle.Render(fmt.Sprintf("%v", value.Interface())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if printed == 0 {
|
||||||
|
fmt.Println(prefix + greyStyle.Render("No data found"))
|
||||||
|
}
|
||||||
|
fmt.Println("")
|
||||||
|
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
if v.Len() == 0 {
|
||||||
|
fmt.Println(prefix + greyStyle.Render("No data found"))
|
||||||
|
fmt.Println("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
PrintStruct(settings, v.Index(i).Interface(), indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
if v.Len() == 0 {
|
||||||
|
fmt.Println(prefix + greyStyle.Render("No data found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := v.MapKeys()
|
||||||
|
keyStrs := make([]string, len(keys))
|
||||||
|
for i, k := range keys {
|
||||||
|
keyStrs[i] = fmt.Sprintf("%v", k.Interface())
|
||||||
|
}
|
||||||
|
sort.Strings(keyStrs)
|
||||||
|
|
||||||
|
for _, keyStr := range keyStrs {
|
||||||
|
for _, k := range keys {
|
||||||
|
if fmt.Sprintf("%v", k.Interface()) == keyStr {
|
||||||
|
val := v.MapIndex(k)
|
||||||
|
fmt.Println(prefix + greyStyle.Render(fmt.Sprintf("%v:", k.Interface())))
|
||||||
|
PrintStruct(settings, val.Interface(), indent+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Println(prefix + greenStyle.Render(fmt.Sprintf("%v", v.Interface())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Header() {
|
||||||
|
asciiArt := " __ \n ___ _/ / _______ _______ ___ \n / _ `/ _ \\/ __/ -_) __/ _ \\/ _ \\\n \\_, /_//_/_/ \\__/\\__/\\___/_//_/\n/___/ "
|
||||||
|
|
||||||
|
grey := lipgloss.Color("#7d7d7d")
|
||||||
|
|
||||||
|
greyStyle := lipgloss.NewStyle().Foreground(grey)
|
||||||
|
fmt.Println(
|
||||||
|
greyStyle.Render(lipgloss.JoinVertical(lipgloss.Right, asciiArt, "@anotherhadi\n")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrintTitle(silent bool, title string) {
|
||||||
|
if silent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(titleStyle.Render(title) + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrintAvatar(settings github_recon_settings.Settings, url string) {
|
||||||
|
if !settings.PrintAvatar || url == "" || settings.Silent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
tmpfile, err := os.CreateTemp("", "avatar-*.png")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpfile.Name())
|
||||||
|
|
||||||
|
_, err = io.Copy(tmpfile, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := gopixels.FromImagePath(tmpfile.Name(), 30, 25, "halfcell", true)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(output + "\n")
|
||||||
|
}
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||||
|
"github.com/google/go-github/v72/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WaitForRateLimit(settings github_recon_settings.Settings, resp *github.Response) {
|
||||||
|
if resp.Rate.Remaining == 0 {
|
||||||
|
settings.Logger.Info(
|
||||||
|
"Rate limit reached, waiting... (time:" + resp.Rate.Reset.Time.String() + ")",
|
||||||
|
)
|
||||||
|
time.Sleep(time.Until(resp.Rate.Reset.Time) + time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 DoesFolderExists(path string) bool {
|
||||||
|
if stat, err := os.Stat(path); err == nil && stat.IsDir() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func LevenshteinDistance(s1, s2 string) int {
|
||||||
|
len1 := len(s1)
|
||||||
|
len2 := len(s2)
|
||||||
|
|
||||||
|
dp := make([][]int, len1+1)
|
||||||
|
for i := range dp {
|
||||||
|
dp[i] = make([]int, len2+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i <= len1; i++ {
|
||||||
|
dp[i][0] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
for j := 0; j <= len2; j++ {
|
||||||
|
dp[0][j] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i <= len1; i++ {
|
||||||
|
for j := 1; j <= len2; j++ {
|
||||||
|
cost := 0
|
||||||
|
if s1[i-1] != s2[j-1] {
|
||||||
|
cost = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dp[i][j] = int(
|
||||||
|
math.Min(
|
||||||
|
float64(dp[i-1][j]+1),
|
||||||
|
math.Min(float64(dp[i][j-1]+1), float64(dp[i-1][j-1]+cost)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dp[len1][len2]
|
||||||
|
}
|
||||||
|
|
||||||
|
func SkipResult(name, email string) bool {
|
||||||
|
if name == "github-actions[bot]" || name == "GITHUB-RECON-SPOOFING" || name == "dependabot[bot]" || name == "github-actions" || name == "GitHub Actions" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if email == "github-actions[bot]@users.noreply.github.com" || email == "[email protected]" ||
|
||||||
|
email == "[email protected]" || email == "41898282+github-actions[bot]@users.noreply.github.com" || email == "49699333+dependabot[bot]@users.noreply.github.com" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user