mirror of
https://github.com/anotherhadi/github-recon.git
synced 2026-10-05 19:08:24 +02:00
Init v2, rewrite
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user