From 6225e7cf22239a42fcca175be8c73ec46dc97921 Mon Sep 17 00:00:00 2001 From: Sidhu <54258853+sidhu-patil@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:57:49 +0530 Subject: [PATCH] adding: support for faster & light mail to github profile --- gitfive/lib/cli.py | 135 +++++++++++++++++++-------- gitfive/modules/email_light_mod.py | 143 +++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+), 38 deletions(-) create mode 100644 gitfive/modules/email_light_mod.py diff --git a/gitfive/lib/cli.py b/gitfive/lib/cli.py index d0d967e..d30c2ed 100644 --- a/gitfive/lib/cli.py +++ b/gitfive/lib/cli.py @@ -3,64 +3,123 @@ def parse_args(): - parser = argparse.ArgumentParser('gitfive') - subparsers = parser.add_subparsers(dest='command') - - login_parser = subparsers.add_parser('login', help='Let GitFive authenticate to GitHub.') - login_parser.add_argument('--clean', action='store_true', help="Clear credentials and session local files.") - - user_parser = subparsers.add_parser('user', help='Track down a GitHub user by its username.') - user_parser.add_argument(dest="username", - action='store', - type=str, - help="GitHub's username of the target") - user_parser.add_argument('--json', type=str, help="File to write the JSON output to") - - email_parser = subparsers.add_parser('email', help='Track down a GitHub user by its email address.') - email_parser.add_argument(dest="email_address", - action='store', - type=str, - help="GitHub's email address of the target") - email_parser.add_argument('--json', type=str, help="File to write the JSON output to") - - emails_parser = subparsers.add_parser('emails', help='Find GitHub usernames of a given list of email addresses.') - emails_parser.add_argument(dest="emails_file", - action='store', - type=str, - help="File containing a list of email adresses") - emails_parser.add_argument('--json', type=str, help="File to write the JSON output to") - emails_parser.add_argument('-t', type=str, help="GitHub's username of the target") - - light_parser = subparsers.add_parser('light', help='Quickly find emails addresses from a GitHub username.') - light_parser.add_argument(dest="username", - action='store', - type=str, - help="GitHub's username of the target") - - args = parser.parse_args(args=None if sys.argv[1:] else ['--help']) + parser = argparse.ArgumentParser("gitfive") + subparsers = parser.add_subparsers(dest="command") + + login_parser = subparsers.add_parser( + "login", help="Let GitFive authenticate to GitHub." + ) + login_parser.add_argument( + "--clean", + action="store_true", + help="Clear credentials and session local files.", + ) + + user_parser = subparsers.add_parser( + "user", help="Track down a GitHub user by its username." + ) + user_parser.add_argument( + dest="username", + action="store", + type=str, + help="GitHub's username of the target", + ) + user_parser.add_argument( + "--json", type=str, help="File to write the JSON output to" + ) + + email_parser = subparsers.add_parser( + "email", help="Track down a GitHub user by its email address." + ) + email_parser.add_argument( + dest="email_address", + action="store", + type=str, + help="GitHub's email address of the target", + ) + email_parser.add_argument( + "--json", type=str, help="File to write the JSON output to" + ) + + emails_parser = subparsers.add_parser( + "emails", help="Find GitHub usernames of a given list of email addresses." + ) + emails_parser.add_argument( + dest="emails_file", + action="store", + type=str, + help="File containing a list of email adresses", + ) + emails_parser.add_argument( + "--json", type=str, help="File to write the JSON output to" + ) + emails_parser.add_argument("-t", type=str, help="GitHub's username of the target") + + light_parser = subparsers.add_parser( + "light", help="Quickly find emails addresses from a GitHub username." + ) + light_parser.add_argument( + dest="username", + action="store", + type=str, + help="GitHub's username of the target", + ) + + email_light_parser = subparsers.add_parser( + "email_light", help="Get basic information of a GitHub user by its email address." + ) + email_light_parser.add_argument( + dest="email_address", + action="store", + type=str, + help="GitHub's email address of the target", + ) + email_light_parser.add_argument( + "--json", type=str, help="File to write the JSON output to" + ) + + args = parser.parse_args(args=None if sys.argv[1:] else ["--help"]) import trio + match args.command: case "login": from gitfive.modules import login_mod + trio.run(login_mod.check_and_login, args.clean) case "user": from gitfive.modules import username_mod + if not args.username: exit("[-] Please give a valid username.\nExample : gitfive user mxrch") trio.run(username_mod.hunt, args.username, args.json) case "email": from gitfive.modules import email_mod + if not args.email_address: - exit("[-] Please give a valid email address.\nExample : gitfive email ") + exit( + "[-] Please give a valid email address.\nExample : gitfive email " + ) trio.run(email_mod.hunt, args.email_address, args.json) case "emails": from gitfive.modules import emails_mod + if not args.emails_file: - exit("[-] Please give a valid file.\nExample : gitfive emails ~/Desktop/my_emails_list.txt") + exit( + "[-] Please give a valid file.\nExample : gitfive emails ~/Desktop/my_emails_list.txt" + ) trio.run(emails_mod.hunt, args.emails_file, args.json, args.t) case "light": from gitfive.modules import light_mod + if not args.username: exit("[-] Please give a valid username.\nExample : gitfive light mxrch") - trio.run(light_mod.hunt, args.username) \ No newline at end of file + trio.run(light_mod.hunt, args.username) + case "email_light": + from gitfive.modules import email_light_mod + + if not args.email_address: + exit( + "[-] Please give a valid email address.\nExample : gitfive email_light " + ) + trio.run(email_light_mod.hunt, args.email_address, args.json) diff --git a/gitfive/modules/email_light_mod.py b/gitfive/modules/email_light_mod.py new file mode 100644 index 0000000..010494e --- /dev/null +++ b/gitfive/modules/email_light_mod.py @@ -0,0 +1,143 @@ +from gitfive.lib import metamon, commits, close_friends +from gitfive.lib.objects import GitfiveRunner +import json +from pathlib import Path +import trio +import contextlib +import os +import sys + + +async def get_list_silent(runner: GitfiveRunner): + req = await runner.as_client.get( + f"https://github.com/{runner.target.username}?tab=repositories" + ) + from bs4 import BeautifulSoup + from math import ceil + + body = BeautifulSoup(req.text, "html.parser") + total_repos = runner.target.nb_public_repos + + to_request = range(1, ceil(total_repos / 30) + 1) if total_repos > 0 else [] + + repos = [] + if to_request: + from gitfive.lib.repos import fetch_repos_page + + async with trio.open_nursery() as nursery: + for page in to_request: + if page == 1: + nursery.start_soon(fetch_repos_page, runner, page, repos, body) + else: + nursery.start_soon(fetch_repos_page, runner, page, repos) + + main_languages = [x["main_language"] for x in repos if x.get("main_language")] + languages_stats = {} + if main_languages: + languages_stats = { + x: round(main_languages.count(x) / len(main_languages) * 100, 2) + for x in set(main_languages) + } + + runner.target.repos = repos + runner.target.languages_stats = dict( + sorted(languages_stats.items(), key=lambda item: item[1], reverse=True) + ) + + +async def _fetch_all_data(runner, username): + data = await runner.api.query(f"/users/{username}") + if data.get("message") == "Not Found": + return False + + runner.target._scrape(data) + + # get ext contribs + data1 = await runner.api.query( + f"/search/commits?q=author:{runner.target.username.lower()} -user:{runner.target.username.lower()}&per_page=100&sort=author-date&order=asc" + ) + if data1.get("message") != "Validation Failed": + from gitfive.lib.xray import analyze_ext_contribs + + await analyze_ext_contribs(runner) + + # get repos for language stats silently + await get_list_silent(runner) + + return True + + +async def hunt(email: str, json_file="", runner: GitfiveRunner = None): + if not runner: + runner = GitfiveRunner() + # Login but keep it silent optionally? We let login prints through so user knows it's logging in. + await runner.login() + + if json_file and not (parent := Path(json_file).parent).is_dir(): + exit(f"[-] The directory {parent} can't be found.") + + temp_repo_name, emails_index = await metamon.start(runner, [email]) + emails_accounts = await commits.scrape(runner, temp_repo_name, emails_index) + + # Delete temp repo earlier to be cleaner + from gitfive.lib import github + + await github.delete_repo(runner, temp_repo_name) + + if not emails_accounts: + exit("[-] Email isn't linked to a GitHub account.") + + print("[+] Target found !") + + username = [*emails_accounts.values()][0]["username"] + + runner.rc.print("\n✍️ PROFILE", style="navajo_white1") + runner.tmprinter.out("Loading profile...") + + with contextlib.redirect_stdout(open(os.devnull, "w")): + success = await _fetch_all_data(runner, username) + + if not success: + exit(f'\n[-] User "{username}" not found.') + + # Clear TMPrinter logs left over + runner.tmprinter.clear() + + out_dict = { + "username": runner.target.username, + "name": runner.target.name, + "id": runner.target.id, + "is_site_admin": runner.target.is_site_admin, + "is_hireable": getattr(runner.target, "is_hireable", False) + or getattr(runner.target, "hireable", False), + "company": runner.target.company, + "blog": runner.target.blog, + "location": runner.target.location, + "bio": runner.target.bio, + "twitter": runner.target.twitter, + "nb_public_repos": runner.target.nb_public_repos, + "nb_followers": runner.target.nb_followers, + "nb_following": runner.target.nb_following, + "created_at": ( + runner.target.created_at.strftime("%Y/%m/%d %H:%M:%S (UTC)") + if runner.target.created_at + else None + ), + "updated_at": ( + runner.target.updated_at.strftime("%Y/%m/%d %H:%M:%S (UTC)") + if runner.target.updated_at + else None + ), + "avatar_url": runner.target.avatar_url, + "is_default_avatar": runner.target.is_default_avatar, + "nb_ext_contribs": runner.target.nb_ext_contribs, + "repos": runner.target.repos, + "languages_stats": runner.target.languages_stats, + } + + output = json.dumps(out_dict, indent=4) + print(output) + + if json_file: + with open(json_file, "w", encoding="utf-8") as f: + f.write(output)