mirror of
https://github.com/anotherhadi/github-recon.git
synced 2026-10-05 10:58:25 +02:00
Init v2, rewrite
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1 @@
|
||||
result/
|
||||
@@ -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.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Hadi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,164 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/anotherhadi/github-recon/main/.github/assets/logo.png" width="120px" />
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
# Github-Recon 🔍
|
||||
|
||||
<p>
|
||||
<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/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/github-recon"><img src="https://goreportcard.com/badge/github.com/anotherhadi/github-recon" alt="GoReportCard"></a>
|
||||
</p>
|
||||
|
||||
- [🧾 Project Overview](#-project-overview)
|
||||
- [🚀 Features](#-features)
|
||||
- [⚠️ Disclaimer](#-disclaimer)
|
||||
- [📦 Installation](#-installation)
|
||||
- [With Go](#with-go)
|
||||
- [With Nix/NixOS](#with-nixnixos)
|
||||
- [🧪 Usage](#-usage)
|
||||
- [Flags](#flags)
|
||||
- [💡 Examples](#-examples)
|
||||
- [🕵️♂️ Cover your tracks](#-cover-your-tracks)
|
||||
- [🤝 Contributing](#-contributing)
|
||||
- [🙏 Credits](#-credits)
|
||||
|
||||
## 🧾 Project Overview
|
||||
|
||||
Retrieves and aggregates public OSINT data about a GitHub user using Go and the
|
||||
GitHub API. Finds hidden emails in commit history, previous usernames, friends,
|
||||
other GitHub accounts, and more.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- Export results to JSON
|
||||
|
||||
**From usernames:**
|
||||
|
||||
- Retrieve basic user profile information (username, ID, avatar, bio, creation
|
||||
dates)
|
||||
- Display the avatar in the terminal
|
||||
- List organizations and roles
|
||||
- Fetch SSH and GPG keys
|
||||
- Enumerate social accounts
|
||||
- Extract unique commit authors (name + email)
|
||||
- Find close friends
|
||||
- Deep scan option (clone repositories, regex search, analyze licenses, etc.)
|
||||
- Levenshtein distance for matching usernames and emails
|
||||
|
||||
**From emails:**
|
||||
|
||||
- Search for a specific email through all Github commits
|
||||
- Spoof an email to found an user account
|
||||
|
||||
## ⚠️ Disclaimer
|
||||
|
||||
This tool is intended for educational purposes only. Use responsibly and ensure
|
||||
you have permission to access the data you are querying.
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### With Go
|
||||
|
||||
```bash
|
||||
go install github.com/anotherhadi/github-recon@latest
|
||||
```
|
||||
|
||||
### With Nix/NixOS
|
||||
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
**From anywhere (using the repo URL):**
|
||||
|
||||
```bash
|
||||
nix run github:anotherhadi/github-recon -- [--flags value] target_username_or_email
|
||||
```
|
||||
|
||||
**Permanent Installation:**
|
||||
|
||||
```bash
|
||||
# add the flake to your flake.nix
|
||||
{
|
||||
inputs = {
|
||||
github-recon.url = "github:anotherhadi/github-recon";
|
||||
};
|
||||
}
|
||||
|
||||
# then add it to your packages
|
||||
environment.systemPackages = with pkgs; [ # or home.packages
|
||||
github-recon
|
||||
];
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 🧪 Usage
|
||||
|
||||
```bash
|
||||
github-recon [--flags value] target_username_or_email
|
||||
```
|
||||
|
||||
### Flags
|
||||
|
||||
```txt
|
||||
-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.)
|
||||
--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)
|
||||
-r, --refresh Refresh the cache (only for deep scan)
|
||||
-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)
|
||||
-S, --silent Suppress all non-essential output
|
||||
-h, --hide-avatar Hide the avatar in the output
|
||||
-j, --json string Write results to specified JSON file
|
||||
```
|
||||
|
||||
## 💡 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.
|
||||
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||
gopixels "github.com/saran13raj/go-pixels"
|
||||
)
|
||||
|
||||
func printAvatar(settings github_recon_settings.Settings, url string) {
|
||||
if settings.HideAvatar || 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")
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
recon "github.com/anotherhadi/github-recon/github-recon/email"
|
||||
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||
)
|
||||
|
||||
type EmailResult struct {
|
||||
DateTime string
|
||||
Target string
|
||||
TargetType github_recon_settings.TargetType
|
||||
|
||||
Commit recon.EmailsResult
|
||||
}
|
||||
|
||||
func email(settings github_recon_settings.Settings, datetime string) {
|
||||
result := EmailResult{
|
||||
Target: settings.Target,
|
||||
TargetType: settings.TargetType,
|
||||
DateTime: datetime,
|
||||
}
|
||||
|
||||
printTitle(settings.Silent, "👤 Commits author")
|
||||
result.Commit = recon.Email(settings)
|
||||
printStruct(settings, result.Commit, 0)
|
||||
}
|
||||
+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)
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
settings := github_recon_settings.GetSettings()
|
||||
datetime := time.Now().String()
|
||||
|
||||
if !settings.Silent {
|
||||
header()
|
||||
|
||||
printStruct(settings, struct {
|
||||
Target string
|
||||
TargetType string
|
||||
DateTime string
|
||||
}{
|
||||
Target: settings.Target,
|
||||
TargetType: string(settings.TargetType),
|
||||
DateTime: datetime,
|
||||
}, 0)
|
||||
}
|
||||
|
||||
if settings.TargetType == github_recon_settings.TargetUsername {
|
||||
username(settings, datetime)
|
||||
} else {
|
||||
email(settings, datetime)
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
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)
|
||||
t := reflect.TypeOf(s)
|
||||
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
if v.NumField() == 0 {
|
||||
fmt.Println(prefix + greyStyle.Render("No data found"))
|
||||
fmt.Println("")
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := t.Field(i).Name
|
||||
value := v.Field(i)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.Struct, reflect.Slice, reflect.Array, reflect.Ptr:
|
||||
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())))
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Println(prefix + greenStyle.Render(fmt.Sprintf("%v", v.Interface())))
|
||||
fmt.Println("")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
recon "github.com/anotherhadi/github-recon/github-recon/username"
|
||||
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||
)
|
||||
|
||||
type UsernameResult struct {
|
||||
DateTime string // Now
|
||||
Target string
|
||||
TargetType github_recon_settings.TargetType
|
||||
|
||||
User recon.UserResult
|
||||
Socials recon.SocialsResult
|
||||
Orgs recon.OrgsResult
|
||||
|
||||
SshKeys recon.SshKeysResult
|
||||
SshSigningKeys recon.SshSigningKeysResult
|
||||
GpgKeys recon.GpgKeysResult
|
||||
|
||||
CloseFriends recon.CloseFriendsResult
|
||||
|
||||
Commits recon.CommitsResult
|
||||
|
||||
DeepScan recon.DeepScanResult
|
||||
}
|
||||
|
||||
func username(settings github_recon_settings.Settings, datetime string) {
|
||||
result := UsernameResult{
|
||||
Target: settings.Target,
|
||||
TargetType: settings.TargetType,
|
||||
DateTime: datetime,
|
||||
}
|
||||
|
||||
printTitle(settings.Silent, "👤 User informations")
|
||||
result.User = recon.User(settings)
|
||||
printAvatar(settings, result.User.AvatarURL)
|
||||
printStruct(settings, result.User, 0)
|
||||
|
||||
printTitle(settings.Silent, "🐥 Socials")
|
||||
result.Socials = recon.Socials(settings)
|
||||
printStruct(settings, result.Socials, 0)
|
||||
|
||||
printTitle(settings.Silent, "🏢 Organizations")
|
||||
result.Orgs = recon.Orgs(settings)
|
||||
printStruct(settings, result.Orgs, 0)
|
||||
|
||||
printTitle(settings.Silent, "🔑 SSH Keys")
|
||||
result.SshKeys = recon.SshKeys(settings)
|
||||
printStruct(settings, result.SshKeys, 0)
|
||||
|
||||
printTitle(settings.Silent, "🖋️ SSH Signing Keys")
|
||||
result.SshSigningKeys = recon.SshSigningKeys(settings)
|
||||
printStruct(settings, result.SshSigningKeys, 0)
|
||||
|
||||
printTitle(settings.Silent, "🔐 GPG Keys")
|
||||
result.GpgKeys = recon.GpgKeys(settings)
|
||||
printStruct(settings, result.GpgKeys, 0)
|
||||
|
||||
printTitle(settings.Silent, "🤝 Close Friends")
|
||||
result.CloseFriends = recon.CloseFriends(settings)
|
||||
printStruct(settings, result.CloseFriends, 0)
|
||||
|
||||
printTitle(settings.Silent, "📝 Commits")
|
||||
result.Commits = recon.Commits(settings)
|
||||
printStruct(settings, result.Commits, 0)
|
||||
|
||||
if settings.DeepScan {
|
||||
printTitle(settings.Silent, "🔍 Deep Scan")
|
||||
result.DeepScan = recon.DeepScan(settings)
|
||||
printStruct(settings, result.DeepScan, 0)
|
||||
}
|
||||
|
||||
writeJson(settings, result)
|
||||
}
|
||||
Generated
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1755615617,
|
||||
"narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "20075955deac2583bb12f07151c2df830ef346b4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
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.";
|
||||
|
||||
inputs = {nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
}: let
|
||||
supportedSystems = ["x86_64-linux" "aarch64-linux"];
|
||||
|
||||
forAllSystems = f:
|
||||
nixpkgs.lib.genAttrs supportedSystems
|
||||
(system: f system (import nixpkgs {inherit system;}));
|
||||
|
||||
pname = "github-recon";
|
||||
version = "2.0.0";
|
||||
|
||||
ldflags = ["-s" "-w"];
|
||||
in {
|
||||
packages = forAllSystems (system: pkgs: {
|
||||
"${pname}" = pkgs.buildGoModule {
|
||||
inherit pname version ldflags;
|
||||
|
||||
src = ./.;
|
||||
subPackages = ["cmd"];
|
||||
outputs = ["out"];
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp $GOPATH/bin/cmd $out/bin/github-recon
|
||||
'';
|
||||
|
||||
vendorHash = "sha256-AD0h0k2n8gPqSBz5qqb0ZON/jWiSEWpeO97xR7cYSy8=";
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
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.";
|
||||
homepage = "https://github.com/anotherhadi/github-recon";
|
||||
platforms = platforms.unix;
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
defaultPackage =
|
||||
forAllSystems (system: pkgs: self.packages.${system}.${pname});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package github_recon
|
||||
@@ -0,0 +1,91 @@
|
||||
package recon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||
"github.com/anotherhadi/github-recon/utils"
|
||||
"github.com/google/go-github/v72/github"
|
||||
)
|
||||
|
||||
type EmailsResult []EmailResult
|
||||
|
||||
type EmailResult struct {
|
||||
Name string
|
||||
Email string
|
||||
Username string
|
||||
Occurrences int
|
||||
FirstFoundIn string
|
||||
}
|
||||
|
||||
func Email(s github_recon_settings.Settings) (response EmailsResult) {
|
||||
results := make(map[string]EmailResult)
|
||||
|
||||
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[name+" - "+email+" - "+login]; !seen {
|
||||
author := EmailResult{
|
||||
Name: name,
|
||||
Email: email,
|
||||
Username: login,
|
||||
Occurrences: 1,
|
||||
FirstFoundIn: item.GetRepository().Owner.GetLogin() + "/" + item.GetRepository().
|
||||
GetName(),
|
||||
}
|
||||
results[name+" - "+email+" - "+login] = author
|
||||
} else {
|
||||
result := results[name+" - "+email+" - "+login]
|
||||
result.Occurrences++
|
||||
results[name+" - "+email+" - "+login] = result
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Range of dates to bypass the limit of 1000 results
|
||||
for _, date := range []string{
|
||||
"<2023-01-01", "2023-01-01..2023-12-31",
|
||||
"2024-01-01..2024-05-31",
|
||||
"2024-06-01..2024-12-31",
|
||||
"2025-01-01..2025-05-31",
|
||||
"2025-06-01..2025-12-31",
|
||||
">2026-01-01",
|
||||
} {
|
||||
if err := collect(date); err != nil {
|
||||
s.Logger.Error("Failed to fetch commits", "err", err, "date", date)
|
||||
}
|
||||
}
|
||||
|
||||
for _, result := range results {
|
||||
response = append(response, result)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package recon
|
||||
@@ -0,0 +1,120 @@
|
||||
package recon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
github_recon_settings "github.com/anotherhadi/github-recon/settings"
|
||||
"github.com/anotherhadi/github-recon/utils"
|
||||
)
|
||||
|
||||
type CloseFriendsResult []CloseFriendResult
|
||||
|
||||
type CloseFriendResult 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 CloseFriends(s github_recon_settings.Settings) (response CloseFriendsResult) {
|
||||
following, resp, err := s.Client.Users.ListFollowing(s.Ctx, s.Target, nil)
|
||||
if err != nil {
|
||||
s.Logger.Error("Failed to fetch user's following list", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
utils.WaitForRateLimit(s, resp)
|
||||
|
||||
if len(following) >= maxFollowingForTarget {
|
||||
s.Logger.Info("Skipping close friends check", "reason", fmt.Sprintf("Target follows %d or more users (%d)", maxFollowingForTarget, len(following)))
|
||||
return
|
||||
}
|
||||
|
||||
if len(following) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, userBeingFollowedByTarget := range following {
|
||||
loginName := userBeingFollowedByTarget.GetLogin()
|
||||
if loginName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
currentScore := 0
|
||||
|
||||
userDetails, userResp, userErr := s.Client.Users.Get(s.Ctx, loginName)
|
||||
if userErr != nil {
|
||||
s.Logger.Warn(
|
||||
"Failed to fetch details for followed user",
|
||||
"followed_user",
|
||||
loginName,
|
||||
"err",
|
||||
userErr,
|
||||
)
|
||||
if userResp != nil {
|
||||
utils.WaitForRateLimit(s, userResp)
|
||||
}
|
||||
continue
|
||||
}
|
||||
utils.WaitForRateLimit(s, userResp)
|
||||
|
||||
if userDetails.GetFollowers() < maxFollowersForFollowing {
|
||||
currentScore += pointPerCriterion
|
||||
}
|
||||
|
||||
followsTargetBack, checkErr := checkIfUserFollows(s, loginName, s.Target)
|
||||
if checkErr != nil {
|
||||
continue
|
||||
} else if followsTargetBack {
|
||||
currentScore += pointPerCriterion
|
||||
}
|
||||
|
||||
if currentScore > 0 {
|
||||
response = append(response, CloseFriendResult{
|
||||
Login: loginName,
|
||||
Score: currentScore,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(response) == 0 {
|
||||
return
|
||||
} else {
|
||||
sort.Slice(response, func(i, j int) bool {
|
||||
return response[i].Score > response[j].Score
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// checkIfUserFollows checks if sourceUserLogin follows targetUserLogin.
|
||||
func checkIfUserFollows(s github_recon_settings.Settings, sourceUserLogin, targetUserLogin string) (bool, error) {
|
||||
isFollowing, resp, err := s.Client.Users.IsFollowing(s.Ctx, sourceUserLogin, targetUserLogin)
|
||||
if err != nil {
|
||||
s.Logger.Warn("Error checking if user follows target",
|
||||
"source_user_checking", sourceUserLogin,
|
||||
"target_user_to_check", targetUserLogin,
|
||||
"err", err)
|
||||
if resp != nil {
|
||||
utils.WaitForRateLimit(s, resp)
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
utils.WaitForRateLimit(s, resp)
|
||||
}
|
||||
return isFollowing, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package recon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
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 {
|
||||
name := item.Commit.GetAuthor().GetName()
|
||||
email := item.Commit.GetAuthor().GetEmail()
|
||||
if utils.SkipResult(name, email) {
|
||||
continue
|
||||
}
|
||||
if _, seen := results[name+" - "+email]; !seen {
|
||||
author := CommitResult{
|
||||
Name: name,
|
||||
Email: email,
|
||||
Occurrences: 1,
|
||||
FirstFoundIn: item.GetRepository().Owner.GetLogin() + "/" + item.GetRepository().
|
||||
GetName(),
|
||||
}
|
||||
results[name+" - "+email] = author
|
||||
} else {
|
||||
result := results[name+" - "+email]
|
||||
result.Occurrences++
|
||||
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 {
|
||||
s.Logger.Error("Failed to fetch commits", "err", err, "date", date)
|
||||
}
|
||||
}
|
||||
|
||||
for _, result := range results {
|
||||
response = append(response, result)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
package recon
|
||||
|
||||
import (
|
||||
"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 []AuthorOccurrence
|
||||
|
||||
type AuthorOccurrence struct {
|
||||
Name string
|
||||
Levenshtein int
|
||||
Email string
|
||||
FoundIn []string
|
||||
}
|
||||
|
||||
type Emails []EmailOccurrence
|
||||
|
||||
type EmailOccurrence struct {
|
||||
Email string
|
||||
Levenshtein int
|
||||
FoundIn []string
|
||||
}
|
||||
|
||||
type DeepScanResult struct {
|
||||
Authors []AuthorOccurrence
|
||||
Emails []EmailOccurrence
|
||||
}
|
||||
|
||||
type Repositorie struct {
|
||||
Repository string
|
||||
Owner string
|
||||
Name string
|
||||
Size int
|
||||
}
|
||||
|
||||
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.IsDir() {
|
||||
if strings.Contains(path, ".git/logs/") {
|
||||
return nil
|
||||
}
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 []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,
|
||||
Levenshtein: utils.LevenshteinDistance(username, strings.SplitN(email, "@", 2)[0]),
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// r.PrintTitle("📦 Repositories")
|
||||
|
||||
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)
|
||||
|
||||
authorOccurrences := []AuthorOccurrence{}
|
||||
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, AuthorOccurrence{
|
||||
Name: authorName,
|
||||
Email: authorEmail,
|
||||
FoundIn: []string{repoIdentifier},
|
||||
Levenshtein: utils.LevenshteinDistance(s.Target, authorName),
|
||||
})
|
||||
mapAuthorToIndex[trimmedLine] = len(authorOccurrences) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
slices.SortFunc(authorOccurrences, func(a, b AuthorOccurrence) int {
|
||||
if a.Levenshtein != b.Levenshtein {
|
||||
return a.Levenshtein - b.Levenshtein
|
||||
}
|
||||
return 1
|
||||
})
|
||||
|
||||
authors := []AuthorOccurrence{}
|
||||
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 EmailOccurrence) int {
|
||||
if a.Levenshtein != b.Levenshtein {
|
||||
return a.Levenshtein - b.Levenshtein
|
||||
}
|
||||
return 1
|
||||
})
|
||||
|
||||
emails := []EmailOccurrence{}
|
||||
for _, email := range emailsFound {
|
||||
if email.Levenshtein > s.MaxDistance {
|
||||
continue
|
||||
}
|
||||
emails = append(emails, email)
|
||||
}
|
||||
|
||||
response.Authors = authors
|
||||
response.Emails = emails
|
||||
|
||||
return
|
||||
}
|
||||
@@ -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,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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
user, resp, err := s.Client.Users.Get(s.Ctx, s.Target)
|
||||
if resp.StatusCode == 404 {
|
||||
s.Logger.Fatal("User not found with username")
|
||||
}
|
||||
if err != nil {
|
||||
s.Logger.Fatal("Failed to fetch user's information", "err", err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
module github.com/anotherhadi/github-recon
|
||||
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/charmbracelet/log v0.4.2
|
||||
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 (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // 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
|
||||
)
|
||||
@@ -0,0 +1,58 @@
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
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/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||
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/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/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM=
|
||||
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/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/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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/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/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/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/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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,148 @@
|
||||
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
|
||||
HideAvatar bool
|
||||
|
||||
// Internal
|
||||
Client *github.Client
|
||||
Logger *log.Logger
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
func GetSettings() (settings Settings) {
|
||||
//// 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", "null", "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", false, "Enable deep scan (clone repos, regex search, analyse licenses, etc.)")
|
||||
flag.IntVar(
|
||||
&settings.MaxRepoSize,
|
||||
"max-size",
|
||||
150,
|
||||
"Limit the size of repositories to scan (in MB) (only for deep scan)",
|
||||
)
|
||||
flag.StringSliceVarP(
|
||||
&settings.ExcludedRepos,
|
||||
"exclude-repo",
|
||||
"e",
|
||||
[]string{},
|
||||
"Exclude repos from deep scan (comma-separated list, only for deep scan)",
|
||||
)
|
||||
flag.BoolVarP(
|
||||
&settings.Refresh,
|
||||
"refresh",
|
||||
"r",
|
||||
false,
|
||||
"Refresh the cache (only for deep scan)",
|
||||
)
|
||||
flag.BoolVarP(
|
||||
&settings.ShowSource,
|
||||
"show-source",
|
||||
"s",
|
||||
false,
|
||||
"Show where the information (authors, emails, etc) were found (only for deep scan)",
|
||||
)
|
||||
flag.IntVarP(
|
||||
&settings.MaxDistance,
|
||||
"max-distance",
|
||||
"m",
|
||||
20,
|
||||
"Maximum Levenshtein distance for matching usernames & emails (only for deep scan)",
|
||||
)
|
||||
|
||||
flag.BoolVarP(&settings.Silent, "silent", "S", false, "Suppress all non-essential output")
|
||||
flag.BoolVarP(&settings.HideAvatar, "hide-avatar", "h", false, "Hide the avatar in the output")
|
||||
flag.StringVarP(&settings.JsonOutput, "json", "j", "", "Write results to specified JSON file")
|
||||
|
||||
//// Parse
|
||||
flag.Parse()
|
||||
|
||||
//// Setup
|
||||
settings.Client = github.NewClient(nil)
|
||||
settings.Logger = log.NewWithOptions(os.Stderr, log.Options{
|
||||
ReportCaller: false,
|
||||
ReportTimestamp: false,
|
||||
})
|
||||
settings.Ctx = context.Background()
|
||||
|
||||
//// 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) < 1 {
|
||||
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, " ") {
|
||||
settings.Logger.Fatal("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 = getToken()
|
||||
}
|
||||
|
||||
if settings.Token == "null" {
|
||||
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,39 @@
|
||||
package github_recon_settings
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
+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 == "dependabot[bot]" || name == "github-actions" {
|
||||
return true
|
||||
}
|
||||
if email == "github-actions[bot]@users.noreply.github.com" ||
|
||||
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