From 9b914e14ba227609e63c305c690b24054befa33f Mon Sep 17 00:00:00 2001 From: Hadi <112569860+anotherhadi@users.noreply.github.com> Date: Sat, 23 Aug 2025 15:08:50 +0200 Subject: [PATCH] Add more score when two users are in the same orgs #7 Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com> --- github-recon/username/close-friends.go | 134 +++++++++++++++---------- 1 file changed, 83 insertions(+), 51 deletions(-) diff --git a/github-recon/username/close-friends.go b/github-recon/username/close-friends.go index 243188f..ef3d1ec 100644 --- a/github-recon/username/close-friends.go +++ b/github-recon/username/close-friends.go @@ -1,11 +1,13 @@ package recon import ( + "errors" "fmt" "sort" github_recon_settings "github.com/anotherhadi/github-recon/settings" "github.com/anotherhadi/github-recon/utils" + "github.com/google/go-github/v72/github" ) type CloseFriendsResult []CloseFriendResult @@ -16,51 +18,56 @@ type CloseFriendResult struct { } const ( - maxFollowingForTarget = 50 - maxFollowersForFollowing = 20 - pointPerCriterion = 1 + maxTargetFollowing = 50 + maxFollowersForCandidate = 20 + pointsPerCondition = 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) +// CloseFriends returns a list of "close friends" for the target user. +// A candidate is considered closer if: +// 1. The target follows fewer than 50 people. +// 2. The candidate has fewer than 20 followers (+1 point). +// 3. The candidate follows the target back (+1 point). +// 4. The candidate shares at least one organization with the target (+1 point). +func CloseFriends(s github_recon_settings.Settings) (results CloseFriendsResult) { + targetFollowing, resp, err := s.Client.Users.ListFollowing(s.Ctx, s.Target, nil) if err != nil { - s.Logger.Error("Failed to fetch user's following list", "err", err) + s.Logger.Error("Failed to fetch target'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))) + targetOrgs, err := getOrgs(s, s.Target) + if err != nil { + s.Logger.Error("Failed to fetch target's organizations", "err", err) + targetOrgs = []*github.Organization{} + } + + if len(targetFollowing) >= maxTargetFollowing { + s.Logger.Info("Skipping close friends check", + "reason", + fmt.Sprintf("Target follows %d or more users (limit: %d)", len(targetFollowing), maxTargetFollowing), + ) return } - if len(following) == 0 { + if len(targetFollowing) == 0 { return } - for _, userBeingFollowedByTarget := range following { - loginName := userBeingFollowedByTarget.GetLogin() - if loginName == "" { + for _, candidate := range targetFollowing { + candidateLogin := candidate.GetLogin() + if candidateLogin == "" { continue } - currentScore := 0 + score := 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, + candidateDetails, userResp, err := s.Client.Users.Get(s.Ctx, candidateLogin) + if err != nil { + s.Logger.Warn("Failed to fetch details for candidate", + "candidate", candidateLogin, + "err", err, ) if userResp != nil { utils.WaitForRateLimit(s, userResp) @@ -69,52 +76,77 @@ func CloseFriends(s github_recon_settings.Settings) (response CloseFriendsResult } utils.WaitForRateLimit(s, userResp) - if userDetails.GetFollowers() < maxFollowersForFollowing { - currentScore += pointPerCriterion + // Condition: candidate has few followers + if candidateDetails.GetFollowers() < maxFollowersForCandidate { + score += pointsPerCondition } - followsTargetBack, checkErr := checkIfUserFollows(s, loginName, s.Target) - if checkErr != nil { - continue - } else if followsTargetBack { - currentScore += pointPerCriterion + // Condition: candidate follows target back + followsBack, err := checkIfUserFollows(s, candidateLogin, s.Target) + if err == nil && followsBack { + score += pointsPerCondition } - if currentScore > 0 { - response = append(response, CloseFriendResult{ - Username: loginName, - Score: currentScore, + // Condition: same organization + candidateOrgs, _ := getOrgs(s, candidateLogin) + if isInSameOrg(targetOrgs, candidateOrgs) { + score += pointsPerCondition + } + + // Add candidate if they matched at least one condition + if score > 0 { + results = append(results, CloseFriendResult{ + Username: candidateLogin, + Score: score, }) } } - if len(response) == 0 { - return - } else { - sort.Slice(response, func(i, j int) bool { - return response[i].Score > response[j].Score + if len(results) > 0 { + sort.Slice(results, func(i, j int) bool { + return results[i].Score > results[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) +// checkIfUserFollows checks if sourceUser follows targetUser. +func checkIfUserFollows(s github_recon_settings.Settings, sourceUser, targetUser string) (bool, error) { + isFollowing, resp, err := s.Client.Users.IsFollowing(s.Ctx, sourceUser, targetUser) if err != nil { s.Logger.Warn("Error checking if user follows target", - "source_user_checking", sourceUserLogin, - "target_user_to_check", targetUserLogin, - "err", err) + "source", sourceUser, + "target", targetUser, + "err", err, + ) if resp != nil { utils.WaitForRateLimit(s, resp) } return false, err } - if resp != nil { utils.WaitForRateLimit(s, resp) } return isFollowing, nil } + +func getOrgs(s github_recon_settings.Settings, user string) ([]*github.Organization, error) { + orgs, resp, err := s.Client.Organizations.List(s.Ctx, user, nil) + if err != nil { + return nil, errors.New("failed to fetch organizations for user") + } + utils.WaitForRateLimit(s, resp) + return orgs, nil +} + +func isInSameOrg(orgsA, orgsB []*github.Organization) bool { + for _, orgA := range orgsA { + for _, orgB := range orgsB { + if orgA.GetLogin() == orgB.GetLogin() { + return true + } + } + } + return false +}