mirror of
https://github.com/anotherhadi/github-recon.git
synced 2026-10-05 10:58:25 +02:00
@@ -0,0 +1,71 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user