mirror of
https://github.com/bellingcat/octosuite.git
synced 2026-06-11 12:58:34 +03:00
470 lines
23 KiB
Python
470 lines
23 KiB
Python
import os
|
||
import logging
|
||
import requests
|
||
import platform
|
||
import subprocess
|
||
import urllib.request
|
||
from tqdm import tqdm
|
||
from pprint import pprint
|
||
from lib.banner import banner
|
||
from datetime import datetime
|
||
from lib.colors import red, white, green, reset
|
||
|
||
class octosuite:
|
||
def __init__(self):
|
||
# Path attribute
|
||
self.path_attrs =['size','type','path','sha','html_url']
|
||
# Path attribute dictionary
|
||
self.path_attr_dict = {'size': 'Size (bytes)',
|
||
'type': 'Type',
|
||
'path': 'Path',
|
||
'sha': 'SHA',
|
||
'html_url': 'URL'}
|
||
|
||
# Organization 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
|
||
self.org_attr_dict = {'avatar_url': 'Profile Photo',
|
||
'login': 'Username',
|
||
'id': 'ID#',
|
||
'node_id': 'Node ID',
|
||
'email': 'Email',
|
||
'description': 'About',
|
||
'location': 'Location',
|
||
'blog': 'Blog',
|
||
'followers': 'Followers',
|
||
'following': 'Following',
|
||
'twitter_username': 'Twitter Handle',
|
||
'public_gists': 'Gists (public)',
|
||
'public_repos': 'Repositories (public)',
|
||
'type': 'Account type',
|
||
'is_verified': 'Is verified?',
|
||
'has_organization_projects': 'Has organization projects?',
|
||
'has_repository_projects': 'Has repository projects?',
|
||
'created_at': 'Created at',
|
||
'updated_at': 'Updated at'}
|
||
# Repository attributes
|
||
self.repo_attrs = ['id','description','forks','allow_forking','fork','stargazers_count','watchers','license','default_branch','visibility','language','open_issues','topics','homepage','clone_url','ssh_url','private','archived','has_downloads','has_issues','has_pages','has_projects','has_wiki','pushed_at','created_at','updated_at']
|
||
# Repository attribute dictionary
|
||
self.repo_attr_dict = {'id': 'ID#',
|
||
'description': 'About',
|
||
'forks': 'Forks',
|
||
'allow_forking': 'Is forkable?',
|
||
'fork': 'Is fork?',
|
||
'stargazers_count': 'Stars',
|
||
'watchers': 'Watchers',
|
||
'license': 'License',
|
||
'default_branch': 'Branch',
|
||
'visibility': 'Visibility',
|
||
'language': 'Language(s)',
|
||
'open_issues': 'Open issues',
|
||
'topics': 'Topics',
|
||
'homepage': 'Homepage',
|
||
'clone_url': 'Clone URL',
|
||
'ssh_url': 'SSH URL',
|
||
'private': 'Is private?',
|
||
'archived': 'Is archived?',
|
||
'is_template': 'Is template?',
|
||
'has_wiki': 'Has wiki?',
|
||
'has_pages': 'Has pages?',
|
||
'has_projects': 'Has projects?',
|
||
'has_issues': 'Has issues?',
|
||
'has_downloads': 'Has downloads?',
|
||
'pushed_at': 'Pushed at',
|
||
'created_at': 'Created at',
|
||
'updated_at': 'Updated at'}
|
||
|
||
# Profile attributes
|
||
self.profile_attrs = ['avatar_url','login','id','node_id','bio','blog','location','followers','following','twitter_username','public_gists','public_repos','company','hireable','site_admin','created_at','updated_at']
|
||
# Profile attribute dictionary
|
||
self.profile_attr_dict = {'avatar_url': 'Profile Photo',
|
||
'login': 'Username',
|
||
'id': 'ID#',
|
||
'node_id': 'Node ID',
|
||
'bio': 'Bio',
|
||
'blog': 'Blog',
|
||
'location': 'Location',
|
||
'followers': 'Followers',
|
||
'following': 'Following',
|
||
'twitter_username': 'Twitter Handle',
|
||
'public_gists': 'Gists (public)',
|
||
'public_repos': 'Repositories (public)',
|
||
'company': 'Organization',
|
||
'hireable': 'Is hireable?',
|
||
'site_admin': 'Is site admin?',
|
||
'created_at': 'Joined at',
|
||
'updated_at': 'Updated at'}
|
||
|
||
# User attributes
|
||
self.user_attrs = ['avatar_url','id','node_id','gravatar_id','site_admin','type','html_url']
|
||
# User attribute dictionary
|
||
self.user_attr_dict = {'avatar_url': 'Profile Photo',
|
||
'id': 'ID#',
|
||
'node_id': 'Node ID',
|
||
'gravatar_id': 'Gravatar ID',
|
||
'site_admin': 'Is site admin?',
|
||
'type': 'Account type',
|
||
'html_url': 'URL'}
|
||
|
||
# Topic atrributes
|
||
self.topic_attrs = ['score','curated','featured','display_name','created_by','created_at','updated_at']
|
||
# Topic attribute dictionary
|
||
self.topic_attr_dict = {'score': 'Score',
|
||
'curated': 'Curated',
|
||
'featured': 'Featured',
|
||
'display_name': 'Display Name',
|
||
'created_by': 'Created by',
|
||
'created_at': 'Created at',
|
||
'updated_at': 'Updated at'}
|
||
|
||
# Gists attributes
|
||
self.gists_attrs = ['node_id','description','comments','files','git_push_url','public','truncated','updated_at']
|
||
# Gists attribute dictionary
|
||
self.gists_attr_dict = {'node_id': 'Node ID',
|
||
'description': 'About',
|
||
'comments': 'Comments',
|
||
'files': 'Files',
|
||
'git_push_url': 'Git Push URL',
|
||
'public': 'Is public?',
|
||
'truncated': 'Is truncated?',
|
||
'updated_at': 'Updated at'}
|
||
|
||
# Issue attributes
|
||
self.issue_attrs = ['id','node_id','score','state','number','comments','milestone','assignee','assignees','labels','locked','draft','closed_at','body']
|
||
# Issue attribute dict
|
||
self.issue_attr_dict = {'id': 'ID#',
|
||
'node_id': 'Node ID',
|
||
'score': 'Score',
|
||
'state': 'State',
|
||
'closed_at': 'Closed at',
|
||
'number': 'Number',
|
||
'comments': 'Comments',
|
||
'milestone': 'Milestone',
|
||
'assignee': 'Assignee',
|
||
'assignees': 'Assignees',
|
||
'labels': 'Labels',
|
||
'draft': 'Is draft?',
|
||
'locked': 'Is locked?',
|
||
'created_at': 'Created at',
|
||
'body': 'Body'}
|
||
|
||
# Author dictionary
|
||
self.author_dict = {'Alias': 'rly0nheart',
|
||
'Country': 'Zambia, Africa',
|
||
'About.me': 'https://about.me/rly0nheart'}
|
||
|
||
def on_start(self):
|
||
logging.info(f'Started new session on {platform.node()}')
|
||
while True:
|
||
if platform.system() == 'Windows':
|
||
subprocess.run(['cls'])
|
||
else:
|
||
subprocess.run(['clear'],shell=False)
|
||
|
||
print(banner)
|
||
command = input(f'''{white}┌─({red}{platform.node()}{white}@{red}octosuite{white})-[{green}{os.getcwd()}{white}]\n└─╼[{green}:~{white}]{reset} ''')
|
||
if command == 'orginfo':
|
||
self.org_info()
|
||
elif command == 'userinfo':
|
||
self.user_profile()
|
||
elif command == 'repoinfo':
|
||
self.repo_info()
|
||
elif command == 'pathcontents':
|
||
self.path_contents()
|
||
elif command == 'orgrepos':
|
||
self.org_repos()
|
||
elif command == 'userrepos':
|
||
self.user_repos()
|
||
elif command == 'usergists':
|
||
self.user_gists()
|
||
elif command == 'userfollowers':
|
||
self.followers()
|
||
elif command == 'userfollowing':
|
||
self.following()
|
||
elif command == 'usersearch':
|
||
self.user_search()
|
||
elif command == 'reposearch':
|
||
self.repo_search()
|
||
elif command == 'topicsearch':
|
||
self.topic_search()
|
||
elif command == 'issuesearch':
|
||
self.issue_search()
|
||
elif command == 'commitsearch':
|
||
self.commits_search()
|
||
elif command == 'update':
|
||
self.update()
|
||
elif command == 'changelog':
|
||
print(self.changelog())
|
||
elif command == 'author':
|
||
self.author()
|
||
elif command == 'help':
|
||
print(self.help())
|
||
elif command == 'exit':
|
||
logging.info('Session terminated.')
|
||
exit(f'\n{white}[{red}-{white}] Session terminated.{reset}')
|
||
else:
|
||
print(f'\n{white}[{red}!{white}] Unknown command: ‘{command}’{reset}')
|
||
logging.warning(f'Unknown command: ‘{command}’')
|
||
|
||
input(f'\n{white}^ Press any key to continue{reset} ')
|
||
|
||
|
||
def org_info(self):
|
||
organization = input(f'{white}@{green}Organization{white} >> {reset}')
|
||
api = f'https://api.github.com/orgs/{organization}'
|
||
response = requests.get(api)
|
||
if response.status_code != 200:
|
||
print(f'\n{white}[{red}-{white}] Organization @{organization} {red}Not Found{reset}')
|
||
else:
|
||
response = response.json()
|
||
print(f"\n{white}{response['name']}{reset}")
|
||
for attr in self.org_attrs:
|
||
print(f'{white}├─ {self.org_attr_dict[attr]}: {green}{response[attr]}{reset}')
|
||
|
||
|
||
# Fetching user information
|
||
def user_profile(self):
|
||
username = input(f'{white}@{green}Username{white} >> {reset}')
|
||
api = f'https://api.github.com/users/{username}'
|
||
response = requests.get(api)
|
||
if response.status_code != 200:
|
||
print(f'\n{white}[{red}-{white}] User @{username} {red}Not Found{reset}')
|
||
else:
|
||
response = response.json()
|
||
print(f"\n{white}{response['name']}{reset}")
|
||
for attr in self.profile_attrs:
|
||
print(f'{white}├─ {self.profile_attr_dict[attr]}: {green}{response[attr]}{reset}')
|
||
|
||
|
||
# Fetching repository information
|
||
def repo_info(self):
|
||
username = input(f'{white}@{green}Owner-username{white} >> {reset}')
|
||
repo_name = input(f'{white}%{green}reponame{white} >> {reset}')
|
||
api = f'https://api.github.com/repos/{username}/{repo_name}'
|
||
response = requests.get(api)
|
||
if response.status_code != 200:
|
||
print(f'\n{white}[{red}-{white}] Repository %{repo_name} {red}Not Found{reset}')
|
||
else:
|
||
response = response.json()
|
||
print(f"\n{white}{response['full_name']}{reset}")
|
||
for attr in self.repo_attrs:
|
||
print(f"{white}├─ {self.repo_attr_dict[attr]}: {green}{response[attr]}{reset}")
|
||
|
||
|
||
# Get path contents
|
||
def path_contents(self):
|
||
username = input(f'{white}@{green}Owner-username{white} >> {reset}')
|
||
repo_name = input(f'{white}%{green}reponame{white} >> {reset}')
|
||
path_name = input(f'{white}/path/name >>{reset} ')
|
||
api = f'https://api.github.com/repos/{username}/{repo_name}/contents/{path_name}'
|
||
response = requests.get(api)
|
||
if response.status_code != 200:
|
||
print(f'\n{white}[{red}-{white}] Information {red}Not Found{reset}')
|
||
else:
|
||
response = response.json()
|
||
for item in response:
|
||
print(f"\n{white}{item['name']}{reset}")
|
||
for attr in self.path_attrs:
|
||
print(f'{white}├─ {self.path_attr_dict[attr]}: {green}{item[attr]}{reset}')
|
||
|
||
|
||
# Fetching organozation repositories
|
||
def org_repos(self):
|
||
organization = input(f'{white}@{green}Organization{white} >> {reset}')
|
||
api = f'https://api.github.com/orgs/{organization}/repos?per_page=100'
|
||
response = requests.get(api)
|
||
if response.status_code != 200:
|
||
print(f'\n{white}[{red}-{white}] Organization @{organization} {red}Not Found{reset}')
|
||
else:
|
||
response = response.json()
|
||
for repo in response:
|
||
print(f"\n{white}{repo['full_name']}{reset}")
|
||
for attr in self.repo_attrs:
|
||
print(f"{white}├─ {self.repo_attr_dict[attr]}: {green}{repo[attr]}{reset}")
|
||
print('\n')
|
||
|
||
|
||
# Fetching user repositories
|
||
def user_repos(self):
|
||
username = input(f'{white}@{green}Username{white} >> {reset}')
|
||
api = f'https://api.github.com/users/{username}/repos?per_page=100'
|
||
response = requests.get(api)
|
||
if response.status_code != 200:
|
||
print(f'\n{white}[{red}-{white}] User @{username} {red}Not Found{reset}')
|
||
else:
|
||
response = response.json()
|
||
for repo in response:
|
||
print(f"\n{white}{repo['full_name']}{reset}")
|
||
for attr in self.repo_attrs:
|
||
print(f"{white}├─ {self.repo_attr_dict[attr]}: {green}{repo[attr]}{reset}")
|
||
print('\n')
|
||
|
||
|
||
# Fetching user's gists
|
||
def user_gists(self):
|
||
username = input(f'{white}@{green}Username{white} >> {reset}')
|
||
api = f'https://api.github.com/users/{username}/gists'
|
||
response = requests.get(api).json()
|
||
if response == []:
|
||
print(f'{white}[{red}-{white}]User @{username} does not have any active gists.{reset}')
|
||
else:
|
||
for item in response:
|
||
print(f"\n{white}{item['id']}{reset}")
|
||
for attr in self.gists_attrs:
|
||
print(f"{white}├─ {self.gists_attr_dict[attr]}: {green}{item[attr]}{reset}")
|
||
print('\n')
|
||
|
||
|
||
# Fetching user's followera'
|
||
def followers(self):
|
||
username = input(f'{white}@{green}Username{white} >> {reset}')
|
||
api = f'https://api.github.com/users/{username}/followers?per_page=100'
|
||
response = requests.get(api).json()
|
||
if response == []:
|
||
print(f'\n{white}[{red}-{white}]User @{username} does not have followers.{reset}')
|
||
else:
|
||
for item in response:
|
||
print(f"\n{white}@{item['login']}{reset}")
|
||
for attr in self.user_attrs:
|
||
print(f"{white}├─ {self.user_attr_dict[attr]}: {green}{item[attr]}{reset}")
|
||
print('\n')
|
||
|
||
|
||
# Checking whether or not user[A] follows user[B]
|
||
def following(self):
|
||
user_a = input(f'{white}@{green}User[A]{white} >> {reset}')
|
||
user_b = input(f'{white}@{green}User[B]{white} >> {reset}')
|
||
api = f'https://api.github.com/users/{user_a}/following/{user_b}'
|
||
response = requests.get(api)
|
||
if response.status_code == 204:
|
||
print(f'{white}[{green}+{white}] @{user_a} follows @{user_b}.{reset}')
|
||
else:
|
||
print(f'{white}[{red}-{white}] @{user_a} does not follow @{user_b}.{reset}')
|
||
|
||
|
||
# User search
|
||
def user_search(self):
|
||
query = input(f'{white}#{green}Query{white} >> {reset}')
|
||
api = f'https://api.github.com/search/users?q={query}&per_page=100'
|
||
response = requests.get(api).json()
|
||
for item in response['items']:
|
||
print(f"\n{white}@{item['login']}{reset}")
|
||
for attr in self.user_attrs:
|
||
print(f"{white}├─ {self.user_attr_dict[attr]}: {green}{item[attr]}{reset}")
|
||
print('\n')
|
||
|
||
|
||
# Repository search
|
||
def repo_search(self):
|
||
query = input(f'{white}#{green}Query{white} >> {reset}')
|
||
api = f'https://api.github.com/search/repositories?q={query}&per_page=100'
|
||
response = requests.get(api).json()
|
||
for item in response['items']:
|
||
print(f"\n{white}{item['full_name']}{reset}")
|
||
for attr in self.repo_attrs:
|
||
print(f"{white}├─ {self.repo_attr_dict[attr]}: {green}{item[attr]}{reset}")
|
||
print('\n')
|
||
|
||
|
||
# Topics search
|
||
def topic_search(self):
|
||
query = input(f'{white}#{green}Query{white} >> {reset}')
|
||
api = f'https://api.github.com/search/topics?q={query}&per_page=100'
|
||
response = requests.get(api).json()
|
||
for item in response['items']:
|
||
print(f"\n{white}{item['name']}{reset}")
|
||
for attr in self.topic_attrs:
|
||
print(f"{white}├─ {self.topic_attr_dict[attr]}: {green}{item[attr]}{reset}")
|
||
print('\n')
|
||
|
||
|
||
# Issue search
|
||
def issue_search(self):
|
||
query = input(f'{white}#{green}Query{white} >> {reset}')
|
||
api = f'https://api.github.com/search/issues?q={query}&per_page=100'
|
||
response = requests.get(api).json()
|
||
for item in response['items']:
|
||
print(f"\n{white}{item['title']}{reset}")
|
||
for attr in self.issue_attrs:
|
||
print(f"{white}├─ {self.issue_attr_dict[attr]}: {green}{item[attr]}{reset}")
|
||
print('\n')
|
||
|
||
|
||
# Commits search
|
||
def commits_search(self):
|
||
query = input(f'{white}#{green}Query{white} >> {reset}')
|
||
api = f'https://api.github.com/search/commits?q={query}&per_page=100'
|
||
response = requests.get(api).json()
|
||
n=0
|
||
for item in response['items']:
|
||
n+=1
|
||
print(f'{white}{n}.{reset}')
|
||
pprint(item['commit'])
|
||
print('\n')
|
||
|
||
|
||
# Update program
|
||
def update(self):
|
||
logging.info('Fetching updates...')
|
||
files_to_update = ['src/main.py','lib/banner.py','lib/colors.py','octosuite','.github/dependabot.yml','LICENSE','README.md','requirements.txt']
|
||
for file in tqdm(files_to_update,desc=f'{white}[{green}*{white}] Updating{reset}'):
|
||
data = urllib.request.urlopen(f'https://raw.githubusercontent.com/rly0nheart/octosuite/master/{file}').read()
|
||
with open(file, 'wb') as code:
|
||
code.write(data)
|
||
code.close()
|
||
|
||
logging.info('Update complete.')
|
||
exit(f'{white}[{green}+{white}] Updated successfully. Re-run octosuite.{reset}')
|
||
|
||
|
||
# Show changelog
|
||
def changelog(self):
|
||
# lol yes the changelog is hard coded
|
||
changelog_text = '''
|
||
v1.5.0 Changelog:
|
||
|
||
• Fixed import error in src/main.py
|
||
'''
|
||
return changelog_text
|
||
|
||
|
||
# Author info
|
||
def author(self):
|
||
print(f'\n{white}Richard Mwewa (Ritchie){reset}')
|
||
for key,value in self.author_dict.items():
|
||
print(f'{white}├─ {key}: {green}{value}{reset}')
|
||
|
||
|
||
def help(self):
|
||
help = '''
|
||
|
||
help:
|
||
|
||
Command Descritption
|
||
------------ ---------------------------------------------------------
|
||
orginfo --> Get target organization info
|
||
userinfo --> Get target user profile info
|
||
repoinfo --> Get target repository info
|
||
pathcontents --> Get contents of a specified path from a target repository
|
||
orgrepos --> Get a list of repositories owned by a target organization
|
||
userrepos --> Get a list of repositories owned by a target user
|
||
usergists --> Get a list of gists owned by a target user
|
||
userfollowers --> Get a list of the target's followers
|
||
userfollowing --> Check whether or not User[A] follows User[B]
|
||
usersearch --> Search user(s)
|
||
reposearch --> Search repositor[y][ies]
|
||
topicsearch --> Search topic(s)
|
||
issuesearch --> Search issue(s)
|
||
commitsearch --> Search commit(s)
|
||
update --> Update octosuite
|
||
changelog --> Show changelog
|
||
author --> Show author info
|
||
help --> Show usage/help
|
||
exit --> Exit session
|
||
'''
|
||
return help
|
||
|
||
|
||
# Set to automatically monitor and log network and user activity into the .logs folder
|
||
logging.basicConfig(filename=f'.logs/{datetime.now()}.log',format='[%(asctime)s] %(message)s',level=logging.DEBUG)
|