Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 97 additions & 38 deletions gitfive/lib/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <email_address>")
exit(
"[-] Please give a valid email address.\nExample : gitfive email <email_address>"
)
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)
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 <email_address>"
)
trio.run(email_light_mod.hunt, args.email_address, args.json)
143 changes: 143 additions & 0 deletions gitfive/modules/email_light_mod.py
Original file line number Diff line number Diff line change
@@ -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)