diff --git a/.gitignore b/.gitignore index 68bc17f..32a6bb4 100644 --- a/.gitignore +++ b/.gitignore @@ -138,6 +138,11 @@ venv.bak/ # mkdocs documentation /site +# ignores program data that has been generated from tests +input_file.json +output_file.json +repo_count.txt + # mypy .mypy_cache/ .dmypy.json @@ -158,3 +163,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# ignore the venv dir +*patchpirate_venv \ No newline at end of file diff --git a/LICENSE b/LICENSE index c2b3896..3cd04c6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Server-188 +Copyright (c) 2025, Br0k3nPix3l (https://github.com/FailurePoint) and Gratonic (https://github.com/Gratonic) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index b317d9f..c4d2b35 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,31 @@ Github Recon/OSINT Software for finding personal email adresses . PatchPirate is a Recon/OSINT software for mass processing GitHub commit and repo data to find unintentionally exposed private email addresses from a GitHub username or account via the GitHub API. -## Instalation and use +## Installation and Use Requires python 3.11+ with pip. -To install PatchPirate on Linux run: `git clone https://github.com/FailurePoint/PatchPirate.git & cd PatchPirate & python3 -m pip install -r requirements.txt` -To run it, from the folder it is installed in run: `python3 patchpirate.py` + +To install PatchPirate on Linux run: + +`git clone https://github.com/FailurePoint/PatchPirate.git & cd PatchPirate & python3 -m pip install -r requirements.txt` + +Make sure to install the dependencies using this command from the program directory: + +`pip install -r requirements.txt` + +To run it, from the folder it is installed in run: + +`python3 patchpirate.py` ## Usage and rate limmits -Usage is self obvious... please dont ask me how to use it... just use your brain for 30 secconds. it's not that hard. +Usage is self obvious... please dont ask us how to use it... just use your brain for 30 secconds. it's not that hard. GitHub imposes a rate limmit on unauthenticated users for API access of 60 requests/hr. the usage of the API varies per scan, but is roughly the same as the amount of public repos the target maintains. -If 60 requests is not going to be enough, you can authenticated using a Personal Acess Token (PAT) and unlock up to 5000 requests an hour. [How do I get one](https://www.geeksforgeeks.org/how-to-generate-personal-access-token-in-github/)? +If 60 requests is not going to be enough, you can run an authenticated scan using a Personal Access Token (PAT) and unlock up to 5000 requests an hour. + +## How do I get a personal access token for an authenticated scan? + +https://www.geeksforgeeks.org/how-to-generate-personal-access-token-in-github/ ## Screenshots ![Menu](Screenshots/menu.png) diff --git a/data/input_file.json b/data/input_file.json new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/data/input_file.json @@ -0,0 +1 @@ + diff --git a/data/output_file.json b/data/output_file.json new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/data/output_file.json @@ -0,0 +1 @@ + diff --git a/data/repo_count.txt b/data/repo_count.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/data/repo_count.txt @@ -0,0 +1 @@ + diff --git a/patchpirate.py b/patchpirate.py index bbbfba1..ca5f9a4 100644 --- a/patchpirate.py +++ b/patchpirate.py @@ -1,7 +1,27 @@ -import requests -from halo import Halo -from colorama import Fore, init -from datetime import datetime +# :: Program Information :: # + +""" +Program Name: PatchPirate +Description: A brief description of what your program does. +Authors: Br0k3nPix3l (https://github.com/FailurePoint) and Gratonic (https://github.com/Gratonic) +License: MIT +Date: 4/19/2025 +Version: 1.1.0 +""" + +# :: Imports :: # + +from colorama import Fore, init # Copywrite (c) 2013-2025, Jonathan Hartley (https://github.com/tartley) +from halo import Halo # Copywrite (c) 2016-2025, Singh +import piratescanner # Copywrite (c) 2025, Gratonic (https://github.com/Gratonic) +import json +import os + +# :: Global Variables :: # + +username = "" + +# :: Functionality :: # # Initialize colorama with auto reset init(autoreset=True) @@ -9,7 +29,6 @@ # Initialize loading indicator working_indicator = Halo(text=f'Searching user commits...', spinner='pong') - # program banner banner = r""" ______ _ ______ _ @@ -19,100 +38,99 @@ | | / ___ | | |( (___| | | | | | | | / ___ | | |_| ____| |_| \_____| \__)____)_| |_|_| |_|_| \_____| \__)_____) - BY: ┳┓ ┏┓┓ ┏┓ ┏┓• ┏┓┓ - ┣┫┏┓┃┫┃┏ ┫┏┓┃┃┓┓┏ ┫┃ - ┻┛┛ ┗┛┛┗┗┛┛┗┣┛┗┛┗┗┛┗ Version: 1.0.0 + BY: ┳┓ ┏┓┓ ┏┓ ┏┓• ┏┓┓ ┏┓ • + ┣┫┏┓┃┫┃┏ ┫┏┓┃┃┓┓┏ ┫┃ ┃┓┏┓┏┓╋┏┓┏┓┓┏ + ┻┛┛ ┗┛┛┗┗┛┛┗┣┛┗┛┗┗┛┗ and ┗┛┛ ┗┻┗┗┛┛┗┗┗ Version: 1.1.0 ----------------------------------------------------------------- """ -# Display program banner at top +# display program banner at top of the terminal print(f"{Fore.RED}{banner}") -# Optionally use a personal access token for higher rate limits -GITHUB_TOKEN = input(f"{Fore.YELLOW}Enter GitHub Personal Access Token (or press Enter to skip): ").strip() -HEADERS = {'Authorization': f'token {GITHUB_TOKEN}'} if GITHUB_TOKEN else {} - -# Fetch all commits authored by the user across their repositories -def get_user_commits(username): - commits = [] - repos = [] - - page = 1 - while True: - url = f"https://api.github.com/users/{username}/repos?page={page}&per_page=100" - response = requests.get(url, headers=HEADERS) - if response.status_code == 403: - handle_rate_limit(response) - elif response.status_code != 200: - raise Exception(f"Error fetching repos: {response.status_code}") - data = response.json() - if not data: - break - repos.extend(data) - page += 1 - - print(f"{Fore.BLUE}Found {len(repos)} repositories for user {Fore.RED}{username}{Fore.BLUE}.") +def clear_data_dir(): + os.remove("./data/input_file.json") + os.remove("./data/output_file.json") + os.remove("./data/repo_count.txt") +def collect_user_input(): + # loads the username variable into the function because it is modified and will be used later + global username + + # prompts the user for their target GitHub user and gives them the option to use an API token + username = input(f"{Fore.GREEN}Enter GitHub username: ").strip() + GITHUB_TOKEN = input(f"{Fore.YELLOW}Enter GitHub Personal Access Token (or press Enter to skip): ").strip() + # stores the input as a dictionary so it converts nicely to JSON, GITHUB_TOKEN is null (or None in python) if one was not given + user_input = { + "username": username, + "github_token": GITHUB_TOKEN if GITHUB_TOKEN else None + } + # dumps the input data as JSON into a JSON input_file stored under ./data + with open("./data/input_file.json", "w") as input_file: + json.dump(user_input, input_file, indent=4) + +def get_user_commits(): + # prints a message letting the user know that the program has began scanning + print(f"{Fore.GREEN}Starting the scanner") + + # starts the working indicator working_indicator.start() - for repo in repos: - repo_name = repo['name'] - owner = repo['owner']['login'] - - page = 1 - while True: - url = f"https://api.github.com/repos/{owner}/{repo_name}/commits?author={username}&page={page}&per_page=100" - response = requests.get(url, headers=HEADERS) - if response.status_code == 403: - handle_rate_limit(response) - elif response.status_code != 200: - break - data = response.json() - if not data: - break - for commit in data: - commits.append({ - 'repo': repo_name, - 'message': commit['commit']['message'], - 'url': commit['html_url'], - 'date': commit['commit']['author']['date'], - 'sha': commit['sha'][:7], - 'email': commit['commit']['author']['email'] - }) - page += 1 + + # scans the targeted users github and stores the output has JSON and Text files stored under ./data + # NOTE: This function was written in rust + piratescanner.get_user_commits_sync() + + # stops the working indicator and makes it disappear working_indicator.stop() - return commits - -# Handle GitHub rate limiting errors -def handle_rate_limit(response): - reset_timestamp = int(response.headers.get('X-RateLimit-Reset', 0)) - reset_time = datetime.fromtimestamp(reset_timestamp).strftime('%Y-%m-%d %H:%M:%S') - remaining = response.headers.get('X-RateLimit-Remaining', '0') - print(f"\n{Fore.RED}GitHub API rate limit exceeded.") - print(f"{Fore.YELLOW}Remaining requests: {remaining}") - print(f"{Fore.YELLOW}Rate limit resets at: {Fore.CYAN}{reset_time}") - raise Exception("Rate limit hit. Please wait for cooldown or use a personal access token.") -if __name__ == "__main__": - username = input(f"{Fore.GREEN}Enter GitHub username: ").strip() +def analyse_and_display_output(): + # opens the text repo_count file and collects the information related to the toal repository amount + with open("./data/repo_count.txt", "r") as repo_count_file: + repo_count = repo_count_file.readlines()[0].strip("\n") + + # displays the total amount of repos found + print(f"{Fore.BLUE}Found {repo_count} repositories for user {Fore.RED}{username}{Fore.BLUE}.") + + # opens the JSON output file and collects the discovered information/data + with open("./data/output_file.json", "r") as output_file: + user_commits = json.load(output_file) + + # displays the total amount of commits found + print(f"{Fore.BLUE}Total commits found: {Fore.RED}{len(user_commits)}") + # prints a line so things look a little cleaner + print("---------------------------------------------------------------------") + + # used to store the non-duplicate email address(es) email_addresses = set() + # used to store the obfuscated (AKA no-reply) email address obfuscated = "" - - try: - user_commits = get_user_commits(username) - print(f"{Fore.BLUE}Total commits found: {Fore.RED}{len(user_commits)}") - print("---------------------------------------------------------------------") + # attempts to display the collected information/data + try: for commit in user_commits: - email = commit['email'] + email = commit["email"] if email: if email.endswith("noreply.github.com"): obfuscated = email else: email_addresses.add(email) + print(f"\nObfucated noreply address: {Fore.GREEN}{obfuscated}") - print("Email addresses found:") + print("Email address(es) found:") for email in email_addresses: print(f"{Fore.GREEN}{email}") - except Exception as e: - print(f"An error occurred: {e}") + print(f"An unexpected error has occurred: {e}") + + +if __name__ == "__main__": + # clears the data directory if it has data, ignores this step if it doesn't + try: + clear_data_dir() + except: + pass + # collects the user input and stores it as JSON under ./data + collect_user_input() + # runs the scanner and stores the output as JSON under ./data + get_user_commits() + # analyzes and displays the collected sensitive information/data + analyse_and_display_output() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e5b51aa..9855a53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ # requirements.txt +piratescanner certifi==2025.1.31 charset-normalizer==3.4.1 colorama==0.4.6 @@ -10,4 +11,4 @@ requests==2.32.3 six==1.17.0 spinners==0.0.24 termcolor==3.0.1 -urllib3==2.3.0 +urllib3==2.3.0 \ No newline at end of file