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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
4 changes: 2 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions data/input_file.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions data/output_file.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions data/repo_count.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

178 changes: 98 additions & 80 deletions patchpirate.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
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)

# Initialize loading indicator
working_indicator = Halo(text=f'Searching user commits...', spinner='pong')


# program banner
banner = r"""
______ _ ______ _
Expand All @@ -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()
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# requirements.txt

piratescanner
certifi==2025.1.31
charset-normalizer==3.4.1
colorama==0.4.6
Expand All @@ -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