Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
342a54caf1 | ||
|
|
1c8583132b | ||
|
|
61c0b12d8b | ||
|
|
9d88b66005 | ||
|
|
d1e9932967 | ||
|
|
595dcc5aac | ||
|
|
6a535ae856 | ||
|
|
61188fe29e | ||
|
|
cd6155b553 | ||
|
|
f064f80e47 | ||
|
|
dae2226905 | ||
|
|
2a92723dc8 | ||
|
|
af975a4e7a | ||
|
|
4f96419fbd | ||
|
|
774c8d19a8 | ||
|
|
04f3bd89cf | ||
|
|
3fbb8d8cbe | ||
|
|
e836413719 | ||
|
|
2ba4438f64 | ||
|
|
6c76b8fb39 | ||
|
|
803e3b8ebd | ||
|
|
6f29f633ea | ||
|
|
a9c62ebf36 | ||
|
|
73bdc542fa | ||
|
|
386e77f375 | ||
|
|
033e758e9e | ||
|
|
796e8e943c | ||
|
|
3125c45efb | ||
|
|
01d6115540 | ||
|
|
a7da4f442d | ||
|
|
f967134210 | ||
|
|
47b28b45e0 | ||
|
|
ca5245e0db | ||
|
|
d2c7ba7cd9 | ||
|
|
619ef8cf9f | ||
|
|
8364053f97 | ||
|
|
1f288d503f | ||
|
|
57911bd68f | ||
|
|
264048e4bf | ||
|
|
671fbdcaa4 | ||
|
|
5fc5af4157 | ||
|
|
8bc799c829 | ||
|
|
85cadefd50 | ||
|
|
646ea3cb51 | ||
|
|
3e18a3301a | ||
|
|
d4b595d79e | ||
|
|
0f247d1dd8 | ||
|
|
1b2c441237 | ||
|
|
2723c2f8dd | ||
|
|
7576fa64a1 |
34
README.md
@@ -1,6 +1,6 @@
|
||||

|
||||
|
||||
A framework for gathering open-source intelligence on GitHub users, repositories and organizations
|
||||
A framework for gathering open-source intelligence on GitHub users, repositories and organisations
|
||||
|
||||
[](https://github.com/bellingcat/octosuite/actions/workflows/python-publish.yml)
|
||||
[](https://github.com/bellingcat/octosuite/actions/workflows/codeql.yml)
|
||||
@@ -11,19 +11,19 @@ A framework for gathering open-source intelligence on GitHub users, repositories
|
||||

|
||||
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# Wiki
|
||||
[Refer to the Wiki](https://github.com/bellingcat/octosuite/wiki) for installation instructions, in addition to all other documentation.
|
||||
|
||||
# Features
|
||||
- [x] Fetches an organization's profile information
|
||||
- [x] Fetches an organisation's profile information
|
||||
- [x] Fetches an oganization's events
|
||||
- [x] Returns an organization's repositories
|
||||
- [x] Returns an organization's public members
|
||||
- [x] Returns an organisation's repositories
|
||||
- [x] Returns an organisation's public members
|
||||
- [x] Fetches a repository's information
|
||||
- [x] Returns a repository's contributors
|
||||
- [x] Returns a repository's languages
|
||||
@@ -33,24 +33,28 @@ A framework for gathering open-source intelligence on GitHub users, repositories
|
||||
- [x] Returns a list of files in a specified path of a repository
|
||||
- [x] Fetches a user's profile information
|
||||
- [x] Returns a user's gists
|
||||
- [x] Returns organizations that a user owns/belongs to
|
||||
- [x] Returns organisations that a user owns/belongs to
|
||||
- [x] Fetches a user's events
|
||||
- [x] Fetches a list of users followed by the target
|
||||
- [x] Fetches a user's followers
|
||||
- [x] Checks if user A follows user B
|
||||
- [x] Checks if user is a public member of an organizations
|
||||
- [x] Returns a user's subscriptions
|
||||
- [x] Checks if user is a public member of an organisations
|
||||
- [x] Gets a user's subscriptions
|
||||
- [x] Gets a user's events
|
||||
- [x] Searches users
|
||||
- [x] Searches repositories
|
||||
- [x] Searches topics
|
||||
- [x] Searches issues
|
||||
- [x] Searches commits
|
||||
- [x] Automatically logs network activity (.logs folder)
|
||||
- [x] User can view, read and delete logs
|
||||
- [x] Automatically logs network/user activity (.logs folder)
|
||||
- [x] User can manage logs (view, read, delete)
|
||||
- [x] Results can be saved to a .csv file (varies)
|
||||
- [x] User can manage csv files (view, read, delete)
|
||||
- [x] All the above can be used with command-line arguments (PyPI Package only)
|
||||
- [x] ...And more
|
||||
- [x] And more...
|
||||
|
||||
# TODO
|
||||
- [ ] Rewrite the GUI in Visual Basic .NET (in progress)
|
||||
|
||||
|
||||
## Note
|
||||
> Octosuite automatically logs network and user activity of each session, the logs are saved by date and time in the .logs folder
|
||||
@@ -59,6 +63,10 @@ A framework for gathering open-source intelligence on GitHub users, repositories
|
||||
# License
|
||||

|
||||
|
||||
# Credits
|
||||
* The code used for finding emails from usernames is taken from [Somdev Sangwan](https://github.com/s0md3v)'s [Zen](https://github.com/s0md3v/zen)
|
||||
|
||||
|
||||
# Donations
|
||||
If you like OctoSuite and would like to show support, you can Buy A Coffee for the developer using the button below
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
BIN
images/logo.png
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 207 KiB |
@@ -4,7 +4,7 @@ from octosuite.config import red, white, green, reset, Tree
|
||||
|
||||
# banner.py
|
||||
# This file holds the program's banner and version tag
|
||||
version_tag = "3.0.1"
|
||||
version_tag = "3.1.1"
|
||||
|
||||
|
||||
def banner():
|
||||
@@ -16,7 +16,7 @@ def banner():
|
||||
| |.----.| |_.-----.| __|.--.--.|__| |_.-----.
|
||||
| - || __|| _| _ ||__ || | || | _| -__|
|
||||
|_______||____||____|_____||_______||_____||__|____|_____|
|
||||
v{version_tag}#dev
|
||||
v{version_tag}
|
||||
{white}— Advanced Github {red}OSINT{white} Framework
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import psutil
|
||||
import platform
|
||||
import argparse
|
||||
@@ -6,6 +7,7 @@ from rich.text import Text
|
||||
from rich.table import Table
|
||||
from datetime import datetime
|
||||
from rich import print as xprint
|
||||
from rich.markdown import Markdown
|
||||
from rich.prompt import Prompt, Confirm
|
||||
|
||||
|
||||
@@ -24,14 +26,14 @@ def usage():
|
||||
octosuite --method user_repos --username <username>
|
||||
|
||||
|
||||
Get Organi[sz]ation Profile Info
|
||||
Get Organisation Profile Info
|
||||
-----------------------------
|
||||
octosuite --method org_profile --organization <organization_name>
|
||||
octosuite --method org_profile --organisation <organisation_name>
|
||||
|
||||
|
||||
Get Organi[sz]ation Repos
|
||||
-----------------------------
|
||||
octosuite --method org_repos --organization <organization_name>
|
||||
octosuite --method org_repos --organisation <organisation_name>
|
||||
|
||||
|
||||
Get Repo Profile Info
|
||||
@@ -124,7 +126,7 @@ def usage():
|
||||
|
||||
def create_parser():
|
||||
parser = argparse.ArgumentParser(description='OCTOSUITE: Advanced GitHub osint framework — by Richard Mwewa | https://about.me/rly0nheart', usage=usage())
|
||||
parser.add_argument('-m', '--method', help='method', choices=['user_profile', 'user_repos', 'user_gists', 'user_orgs', 'user_events',
|
||||
parser.add_argument('-m', '--method', help='method', choices=['user_email', 'user_profile', 'user_repos', 'user_gists', 'user_orgs', 'user_events',
|
||||
'user_subscriptions', 'user_following', 'user_followers', 'user_follows',
|
||||
'org_profile', 'org_repos', 'org_events', 'org_member',
|
||||
'repo_profile', 'repo_contributors', 'repo_stargazers', 'repo_forks',
|
||||
@@ -133,7 +135,7 @@ def create_parser():
|
||||
'clear_logs', 'view_csv', 'read_csv', 'delete_csv', 'clear_csv', 'about', 'author'])
|
||||
parser.add_argument('-u', '--username', help='username')
|
||||
parser.add_argument('-uB', '--username_b', help='username_B (used with user_follows)')
|
||||
parser.add_argument('-o', '--organization', '--organisation', help='organi[sz]ation name')
|
||||
parser.add_argument('-o', '--organisation', '--organization', help='organisation name')
|
||||
parser.add_argument('-r', '--repository', help='repository name')
|
||||
parser.add_argument('-p', '--path_name', help='path name (used with repo_path_contents)')
|
||||
parser.add_argument('-q', '--query', help='query (used with search methods)')
|
||||
@@ -145,6 +147,28 @@ def create_parser():
|
||||
return parser
|
||||
|
||||
|
||||
# Setup readline
|
||||
def setup_readline():
|
||||
if os.name == "nt":
|
||||
try:
|
||||
from pyreadline3 import Readline
|
||||
except ImportError:
|
||||
subprocess.run(['pip3', 'install', 'pyreadline3'], shell=False)
|
||||
readline = Readline()
|
||||
else:
|
||||
import readline
|
||||
|
||||
def completer(text, state):
|
||||
options = [i for i in commands if i.startswith(text)]
|
||||
if state < len(options):
|
||||
return options[state]
|
||||
else:
|
||||
return None
|
||||
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.set_completer(completer)
|
||||
|
||||
|
||||
parser = create_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ from octosuite.message_prefixes import PROMPT, WARNING, POSITIVE, NEGATIVE, INFO
|
||||
def log_org_profile(response):
|
||||
org_profile_fields = ['Profile photo', 'Name', 'Username', 'ID', 'Node ID', 'Email', 'About', 'Location', 'Blog',
|
||||
'Followers', 'Following', 'Twitter handle', 'Gists', 'Repositories', 'Account type',
|
||||
'Is verified?', 'Has organization projects?', 'Has repository projects?', 'Created at',
|
||||
'Is verified?', 'Has organisation projects?', 'Has repository projects?', 'Created at',
|
||||
'Updated at']
|
||||
org_profile_row = [response.json()['avatar_url'], response.json()['name'], response.json()['login'],
|
||||
response.json()['id'], response.json()['node_id'], response.json()['email'],
|
||||
response.json()['description'], response.json()['location'], response.json()['blog'],
|
||||
response.json()['followers'], response.json()['following'], response.json()['twitter_username'],
|
||||
response.json()['public_gists'], response.json()['public_repos'], response.json()['type'],
|
||||
response.json()['is_verified'], response.json()['has_organization_projects'],
|
||||
response.json()['is_verified'], response.json()['has_organisation_projects'],
|
||||
response.json()['has_repository_projects'], response.json()['created_at'],
|
||||
response.json()['updated_at']]
|
||||
|
||||
@@ -34,7 +34,7 @@ def log_org_profile(response):
|
||||
# Creating a .csv file of a user' profile
|
||||
def log_user_profile(response):
|
||||
user_profile_fields = ['Profile photo', 'Name', 'Username', 'ID', 'Node ID', 'Bio', 'Blog', 'Location', 'Followers',
|
||||
'Following', 'Twitter handle', 'Gists', 'Repositories', 'Organization', 'Is hireable?',
|
||||
'Following', 'Twitter handle', 'Gists', 'Repositories', 'organisation', 'Is hireable?',
|
||||
'Is site admin?', 'Joined at', 'Updated at']
|
||||
user_profile_row = [response.json()['avatar_url'], response.json()['name'], response.json()['login'],
|
||||
response.json()['id'], response.json()['node_id'], response.json()['bio'],
|
||||
@@ -184,12 +184,12 @@ def log_repo_contributors(contributor, repo_name):
|
||||
xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}")
|
||||
|
||||
|
||||
# Create .csv for organization' events
|
||||
def log_repo_events(event, organization):
|
||||
# Create .csv for organisation' events
|
||||
def log_repo_events(event, organisation):
|
||||
org_event_fields = ['ID', 'Type', 'Created at', 'Payload']
|
||||
org_event_row = [event['id'], event['type'], event['created_at'], event['payload']]
|
||||
|
||||
with open(os.path.join("output", f"{organization}_event_{event['id']}.csv"), 'w') as file:
|
||||
with open(os.path.join("output", f"{organisation}_event_{event['id']}.csv"), 'w') as file:
|
||||
write_csv = csv.writer(file)
|
||||
write_csv.writerow(org_event_fields)
|
||||
write_csv.writerow(org_event_row)
|
||||
@@ -198,8 +198,8 @@ def log_repo_events(event, organization):
|
||||
xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}")
|
||||
|
||||
|
||||
# Create .csv for organization' repositories
|
||||
def log_org_repos(repository, organization):
|
||||
# Create .csv for organisation' repositories
|
||||
def log_org_repos(repository, organisation):
|
||||
org_repo_fields = ['Name', 'ID', 'About', 'Forks', 'Stars', 'Watchers', 'License', 'Branch', 'Visibility',
|
||||
'Language(s)', 'Open issues', 'Topics', 'Homepage', 'Clone URL', 'SSH URL', 'Is fork?',
|
||||
'Is forkable?', 'Is private?', 'Is archived?', 'Is template?', 'Has wiki?', 'Has pages?',
|
||||
@@ -213,7 +213,7 @@ def log_org_repos(repository, organization):
|
||||
repository['has_projects'], repository['has_issues'], repository['has_downloads'],
|
||||
repository['pushed_at'], repository['created_at'], repository['updated_at']]
|
||||
|
||||
with open(os.path.join("output", f"{repository['name']}_repository_of_{organization}.csv"), 'w') as file:
|
||||
with open(os.path.join("output", f"{repository['name']}_repository_of_{organisation}.csv"), 'w') as file:
|
||||
write_csv = csv.writer(file)
|
||||
write_csv.writerow(org_repo_fields)
|
||||
write_csv.writerow(org_repo_row)
|
||||
@@ -335,13 +335,13 @@ def log_user_subscriptions(repository, username):
|
||||
xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}")
|
||||
|
||||
|
||||
# .csv for user organizations
|
||||
def log_user_orgs(organization, username):
|
||||
# .csv for user organisations
|
||||
def log_user_orgs(organisation, username):
|
||||
user_org_fields = ['Profile photo', 'Name', 'ID', 'Node ID', 'URL', 'About']
|
||||
user_org_row = [organization['avatar_url'], organization['login'], organization['id'], organization['node_id'],
|
||||
organization['url'], organization['description']]
|
||||
user_org_row = [organisation['avatar_url'], organisation['login'], organisation['id'], organisation['node_id'],
|
||||
organisation['url'], organisation['description']]
|
||||
|
||||
with open(os.path.join("output", f"{organization['login']}_{username}.csv"), 'w') as file:
|
||||
with open(os.path.join("output", f"{organisation['login']}_{username}.csv"), 'w') as file:
|
||||
write_csv = csv.writer(file)
|
||||
write_csv.writerow(user_org_fields)
|
||||
write_csv.writerow(user_org_row)
|
||||
|
||||
@@ -67,9 +67,10 @@ def user_command():
|
||||
user_cmd_table = Table(show_header=True, header_style=header_title)
|
||||
user_cmd_table.add_column("Command", style="dim")
|
||||
user_cmd_table.add_column("Description")
|
||||
user_cmd_table.add_row("email", "Return a target's email")
|
||||
user_cmd_table.add_row("profile", "Get a target's profile info")
|
||||
user_cmd_table.add_row("gists", "Return a users's gists")
|
||||
user_cmd_table.add_row("org", "Return organizations that a target belongs to/owns")
|
||||
user_cmd_table.add_row("orgs", "Return organisations that a target belongs to/owns")
|
||||
user_cmd_table.add_row("repos", "Return a target's repositories")
|
||||
user_cmd_table.add_row("events", "Return a target's events")
|
||||
user_cmd_table.add_row("follows", "Check if user(A) follows user(B)")
|
||||
@@ -86,13 +87,13 @@ def org_command():
|
||||
org_cmd_table = Table(show_header=True, header_style=header_title)
|
||||
org_cmd_table.add_column("Command", style="dim")
|
||||
org_cmd_table.add_column("Description")
|
||||
org_cmd_table.add_row("profile", "Get a target organization' profile info")
|
||||
org_cmd_table.add_row("repos", "Return a target organization' repositories")
|
||||
org_cmd_table.add_row("events", "Return a target organization' events")
|
||||
org_cmd_table.add_row("member", "Check if a specified user is a public member of the target organization")
|
||||
org_cmd_table.add_row("profile", "Get a target organisation' profile info")
|
||||
org_cmd_table.add_row("repos", "Return a target organisation' repositories")
|
||||
org_cmd_table.add_row("events", "Return a target organisation' events")
|
||||
org_cmd_table.add_row("member", "Check if a specified user is a public member of the target organisation")
|
||||
|
||||
syntax = f"{green}org:<command>{reset}"
|
||||
xprint(f"{usage_text.format(syntax, 'organization investigation(s)')}")
|
||||
xprint(f"{usage_text.format(syntax, 'organisation investigation(s)')}")
|
||||
xprint(org_cmd_table)
|
||||
|
||||
|
||||
@@ -158,7 +159,7 @@ def help_command():
|
||||
help_sub_cmd_table.add_column("Description")
|
||||
help_sub_cmd_table.add_row("csv", "List all csv management commands")
|
||||
help_sub_cmd_table.add_row("logs", "List all logs management commands")
|
||||
help_sub_cmd_table.add_row("org", "List all organization investigation commands")
|
||||
help_sub_cmd_table.add_row("org", "List all organisation investigation commands")
|
||||
help_sub_cmd_table.add_row("user", "List all users investigation commands")
|
||||
help_sub_cmd_table.add_row("repo", "List all repository investigation commands")
|
||||
help_sub_cmd_table.add_row("search", "List all target discovery commands")
|
||||
|
||||
@@ -14,7 +14,7 @@ file_downloading = "Downloading: {}"
|
||||
file_downloaded = "Downloaded: downloads/{}"
|
||||
info_not_found = "Information not found: {}, {}, {}"
|
||||
user_not_found = "User not found: @{}"
|
||||
org_not_found = "Organization not found: @{}"
|
||||
org_not_found = "organisation not found: @{}"
|
||||
repo_or_user_not_found = "Repository or User not found: {}, @{}"
|
||||
prompt_log_csv = "Would you like to log this output to a .csv file?"
|
||||
logged_to_csv = "Output logged: {}"
|
||||
|
||||
@@ -3,6 +3,7 @@ from octosuite.octosuite import * # I drifted away from the 'pythonic way' here
|
||||
|
||||
|
||||
def octosuite():
|
||||
setup_readline()
|
||||
try:
|
||||
run = Octosuite()
|
||||
path_finder()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!usr/bin/python
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
@@ -9,10 +10,10 @@ import requests
|
||||
import platform
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from octosuite.banner import version_tag, banner
|
||||
from octosuite.config import Tree, Text, Table, Prompt, Confirm, xprint, create_parser, args, red, white, green, yellow, header_title, reset
|
||||
from octosuite.message_prefixes import ERROR, WARNING, PROMPT, POSITIVE, NEGATIVE, INFO # wondering why I name all the variables instead of just using the * wildcard?, because it's the pythonic way lol
|
||||
# seriously now, the reason why I am doing this, is so that you know exactly what I am importing from a named module :)
|
||||
from octosuite.config import Tree, Text, Table, Prompt, Confirm, Markdown, xprint, create_parser, setup_readline, args, red, white, green, yellow, header_title, reset
|
||||
from octosuite.message_prefixes import ERROR, WARNING, PROMPT, POSITIVE, NEGATIVE, INFO
|
||||
from octosuite.helper import help_command, source_command, search_command, user_command, repo_command, \
|
||||
logs_command, csv_command, org_command, source, org, repo, user, search, logs, csv
|
||||
from octosuite.log_roller import ctrl_c, error, session_opened, session_closed, viewing_logs, viewing_csv, \
|
||||
@@ -25,26 +26,6 @@ from octosuite.csv_loggers import log_org_profile, log_user_profile, log_repo_pr
|
||||
log_commits_search
|
||||
|
||||
|
||||
if os.name == "nt":
|
||||
try:
|
||||
from pyreadline3 import Readline
|
||||
except ImportError:
|
||||
subprocess.run(['pip3', 'install', 'pyreadline3'])
|
||||
readline = Readline()
|
||||
else:
|
||||
import readline
|
||||
|
||||
def completer(text, state):
|
||||
options = [i for i in commands if i.startswith(text)]
|
||||
if state < len(options):
|
||||
return options[state]
|
||||
else:
|
||||
return None
|
||||
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.set_completer(completer)
|
||||
|
||||
|
||||
# path_finder()
|
||||
# This function is responsible for creating/checking the availability of the (.logs, output, downloads) folders,
|
||||
# enabling logging to automatically log network/user activity to a file, and logging the start of a session.
|
||||
@@ -72,15 +53,20 @@ def configure_logging():
|
||||
# if it does, it means the program is up-to-date.
|
||||
# If it doesn't match, notify the user about a new release
|
||||
def check_updates():
|
||||
global markdown_release_notes
|
||||
|
||||
response = requests.get("https://api.github.com/repos/bellingcat/octosuite/releases/latest").json()
|
||||
if response['tag_name'] == version_tag:
|
||||
pass
|
||||
else:
|
||||
xprint(f"[{green}UPDATE{reset}] A new release of Octosuite is available ({response['tag_name']}). Run 'pip install --upgrade octosuite' to get the updates.\n")
|
||||
raw_release_notes = response['body']
|
||||
markdown_release_notes = Markdown(raw_release_notes)
|
||||
xprint(f"[{green}UPDATE{reset}] A new release of Octosuite is available ({response['tag_name']}).\n")
|
||||
xprint(markdown_release_notes)
|
||||
|
||||
|
||||
def list_dir_and_files():
|
||||
os.system('dir' if os.name == 'nt' else 'ls')
|
||||
subprocess.call('cmd.exe /c dir' if os.name == "nt" else 'ls')
|
||||
|
||||
|
||||
# Delete a specified csv file
|
||||
@@ -188,23 +174,37 @@ def exit_session():
|
||||
def clear_screen():
|
||||
# Using 'cls' on Windows machines to clear the screen,
|
||||
# otherwise, use 'clear'
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
subprocess.call('cmd.exe /c cls' if os.name == "nt" else 'clear')
|
||||
|
||||
|
||||
def about():
|
||||
about_text = f"""
|
||||
OCTOSUITE © 2023 Richard Mwewa
|
||||
|
||||
An advanced and lightning fast framework for gathering open-source intelligence on GitHub users and organizations.
|
||||
|
||||
|
||||
Whats new in v{version_tag}?
|
||||
[{green}FIXED{reset}] Fixed a bug '[ERROR] An error occurred: can only concatenate str (not "NoneType") to str'
|
||||
An advanced and lightning fast framework for gathering open-source intelligence on GitHub users and organisations.
|
||||
|
||||
Read the wiki: https://github.com/bellingcat/octosuite/wiki
|
||||
GitHub REST API documentation: https://docs.github.com/rest
|
||||
|
||||
"""
|
||||
xprint(about_text)
|
||||
xprint(markdown_release_notes)
|
||||
|
||||
|
||||
def get_email_from_contributor(username, repo, contributor):
|
||||
response = requests.get(f"https://github.com/{username}/{repo}/commits?author={contributor}",
|
||||
auth=HTTPBasicAuth(username, '')).text
|
||||
latest_commit = re.search(rf'href="/{username}/{repo}/commit/(.*?)"', response)
|
||||
if latest_commit:
|
||||
latest_commit = latest_commit.group(1)
|
||||
else:
|
||||
latest_commit = 'dummy'
|
||||
commit_details = requests.get(f"https://github.com/{username}/{repo}/commit/{latest_commit}.patch",
|
||||
auth=HTTPBasicAuth(username, '')).text
|
||||
email = re.search(r'<(.*)>', commit_details)
|
||||
if email:
|
||||
email = email.group(1)
|
||||
return email
|
||||
|
||||
|
||||
class Octosuite:
|
||||
@@ -243,6 +243,7 @@ class Octosuite:
|
||||
("repo:issues", self.repo_issues),
|
||||
("repo:releases", self.repo_releases),
|
||||
("user", user),
|
||||
("user:email", self.get_user_email),
|
||||
("user:repos", self.user_repos),
|
||||
("user:gists", self.user_gists),
|
||||
("user:orgs", self.user_orgs),
|
||||
@@ -271,6 +272,7 @@ class Octosuite:
|
||||
|
||||
# Arguments map will be used to run Octosuite with argparse
|
||||
self.argument_map = [("user_profile", self.user_profile),
|
||||
("user_email", self.get_user_email),
|
||||
("user_repos", self.user_repos),
|
||||
("user_gists", self.user_gists),
|
||||
("user_orgs", self.user_orgs),
|
||||
@@ -315,12 +317,12 @@ class Octosuite:
|
||||
'sha': 'SHA',
|
||||
'html_url': 'URL'}
|
||||
|
||||
# Organization attributes
|
||||
# organisation attributes
|
||||
self.org_attrs = ['avatar_url', 'login', 'id', 'node_id', 'email', 'description', 'blog', 'location',
|
||||
'followers',
|
||||
'following', 'twitter_username', 'public_gists', 'public_repos', 'type', 'is_verified',
|
||||
'has_organization_projects', 'has_repository_projects', 'created_at', 'updated_at']
|
||||
# Organization attribute dictionary
|
||||
'has_organisation_projects', 'has_repository_projects', 'created_at', 'updated_at']
|
||||
# organisation attribute dictionary
|
||||
self.org_attr_dict = {'avatar_url': 'Profile Photo',
|
||||
'login': 'Username',
|
||||
'id': 'ID',
|
||||
@@ -336,7 +338,7 @@ class Octosuite:
|
||||
'public_repos': 'Repositories',
|
||||
'type': 'Account type',
|
||||
'is_verified': 'Is verified?',
|
||||
'has_organization_projects': 'Has organization projects?',
|
||||
'has_organisation_projects': 'Has organisation projects?',
|
||||
'has_repository_projects': 'Has repository projects?',
|
||||
'created_at': 'Created at',
|
||||
'updated_at': 'Updated at'}
|
||||
@@ -412,7 +414,7 @@ class Octosuite:
|
||||
'twitter_username': 'Twitter Handle',
|
||||
'public_gists': 'Gists (public)',
|
||||
'public_repos': 'Repositories (public)',
|
||||
'company': 'Organization',
|
||||
'company': 'organisation',
|
||||
'hireable': 'Is hireable?',
|
||||
'site_admin': 'Is site admin?',
|
||||
'created_at': 'Joined at',
|
||||
@@ -496,7 +498,7 @@ class Octosuite:
|
||||
'created_at': 'Created at',
|
||||
'updated_at': 'Updated at'}
|
||||
|
||||
# User organizations attributes
|
||||
# User organisations attributes
|
||||
self.user_orgs_attrs = ['avatar_url', 'id', 'node_id', 'url', 'description']
|
||||
self.user_orgs_attr_dict = {'avatar_url': 'Profile Photo',
|
||||
'id': 'ID',
|
||||
@@ -510,17 +512,39 @@ class Octosuite:
|
||||
'About.me': 'https://about.me/rly0nheart',
|
||||
'Buy Me A Coffee': 'https://buymeacoffee.com/189381184'}
|
||||
|
||||
# Fetching organization info
|
||||
def org_profile(self):
|
||||
if args.organization:
|
||||
organization = args.organization
|
||||
def get_repos_from_username(self, username):
|
||||
response = requests.get(f"{self.endpoint}/users/{username}/repos?per_page=100&sort=pushed",
|
||||
auth=HTTPBasicAuth(username, '')).text
|
||||
repositories = re.findall(rf'"full_name":"{username}/(.*?)",.*?"fork":(.*?),', response)
|
||||
unforked_repos = []
|
||||
for repository in repositories:
|
||||
if repository[1] == 'false':
|
||||
unforked_repos.append(repository[0])
|
||||
return unforked_repos
|
||||
|
||||
def get_user_email(self):
|
||||
if args.username:
|
||||
username = args.username
|
||||
else:
|
||||
organization = Prompt.ask(f"{white}@{green}Organi[sz]ation{reset}")
|
||||
response = requests.get(f"{self.endpoint}/orgs/{organization}")
|
||||
username = Prompt.ask(f"{white}@{green}Username{reset}")
|
||||
repos = self.get_repos_from_username(username)
|
||||
for repo in repos:
|
||||
email = get_email_from_contributor(username, repo, username)
|
||||
if email:
|
||||
xprint(f"{username}: {email}")
|
||||
break
|
||||
|
||||
# Fetching organisation info
|
||||
def org_profile(self):
|
||||
if args.organisation:
|
||||
organisation = args.organisation
|
||||
else:
|
||||
organisation = Prompt.ask(f"{white}@{green}Organisation{reset}")
|
||||
response = requests.get(f"{self.endpoint}/orgs/{organisation}")
|
||||
if response.status_code == 404:
|
||||
xprint(f"{NEGATIVE} {org_not_found.format(organization)}")
|
||||
xprint(f"{NEGATIVE} {org_not_found.format(organisation)}")
|
||||
elif response.status_code == 200:
|
||||
org_profile_tree = Tree("\n{response.json()['name']}")
|
||||
org_profile_tree = Tree(f"\n{response.json()['name']}")
|
||||
for attr in self.org_attrs:
|
||||
org_profile_tree.add(f"{self.org_attr_dict[attr]}: {response.json()[attr]}")
|
||||
xprint(org_profile_tree)
|
||||
@@ -730,17 +754,17 @@ class Octosuite:
|
||||
else:
|
||||
xprint(response.json())
|
||||
|
||||
# Fetching organization repositories
|
||||
# Fetching organisation repositories
|
||||
def org_repos(self):
|
||||
if args.organization and args.limit:
|
||||
organization = args.organization
|
||||
if args.organisation and args.limit:
|
||||
organisation = args.organisation
|
||||
limit = args.limit
|
||||
else:
|
||||
organization = Prompt.ask(f"{white}@{green}Organi[sz]ation{reset}")
|
||||
limit = Prompt.ask(limit_output.format("organization repositories"))
|
||||
response = requests.get(f"{self.endpoint}/orgs/{organization}/repos?per_page={limit}")
|
||||
organisation = Prompt.ask(f"{white}@{green}Organisation{reset}")
|
||||
limit = Prompt.ask(limit_output.format("organisation repositories"))
|
||||
response = requests.get(f"{self.endpoint}/orgs/{organisation}/repos?per_page={limit}")
|
||||
if response.status_code == 404:
|
||||
xprint(f"{NEGATIVE} {org_not_found.format(organization)}")
|
||||
xprint(f"{NEGATIVE} {org_not_found.format(organisation)}")
|
||||
elif response.status_code == 200:
|
||||
for repository in response.json():
|
||||
repos_tree = Tree("\n" + repository['full_name'])
|
||||
@@ -749,43 +773,43 @@ class Octosuite:
|
||||
xprint(repos_tree)
|
||||
|
||||
if args.log_csv or Prompt.ask(f"{PROMPT} {prompt_log_csv}") == "yes":
|
||||
log_org_repos(repository, organization)
|
||||
log_org_repos(repository, organisation)
|
||||
else:
|
||||
xprint(response.json())
|
||||
|
||||
# organization events
|
||||
# organisation events
|
||||
def org_events(self):
|
||||
if args.organization and args.limit:
|
||||
organization = args.organization
|
||||
if args.organisation and args.limit:
|
||||
organisation = args.organisation
|
||||
limit = args.limit
|
||||
else:
|
||||
organization = Prompt.ask(f"{white}@{green}Organi[sz]ation{reset}")
|
||||
limit = Prompt.ask(limit_output.format("organization events"))
|
||||
response = requests.get(f"{self.endpoint}/orgs/{organization}/events?per_page={limit}")
|
||||
organisation = Prompt.ask(f"{white}@{green}Organisation{reset}")
|
||||
limit = Prompt.ask(limit_output.format("organisation events"))
|
||||
response = requests.get(f"{self.endpoint}/orgs/{organisation}/events?per_page={limit}")
|
||||
if response.status_code == 404:
|
||||
xprint(f"{NEGATIVE} {org_not_found.format(organization)}")
|
||||
xprint(f"{NEGATIVE} {org_not_found.format(organisation)}")
|
||||
elif response.status_code == 200:
|
||||
for event in response.json():
|
||||
events_tree = Tree("\n" + event['id'])
|
||||
events_tree.add(f"Type: {event['type']}")
|
||||
events_tree.add(f"Created at: {event['created_at']}")
|
||||
xprint(events_tree)
|
||||
xprint(event['payload'])
|
||||
# log_org_events(event, organization)
|
||||
xprint(events_tree)
|
||||
xprint(event['payload'])
|
||||
# log_org_events(event, organisation)
|
||||
else:
|
||||
xprint(response.json())
|
||||
|
||||
# organization member
|
||||
# organisation member
|
||||
def org_member(self):
|
||||
if args.organization and args.username:
|
||||
organization = args.organization
|
||||
if args.organisation and args.username:
|
||||
organisation = args.organisation
|
||||
username = args.username
|
||||
else:
|
||||
organization = Prompt.ask(f"{white}@{green}Organi[sz]ation{reset}")
|
||||
organisation = Prompt.ask(f"{white}@{green}Organisation{reset}")
|
||||
username = Prompt.ask(f"{white}@{green}Username{reset}")
|
||||
response = requests.get(f"{self.endpoint}/orgs/{organization}/public_members/{username}")
|
||||
response = requests.get(f"{self.endpoint}/orgs/{organisation}/public_members/{username}")
|
||||
if response.status_code == 204:
|
||||
xprint(f"{POSITIVE} User ({username}) is a public member of the organization -> ({organization})")
|
||||
xprint(f"{POSITIVE} User ({username}) is a public member of the organisation -> ({organisation})")
|
||||
else:
|
||||
xprint(f"{NEGATIVE} {response.json()['message']}")
|
||||
|
||||
@@ -837,28 +861,28 @@ class Octosuite:
|
||||
else:
|
||||
xprint(response.json())
|
||||
|
||||
# Fetching a list of organizations that a user owns or belongs to
|
||||
# Fetching a list of organisations that a user owns or belongs to
|
||||
def user_orgs(self):
|
||||
if args.username and args.limit:
|
||||
username = args.username
|
||||
limit = args.limit
|
||||
else:
|
||||
username = Prompt.ask(f"{white}@{green}Username{reset}")
|
||||
limit = Prompt.ask(limit_output.format("user organizations"))
|
||||
limit = Prompt.ask(limit_output.format("user organisations"))
|
||||
response = requests.get(f"{self.endpoint}/users/{username}/orgs?per_page={limit}")
|
||||
if not response.json():
|
||||
xprint(f"{NEGATIVE} User ({username}) does not (belong to/own) any organizations.")
|
||||
xprint(f"{NEGATIVE} User ({username}) does not (belong to/own) any organisations.")
|
||||
elif response.status_code == 404:
|
||||
xprint(f"{NEGATIVE} {user_not_found.format(username)}")
|
||||
elif response.status_code == 200:
|
||||
for organization in response.json():
|
||||
org_tree = Tree("\n" + organization['login'])
|
||||
for organisation in response.json():
|
||||
org_tree = Tree("\n" + organisation['login'])
|
||||
for attr in self.user_orgs_attrs:
|
||||
org_tree.add(f"{self.user_orgs_attr_dict[attr]}: {organization[attr]}")
|
||||
org_tree.add(f"{self.user_orgs_attr_dict[attr]}: {organisation[attr]}")
|
||||
xprint(org_tree)
|
||||
|
||||
if args.log_csv or Confirm.ask(f"\n{PROMPT} {prompt_log_csv}"):
|
||||
log_user_orgs(organization, username)
|
||||
log_user_orgs(organisation, username)
|
||||
else:
|
||||
xprint(response.json())
|
||||
|
||||
|
||||
39
pyproject.toml
Normal file
@@ -0,0 +1,39 @@
|
||||
[build-system]
|
||||
requires = ["setuptools", "setuptools-scm"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["octosuite"]
|
||||
|
||||
[project]
|
||||
name = "octosuite"
|
||||
version = "3.1.1"
|
||||
description = "Advanced GitHub OSINT Framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
license = {file = "LICENSE"}
|
||||
keywords = ["github", "python", "github-api", "framework", "osint", "osint-framework", "osint-python", "osint-tool"]
|
||||
authors = [{name = "Richard Mwewa", email = "rly0nheart@duck.com"}]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Intended Audience :: Information Technology",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: OS Independent",
|
||||
"Natural Language :: English"
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"rich",
|
||||
"psutil",
|
||||
"requests",
|
||||
"pyreadline3",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/bellingcat/octosuite"
|
||||
documentation = "https://github.com/bellingcat/octosuite/wiki"
|
||||
repository = "https://github.com/bellingcat/octosuite.git"
|
||||
|
||||
[project.scripts]
|
||||
octosuite = "octosuite.main:octosuite"
|
||||
31
setup.py
@@ -1,31 +0,0 @@
|
||||
import setuptools
|
||||
|
||||
with open("README.md", "r", encoding="utf-8") as file:
|
||||
long_description = file.read()
|
||||
|
||||
setuptools.setup(
|
||||
name="octosuite",
|
||||
version="3.0.2",
|
||||
author="Richard Mwewa",
|
||||
author_email="rly0nheart@duck.com",
|
||||
packages=["octosuite"],
|
||||
description="Advanced Github OSINT Framework",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/bellingcat/octosuite",
|
||||
license="GNU General Public License v3 (GPLv3)",
|
||||
install_requires=["requests", "rich", "psutil", "pyreadline3"],
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: Information Technology',
|
||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||
'Operating System :: OS Independent',
|
||||
'Natural Language :: English',
|
||||
'Programming Language :: Python :: 3'
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"octosuite=octosuite.main:octosuite",
|
||||
]
|
||||
},
|
||||
)
|
||||