Compare commits

..
44 Commits
Author SHA1 Message Date
Hadi e572d1326a early return if user not found
Signed-off-by: Hadi <[email protected]>
2025-09-17 21:39:32 +02:00
Hadi 0017d649d3 return nil instead of error
Signed-off-by: Hadi <[email protected]>
2025-09-17 21:31:02 +02:00
Hadi c163fb4ecd Fatal only in cmd
Signed-off-by: Hadi <[email protected]>
2025-09-17 21:26:54 +02:00
Hadi 325397dfef Check repositories length
Signed-off-by: Hadi <[email protected]>
2025-09-17 21:10:35 +02:00
Hadi dab0ba2b38 now retrying to get commits
Signed-off-by: Hadi <[email protected]>
2025-09-17 21:07:16 +02:00
Hadi 98769561c3 Add author.email in levenshtein distance for authors
Signed-off-by: Hadi <[email protected]>
2025-09-17 20:47:12 +02:00
Hadi c76953882c Nil when no result & better error handling
Signed-off-by: Hadi <[email protected]>
2025-09-17 20:39:53 +02:00
Hadi 94fd83ecce fix print with private interface
Signed-off-by: Hadi <[email protected]>
2025-09-17 20:26:31 +02:00
Hadi 2d01c1639f Fix isDir condition
Signed-off-by: Hadi <[email protected]>
2025-09-17 19:47:50 +02:00
Hadi 66030c1c16 add details to close friends
Signed-off-by: Hadi <[email protected]>
2025-09-17 19:31:54 +02:00
Hadi bb8f34055d fix nixos installation step
Signed-off-by: Hadi <[email protected]>
2025-09-16 19:35:38 +02:00
Hadi 26a1cfdd37 Change the way token is retrieve
Signed-off-by: Hadi <[email protected]>
2025-09-15 14:54:51 +02:00
Hadi 3064cdbf7a Change/get defaults
Signed-off-by: Hadi <[email protected]>
2025-09-15 14:40:55 +02:00
Hadi 52f9f1f3e7 Change settings
Signed-off-by: Hadi <[email protected]>
2025-09-15 14:37:21 +02:00
Hadi c7a1689084 Refactor to make the library reusable
Signed-off-by: Hadi <[email protected]>
2025-09-15 14:20:22 +02:00
Hadi fd79420749 update
Signed-off-by: Hadi <[email protected]>
2025-09-03 12:54:46 +02:00
Hadi ebf7dd529f Add trufflehog to readme
Signed-off-by: Hadi <[email protected]>
2025-09-01 15:17:59 +02:00
Hadi c84b2b0458 Trufflehog integration #11
Signed-off-by: Hadi <[email protected]>
2025-09-01 15:17:05 +02:00
Hadi 76e8eee4dc Email spoofing: only for primary email
Signed-off-by: Hadi <[email protected]>
2025-09-01 14:13:16 +02:00
Hadi c67901c9ef update flake
Signed-off-by: Hadi <[email protected]>
2025-08-23 16:20:35 +02:00
Hadi 40ff397df7 add screenshot
Signed-off-by: Hadi <[email protected]>
2025-08-23 15:17:24 +02:00
Hadi a17caa6233 Fix close friends max per page #9
Signed-off-by: Hadi <[email protected]>
2025-08-23 15:12:53 +02:00
Hadi 9b914e14ba Add more score when two users are in the same orgs #7
Signed-off-by: Hadi <[email protected]>
2025-08-23 15:08:50 +02:00
Hadi b0c7ce633e Lower the stored result to count occurence
Signed-off-by: Hadi <[email protected]>
2025-08-23 14:52:53 +02:00
Hadi beec1a873c rename login to username*
Signed-off-by: Hadi <[email protected]>
2025-08-23 14:46:38 +02:00
Hadi d5781169b7 rename login to username
Signed-off-by: Hadi <[email protected]>
2025-08-23 14:46:23 +02:00
Hadi ecb9180283 remove old comment
Signed-off-by: Hadi <[email protected]>
2025-08-23 14:45:19 +02:00
Hadi 8e2715e0ef edit strings for github.Client
Signed-off-by: Hadi <[email protected]>
2025-08-23 14:42:52 +02:00
Hadi 7b8cb1654f add json output for email
Signed-off-by: Hadi <[email protected]>
2025-08-23 14:42:35 +02:00
Hadi 426d10b2fe add avatar when found a user via email spoofing
Signed-off-by: Hadi <[email protected]>
2025-08-23 14:40:39 +02:00
Hadi df6c8bca11 skip email spoofing if no token provided
Signed-off-by: Hadi <[email protected]>
2025-08-23 14:32:32 +02:00
Hadi 39b72bc444 Add sleep for rate limit in the context
Signed-off-by: Hadi <[email protected]>
2025-08-23 14:13:53 +02:00
Hadi 586c91103a fix markdown links for emojis
Signed-off-by: Hadi <[email protected]>
2025-08-23 14:13:19 +02:00
Hadi 00b63b4bc3 Add email spoofing section
Signed-off-by: Hadi <[email protected]>
2025-08-23 14:08:01 +02:00
Hadi 56bc018128 add warning to not use main account
Signed-off-by: Hadi <[email protected]>
2025-08-23 14:03:42 +02:00
Hadi f1a13065fb change hide-avatar shortcut to let the help menu popup
Signed-off-by: Hadi <[email protected]>
2025-08-23 10:51:03 +02:00
Hadi 7037ec0864 add the --spoof-email toggle
Signed-off-by: Hadi <[email protected]>
2025-08-23 10:50:02 +02:00
Hadi 6b11b26678 Update readme
Signed-off-by: Hadi <[email protected]>
2025-08-23 10:44:12 +02:00
Hadi 6229445251 Init the spoofing feature #6
Signed-off-by: Hadi <[email protected]>
2025-08-23 10:35:37 +02:00
Hadi c1d732e03a rename to commit
Signed-off-by: Hadi <[email protected]>
2025-08-23 10:09:05 +02:00
Hadi 7b9c781281 rename to commit
Signed-off-by: Hadi <[email protected]>
2025-08-23 09:54:06 +02:00
Hadi 522e0671e1 add email
Signed-off-by: Hadi <[email protected]>
2025-08-23 09:52:21 +02:00
Hadi 862cf3659c Now read from Commit.Committer, Committer & Author
Signed-off-by: Hadi <[email protected]>
2025-08-23 09:52:14 +02:00
Hadi af4046d234 Init v2, rewrite 2025-08-22 21:58:54 +02:00
35 changed files with 2124 additions and 569 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

+14
View File
@@ -0,0 +1,14 @@
# Contributing
Everybody is invited and welcome to contribute to this repo. There is a lot to
do... Check the issues!
The process is straight-forward.
- Read
[How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews)
by Kubernetes. (but skip step 0 and 1)
- [Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo)
this repo.
- Write your changes (bug fixe, new feature, issues fix, ...).
- Create a Pull Request against the main branch.
+171 -32
View File
@@ -1,46 +1,89 @@
# GH-Recon <div align="center">
<img src="https://raw.githubusercontent.com/anotherhadi/github-recon/main/.github/assets/logo.png" width="120px" />
</div>
<br>
# 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) in both chronological orders - Extract unique commit authors (name + email)
- Find close friends
- Deep scan option (clone repositories, run regex searches, 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) you have permission to access the data you are querying.
## Installation ## 📦 Installation
### With Go ### With Go
```bash ```bash
go get github.com/anotherhadi/gh-recon go install github.com/anotherhadi/github-recon@latest
``` ```
### With Nix/NixOS ### With Nix/NixOS
<details>
<summary>Click to expand</summary>
**From anywhere (using the repo URL):** **From anywhere (using the repo URL):**
```bash ```bash
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:**
@@ -49,36 +92,132 @@ 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}
]; ];
``` ```
## Usage </details>
## 🧪 Usage
```bash ```bash
gh-recon --username TARGET_USER [--token YOUR_TOKEN] github-recon [--flags value] target_username_or_email
``` ```
### Flags ### Flags
- `--username`: GitHub username to inspect (required) ```txt
- `--token`: Personal Access Token (optional but recommended) -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")
-d, --deepscan Enable deep scan (clone repos, regex search, analyse licenses, etc.)
## Example --max-size int Limit the size of repositories to scan (in MB) (only for deep scan) (default 150)
-e, --exclude-repo strings Exclude repos from deep scan (comma-separated list, only for deep scan)
```bash -r, --refresh Refresh the cache (only for deep scan)
gh-recon --username anotherhadi --token ghp_ABC123... -s, --show-source Show where the information (authors, emails, etc) were found (only for deep scan)
-m, --max-distance int Maximum Levenshtein distance for matching usernames & emails (only for deep scan) (default 20)
--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
``` ```
## Todo ### Token
Feel free to contribute! Here are some ideas: For the best experience, provide a **GitHub Personal Access Token**. Without a
token, you will quickly hit the **rate limit** and have to wait.
- Fetch names in License files - For **basic usage**, you can create a token **without any permissions**.
- Fetch emails in README files/comments - 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
github-recon anotherhadi --token ghp_ABC123...
github-recon [email protected] # Find github accounts by email
github-recon anotherhadi --json output.json --deepscan # Clone the repo and search for leaked email
```
## 🕵️‍♂️ Cover your tracks
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.
- **Manage email exposure**: Use GitHub's settings to control which email
addresses are visible on your profile and in commit history. You can also use
a no-reply email address for commits. Delete/modify any sensitive information
in your commit history.
- **Be Mindful of Repository Content**: Avoid including sensitive information in
your repositories, such as API keys, passwords, emails or personal data. Use
`.gitignore` to exclude files that contain sensitive information.
You can also use a tool like [TruffleHog](github.com/trufflesecurity/trufflehog)
to scan your repositories specifically for exposed secrets and tokens.
**Useful links:**
- [Blocking command line pushes that expose your personal email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/blocking-command-line-pushes-that-expose-your-personal-email-address)
- [No-reply email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address)
## 🤝 Contributing
Feel free to contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
## 🙏 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
View File
@@ -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
View File
@@ -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
View File
@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1746663147, "lastModified": 1756787288,
"narHash": "sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ+TCkTRpRc=", "narHash": "sha256-rw/PHa1cqiePdBxhF66V7R+WAP8WekQ0mCDG4CFqT8Y=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "dda3dcd3fe03e991015e9a74b22d35950f264a54", "rev": "d0fc30899600b9b3466ddb260fd83deb486c32f1",
"type": "github" "type": "github"
}, },
"original": { "original": {
+16 -11
View File
@@ -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,
nixpkgs,
}: let
supportedSystems = ["x86_64-linux" "aarch64-linux"]; 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.1.0"; version = "2.1.0";
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-CPk8B8FKEoN8qff6WV/iBf0eVjTBMVfJQvlVcti6dfM="; vendorHash = "sha256-AD0h0k2n8gPqSBz5qqb0ZON/jWiSEWpeO97xR7cYSy8=";
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;
}; };
}; };
-71
View File
@@ -1,71 +0,0 @@
package ghrecon
import (
"fmt"
"github.com/google/go-github/v72/github"
)
func (r Recon) Commits(username string) {
PrintTitle("🐙 Commits")
seenNames := make(map[string]struct{})
seenEmails := make(map[string]struct{})
var names, emails []string
collect := func(order string) error {
opts := &github.SearchOptions{
Sort: "author-date",
Order: order,
ListOptions: github.ListOptions{PerPage: 100},
}
for page := 1; page <= 10; page++ {
opts.ListOptions.Page = page
result, resp, err := r.client.Search.Commits(
r.ctx,
fmt.Sprintf("author:%s", username),
opts,
)
if err != nil {
return fmt.Errorf("fetch page %d (%s): %w", page, order, err)
}
WaitForRateLimit(resp)
if len(result.Commits) == 0 {
break
}
for _, item := range result.Commits {
a := item.Commit.GetAuthor()
name := a.GetName()
email := a.GetEmail()
if _, seen := seenNames[name]; !seen {
seenNames[name] = struct{}{}
names = append(names, name)
PrintInfo(
"Name "+fmt.Sprint(len(names)),
name,
"from "+item.GetRepository().Owner.GetLogin()+"/"+item.GetRepository().
GetName(),
)
}
if _, seen := seenEmails[email]; !seen {
seenEmails[email] = struct{}{}
emails = append(emails, email)
PrintInfo(
"Email "+fmt.Sprint(len(emails)),
email,
"from "+item.GetRepository().Owner.GetLogin()+"/"+item.GetRepository().
GetName(),
)
}
}
}
return nil
}
if err := collect("desc"); err != nil {
r.logger.Error("Failed to fetch commits", "err", err, "order", "desc")
}
if err := collect("asc"); err != nil {
r.logger.Error("Failed to fetch commits", "err", err, "order", "asc")
}
}
-111
View File
@@ -1,111 +0,0 @@
package ghrecon
import (
"fmt"
)
func (r Recon) SshKeys(username string) {
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 {
PrintTitle("🔑 SSH Keys")
r.logger.Info("No SSH Keys found\n")
} else {
PrintTitle("🔑 SSH Keys")
for i, key := range sshKeys {
PrintInfo("Key n°", fmt.Sprintf("%d", i))
PrintInfo("ID", fmt.Sprintf("%d", key.GetID()))
PrintInfo("URL", key.GetURL())
PrintInfo("Title", key.GetTitle())
PrintInfo("Created At", key.GetCreatedAt().String())
PrintInfo("Key", key.GetKey())
PrintInfo("Read Only", fmt.Sprintf("%t", key.GetReadOnly()))
PrintInfo("Verified", fmt.Sprintf("%t", key.GetVerified()))
PrintInfo("Last Used", key.GetLastUsed().String())
PrintInfo("Added By", key.GetAddedBy())
fmt.Println()
}
}
WaitForRateLimit(resp)
}
func (r Recon) GpgKeys(username string) {
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 {
PrintTitle("🗝️ GPG Keys")
r.logger.Info("No GPG Keys found\n")
} else {
PrintTitle("🗝️ GPG Keys")
for i, key := range gpgKeys {
PrintInfo("Key n°", fmt.Sprintf("%d", i))
PrintInfo("ID", fmt.Sprintf("%d", key.GetID()))
PrintInfo("Key ID", key.GetKeyID())
PrintInfo("Public Key", key.GetPublicKey())
PrintInfo("Created At", key.GetCreatedAt().String())
PrintInfo("Expires At", key.GetExpiresAt().String())
PrintInfo("Can Sign", fmt.Sprintf("%t", key.GetCanSign()))
PrintInfo("Can Encrypt Comms", fmt.Sprintf("%t", key.GetCanEncryptComms()))
PrintInfo("Can Encrypt Storage", fmt.Sprintf("%t", key.GetCanEncryptStorage()))
PrintInfo("Can Certify", fmt.Sprintf("%t", key.GetCanCertify()))
PrintInfo("Primary Key ID", fmt.Sprintf("%d", key.GetPrimaryKeyID()))
PrintInfo("Raw Key", key.GetRawKey())
PrintInfo("Emails", fmt.Sprintf("%d", len(key.Emails)))
for j, email := range key.Emails {
PrintInfo(" Email n°", fmt.Sprintf("%d", j))
PrintInfo(" Email", email.GetEmail())
PrintInfo(" Verified", fmt.Sprintf("%t", email.GetVerified()))
}
PrintInfo("Subkeys", fmt.Sprintf("%d", len(key.Subkeys)))
for j, subkey := range key.Subkeys {
PrintInfo(" Subkey n°", fmt.Sprintf("%d", j))
PrintInfo(" Subkey ID", fmt.Sprintf("%d", subkey.GetID()))
PrintInfo(" Subkey Key ID", subkey.GetKeyID())
PrintInfo(" Subkey Created At", subkey.GetCreatedAt().String())
PrintInfo(" Subkey Expires At", subkey.GetExpiresAt().String())
PrintInfo(" Subkey Can Sign", fmt.Sprintf("%t", subkey.GetCanSign()))
PrintInfo(
" Subkey Can Encrypt Comms",
fmt.Sprintf("%t", subkey.GetCanEncryptComms()),
)
PrintInfo(
" Subkey Can Encrypt Storage",
fmt.Sprintf("%t", subkey.GetCanEncryptStorage()),
)
PrintInfo(" Subkey Can Certify", fmt.Sprintf("%t", subkey.GetCanCertify()))
PrintInfo(" Subkey Primary Key ID", fmt.Sprintf("%d", subkey.GetPrimaryKeyID()))
PrintInfo(" Subkey Raw Key", subkey.GetRawKey())
PrintInfo(" Subkey Public Key", subkey.GetPublicKey())
}
fmt.Println()
}
}
WaitForRateLimit(resp)
}
func (r Recon) SshSigningKeys(username string) {
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 {
PrintTitle("📝 SSH Signing Keys")
r.logger.Info("No SSH Signing Keys found\n")
} else {
PrintTitle("📝 SSH Signing Keys")
for i, key := range signingKeys {
PrintInfo("Key n°", fmt.Sprintf("%d", i))
PrintInfo("ID", fmt.Sprintf("%d", key.GetID()))
PrintInfo("Key", key.GetKey())
PrintInfo("Title", key.GetTitle())
PrintInfo("Created At", key.GetCreatedAt().String())
fmt.Println()
}
}
WaitForRateLimit(resp)
}
-22
View File
@@ -1,22 +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
}
func NewRecon(client *github.Client, logger *log.Logger, ctx context.Context) *Recon {
return &Recon{
client: client,
logger: logger,
ctx: ctx,
}
}
-34
View File
@@ -1,34 +0,0 @@
package ghrecon
import (
"fmt"
)
func (r Recon) Orgs(username string) {
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 {
PrintTitle("🏢 Organizations")
r.logger.Info("No Organizations found\n")
} else {
PrintTitle("🏢 Organizations")
for i, org := range orgs {
PrintInfo("Orgs n°", fmt.Sprintf("%d", i))
PrintInfo("Login", org.GetLogin())
PrintInfo("ID", fmt.Sprintf("%d", org.GetID()))
PrintInfo("Node ID", org.GetNodeID())
PrintInfo("URL", org.GetURL())
PrintInfo("Repos URL", org.GetReposURL())
PrintInfo("Events URL", org.GetEventsURL())
PrintInfo("Hooks URL", org.GetHooksURL())
PrintInfo("Issues URL", org.GetIssuesURL())
PrintInfo("Members URL", org.GetMembersURL())
PrintInfo("Public Members URL", org.GetPublicMembersURL())
PrintInfo("Avatar URL", org.GetAvatarURL())
PrintInfo("Description", org.GetDescription())
fmt.Println()
}
}
WaitForRateLimit(resp)
}
-40
View File
@@ -1,40 +0,0 @@
package ghrecon
import (
"encoding/json"
"fmt"
)
func (r Recon) Socials(username string) {
resp, err := FetchGitHubAPI(r.client, "", "/users/"+username+"/social_accounts")
if err != nil {
r.logger.Error("Failed to fetch socials", "err", err)
return
}
type SocialAccount struct {
Provider string `json:"provider"`
URL string `json:"url"`
}
type SocialAccounts []SocialAccount
var socialAccounts SocialAccounts
err = json.Unmarshal(resp, &socialAccounts)
if err != nil {
r.logger.Error("Failed to unmarshal socials", "err", err)
return
}
if len(socialAccounts) == 0 {
PrintTitle("🐥 Socials")
PrintTitle("No Socials found\n")
} else {
PrintTitle("🐥 Socials")
for i, account := range socialAccounts {
PrintInfo("Social n°", fmt.Sprintf("%d", i))
PrintInfo("Provider", account.Provider)
PrintInfo("URL", account.URL)
fmt.Println()
}
}
}
-45
View File
@@ -1,45 +0,0 @@
package ghrecon
import (
"fmt"
)
func (r Recon) User(username string) {
user, resp, err := r.client.Users.Get(r.ctx, username)
if resp.StatusCode == 404 {
r.logger.Fatal("User not found")
}
if err != nil {
r.logger.Fatal("Failed to fetch user's information", "err", err)
}
PrintTitle("👤 User informations")
PrintInfo("Username", user.GetLogin())
PrintInfo("ID", fmt.Sprintf("%d", user.GetID()))
PrintInfo("Avatar URL", user.GetAvatarURL())
PrintInfo("Gravatar ID", user.GetGravatarID())
PrintInfo("Name", user.GetName())
PrintInfo("Company", user.GetCompany())
PrintInfo("Location", user.GetLocation())
PrintInfo("Email", user.GetEmail())
PrintInfo("Hireable", fmt.Sprintf("%t", user.GetHireable()))
PrintInfo("Bio", user.GetBio())
PrintInfo("Public Repos", fmt.Sprintf("%d", user.GetPublicRepos()))
PrintInfo("Public Gists", fmt.Sprintf("%d", user.GetPublicGists()))
PrintInfo("Followers", fmt.Sprintf("%d", user.GetFollowers()))
PrintInfo("Following", fmt.Sprintf("%d", user.GetFollowing()))
PrintInfo("Created At", user.GetCreatedAt().String())
PrintInfo("Updated At", user.GetUpdatedAt().String())
PrintInfo("Suspended At", user.GetSuspendedAt().String())
PrintInfo("Type", user.GetType())
PrintInfo("Site Admin", fmt.Sprintf("%t", user.GetSiteAdmin()))
PrintInfo("Total Private Repos", fmt.Sprintf("%d", user.GetTotalPrivateRepos()))
PrintInfo("Owned Private Repos", fmt.Sprintf("%d", user.GetOwnedPrivateRepos()))
PrintInfo("Private Gists", fmt.Sprintf("%d", user.GetPrivateGists()))
PrintInfo("Disk Usage", fmt.Sprintf("%d", user.GetDiskUsage()))
PrintInfo("Collaborators", fmt.Sprintf("%d", user.GetCollaborators()))
PrintInfo("Plan", user.GetPlan().GetName())
fmt.Println()
WaitForRateLimit(resp)
}
-113
View File
@@ -1,113 +0,0 @@
package ghrecon
import (
"fmt"
"io"
"net/http"
"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 Header() {
asciiArt := " __ \n ___ _/ / _______ _______ ___ \n / _ `/ _ \\/ __/ -_) __/ _ \\/ _ \\\n \\_, /_//_/_/ \\__/\\__/\\___/_//_/\n/___/ "
fmt.Println(GreyStyle.Render(lipgloss.JoinVertical(lipgloss.Right, asciiArt, "@anotherhadi\n")))
}
func ParseUsername(username string) error {
if username == "" {
return fmt.Errorf("username is required")
}
if strings.Contains(username, " ") {
return fmt.Errorf("username cannot contain spaces")
}
if strings.Contains(username, "@") {
return fmt.Errorf("username cannot contain @")
}
return nil
}
func FetchGitHubAPI(github *github.Client, token, path string) ([]byte, error) {
url := "https://api.github.com" + path
userAgent := "GHRecon/1.0"
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("error creating request for %s: %w", url, err)
}
if token != "" {
req.Header.Set("Authorization", "token "+token)
}
req.Header.Set("Accept", "application/vnd.github.v3+json")
req.Header.Set("User-Agent", userAgent)
resp, err := github.Client().Do(req)
if err != nil {
return nil, fmt.Errorf("error executing request for %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf(
"request for %s failed with status %d: %s",
url,
resp.StatusCode,
string(bodyBytes),
)
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf(
"error reading response body for %s: %w",
url,
err,
)
}
return bodyBytes, nil
}
func PrintTitle(title string) {
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#7287fd"))
fmt.Println(style.Render(title) + "\n")
}
func PrintInfo(key, value string, more ...string) {
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)
}
}
+1
View File
@@ -0,0 +1 @@
package github_recon
+92
View File
@@ -0,0 +1,92 @@
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
Username 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-email:%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 {
name := item.Commit.GetAuthor().GetName()
email := item.Commit.GetAuthor().GetEmail()
login := item.GetAuthor().GetLogin()
if login == "" {
login = "Unknown"
}
if utils.SkipResult(name, email) {
continue
}
if _, seen := results[strings.ToLower(name)+" - "+strings.ToLower(email)+" - "+strings.ToLower(login)]; !seen {
author := CommitResult{
Name: name,
Email: email,
Username: login,
Occurrences: 1,
FirstFoundIn: item.GetRepository().Owner.GetLogin() + "/" + item.GetRepository().
GetName(),
}
results[strings.ToLower(name)+" - "+strings.ToLower(email)+" - "+strings.ToLower(login)] = author
} else {
result := results[strings.ToLower(name)+" - "+strings.ToLower(email)+" - "+strings.ToLower(login)]
result.Occurrences++
results[strings.ToLower(name)+" - "+strings.ToLower(email)+" - "+strings.ToLower(login)] = result
}
}
}
return nil
}
// Range of dates to bypass the limit of 1000 results
for _, date := range []string{
"<2023-01-01", "2023-01-01..2023-12-31",
"2024-01-01..2024-05-31",
"2024-06-01..2024-12-31",
"2025-01-01..2025-05-31",
"2025-06-01..2025-12-31",
">2026-01-01",
} {
if err := collect(date); err != nil {
s.Logger.Error("Failed to fetch commits", "err", err, "date", date)
}
}
for _, result := range results {
response = append(response, result)
}
return
}
+44
View File
@@ -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
}
+130
View File
@@ -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
}
+169
View File
@@ -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
}
+105
View File
@@ -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
}
+394
View File
@@ -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.GetCloneURL(),
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)})
}
+135
View File
@@ -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
}
+83
View File
@@ -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
}
+35
View File
@@ -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.GetURL(),
Description: org.GetDescription(),
}
response = append(response, o)
}
utils.WaitForRateLimit(s, resp)
return
}
+45
View File
@@ -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
}
+69
View File
@@ -0,0 +1,69 @@
package recon
import (
"fmt"
github_recon_settings "github.com/anotherhadi/github-recon/settings"
"github.com/anotherhadi/github-recon/utils"
)
type UserResult struct {
Username string
AvatarURL string
GravatarID string
Name string
Company string
Location string
Email string
Hireable string
Bio string
PublicRepos string
PublicGists string
Followers string
Following string
CreatedAt string
UpdatedAt string
SuspendedAt string
TotalPrivateRepos string
PrivateGists string
DiskUsage string
Collaborators string
Plan string
}
func User(s github_recon_settings.Settings) (response UserResult, err error) {
user, resp, err := s.Client.Users.Get(s.Ctx, s.Target)
if resp.StatusCode == 404 {
return UserResult{}, nil
}
if err != nil {
return UserResult{}, fmt.Errorf("failed to fetch user's information")
}
u := UserResult{
Username: user.GetLogin(),
AvatarURL: user.GetAvatarURL(),
GravatarID: user.GetGravatarID(),
Name: user.GetName(),
Company: user.GetCompany(),
Location: user.GetLocation(),
Email: user.GetEmail(),
Hireable: fmt.Sprintf("%t", user.GetHireable()),
Bio: user.GetBio(),
PublicRepos: fmt.Sprintf("%d", user.GetPublicRepos()),
PublicGists: fmt.Sprintf("%d", user.GetPublicGists()),
Followers: fmt.Sprintf("%d", user.GetFollowers()),
Following: fmt.Sprintf("%d", user.GetFollowing()),
CreatedAt: user.GetCreatedAt().String(),
UpdatedAt: user.GetUpdatedAt().String(),
SuspendedAt: user.GetSuspendedAt().String(),
TotalPrivateRepos: fmt.Sprintf("%d", user.GetTotalPrivateRepos()),
PrivateGists: fmt.Sprintf("%d", user.GetPrivateGists()),
DiskUsage: fmt.Sprintf("%d", user.GetDiskUsage()),
Collaborators: fmt.Sprintf("%d", user.GetCollaborators()),
Plan: user.GetPlan().GetName(),
}
utils.WaitForRateLimit(s, resp)
return u, nil
}
+7 -3
View File
@@ -1,11 +1,14 @@
module github.com/anotherhadi/gh-recon module github.com/anotherhadi/github-recon
go 1.24.2 go 1.24.5
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/joho/godotenv v1.5.1
github.com/saran13raj/go-pixels v0.0.0-20250629121333-58b240a3ae51
github.com/spf13/pflag v1.0.7
) )
require ( require (
@@ -23,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.28.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.30.0 // indirect
) )
+10 -2
View File
@@ -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,12 +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/saran13raj/go-pixels v0.0.0-20250629121333-58b240a3ae51 h1:H/XUfYcLxI3CBmDlgBpnOeTntRgqWvIoUXnqhCF5a0s=
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.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
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=
-64
View File
@@ -1,64 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"os"
ghrecon "github.com/anotherhadi/gh-recon/gh-recon"
"github.com/charmbracelet/log"
"github.com/google/go-github/v72/github"
)
func main() {
var username string
var token string
flag.StringVar(&username, "username", "", "Target username")
flag.StringVar(&token, "token", "", "Github token")
flag.Parse()
if username == "" {
fmt.Println("Please provide a username with the --username flag")
os.Exit(1)
}
err := ghrecon.ParseUsername(username)
if err != nil {
log.Error("Invalid username", "err", err)
os.Exit(1)
}
client := github.NewClient(nil)
if token == "" {
log.Info(
"It's recommended to set a Github token for better rate limits. You can set it using the --token flag.",
)
} else {
client = client.WithAuthToken(token)
}
ctx := context.Background()
styles := log.DefaultStyles()
styles.Levels[log.InfoLevel] = styles.Levels[log.InfoLevel].Foreground(ghrecon.Grey)
logger := log.NewWithOptions(os.Stderr, log.Options{
ReportCaller: false,
ReportTimestamp: false,
})
logger.SetStyles(styles)
r := ghrecon.NewRecon(
client,
logger,
ctx,
)
ghrecon.Header()
r.User(username)
r.Orgs(username)
r.SshKeys(username)
r.GpgKeys(username)
r.SshSigningKeys(username)
r.Socials(username)
r.Commits(username)
}
+176
View File
@@ -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 of %s:\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
}
+40
View File
@@ -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)
}
+175
View File
@@ -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
View File
@@ -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
}