From 558715fdab85fe88fcd94b3d1e45b88854bc7489 Mon Sep 17 00:00:00 2001 From: Richard Mwewa <74001397+rly0nheart@users.noreply.github.com> Date: Wed, 30 Nov 2022 02:37:07 +0200 Subject: [PATCH] Update octosuite --- dist/octosuite-3.0.0-py3-none-any.whl | Bin 31944 -> 0 bytes dist/octosuite-3.0.0.tar.gz | Bin 31427 -> 0 bytes octosuite.egg-info/PKG-INFO | 83 -- octosuite.egg-info/SOURCES.txt | 18 - octosuite.egg-info/dependency_links.txt | 1 - octosuite.egg-info/entry_points.txt | 2 - octosuite.egg-info/requires.txt | 3 - octosuite.egg-info/top_level.txt | 1 - octosuite/colors.py | 6 +- octosuite/csv_loggers.py | 998 ++++++++++++++---------- octosuite/helper.py | 277 +++---- octosuite/log_roller.py | 41 +- octosuite/main.py | 10 +- octosuite/message_prefixes.py | 17 +- octosuite/octosuite.py | 237 +++--- 15 files changed, 864 insertions(+), 830 deletions(-) delete mode 100644 dist/octosuite-3.0.0-py3-none-any.whl delete mode 100644 dist/octosuite-3.0.0.tar.gz delete mode 100644 octosuite.egg-info/PKG-INFO delete mode 100644 octosuite.egg-info/SOURCES.txt delete mode 100644 octosuite.egg-info/dependency_links.txt delete mode 100644 octosuite.egg-info/entry_points.txt delete mode 100644 octosuite.egg-info/requires.txt delete mode 100644 octosuite.egg-info/top_level.txt diff --git a/dist/octosuite-3.0.0-py3-none-any.whl b/dist/octosuite-3.0.0-py3-none-any.whl deleted file mode 100644 index d1776b2796b3384487edc5640a78385386108513..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31944 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 diff --git a/dist/octosuite-3.0.0.tar.gz b/dist/octosuite-3.0.0.tar.gz deleted file mode 100644 index 05ddb25eded4074907ff9bc516d9b81c539cdc46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31427 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 diff --git a/octosuite.egg-info/PKG-INFO b/octosuite.egg-info/PKG-INFO deleted file mode 100644 index ce70b7f..0000000 --- a/octosuite.egg-info/PKG-INFO +++ /dev/null @@ -1,83 +0,0 @@ -Metadata-Version: 2.1 -Name: octosuite -Version: 3.0.0 -Summary: Advanced Github OSINT Framework -Home-page: https://github.com/bellingcat/octosuite -Author: Richard Mwewa -Author-email: rly0nheart@duck.com -License: GNU General Public License v3 (GPLv3) -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Information Technology -Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) -Classifier: Operating System :: OS Independent -Classifier: Natural Language :: English -Classifier: Programming Language :: Python :: 3 -Description-Content-Type: text/markdown -License-File: LICENSE - -![logo](https://user-images.githubusercontent.com/74001397/175805580-fffc96d4-e0ef-48bb-a55c-80b2da3e714d.png) - -A framework fro gathering osint on GitHub users, repositories and organizations - -[![Upload Python Package](https://github.com/bellingcat/octosuite/actions/workflows/python-publish.yml/badge.svg)](https://github.com/bellingcat/octosuite/actions/workflows/python-publish.yml) -[![CodeQL](https://github.com/bellingcat/octosuite/actions/workflows/codeql.yml/badge.svg)](https://github.com/bellingcat/octosuite/actions/workflows/codeql.yml) -![GitHub](https://img.shields.io/github/license/bellingcat/octosuite?style=flat) -![PyPI](https://img.shields.io/pypi/v/octosuite?style=flat&logo=pypi) -![PyPI - Downloads](https://img.shields.io/pypi/dw/octosuite?style=flat&logo=pypi) -![PyPI - Status](https://img.shields.io/pypi/status/octosuite?style=flat&logo=pypi) -![GitHub repo size](https://img.shields.io/github/repo-size/bellingcat/octosuite?style=flat&logo=github) - - -![octosuite_gui_exe (2)](https://user-images.githubusercontent.com/74001397/186889610-4530ee26-d3c6-46fc-8c92-8709f89617fd.png "Octosuite' about window") - -![octosuite_gui_exe (4)](https://user-images.githubusercontent.com/74001397/186889897-c1c17fac-fddc-4967-9084-39cfe2d1307f.png "Octosuite user profile window") - - -# 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 oganization's events -- [x] Returns an organization's repositories -- [x] Returns an organization's public members -- [x] Fetches a repository's information -- [x] Returns a repository's contributors -- [x] Returns a repository's languages -- [x] Fetches a repository's stargazers -- [x] Fetches a repository's forks -- [x] Fetches a repository's releases -- [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] 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] 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] ...And more - -## Note -> Octosuite automatically logs network and user activity of each session, the logs are saved by date and time in the .logs folder - - -# License -![license](https://user-images.githubusercontent.com/74001397/137917929-2f2cdb0c-4d1d-4e4b-9f0d-e01589e027b5.png) - -# Donations -If you like Octosuite and would like to show support, you can Buy A Coffee for the developer using the button below - -Buy Me A Coffee - -Your support will be much appreciated😊 diff --git a/octosuite.egg-info/SOURCES.txt b/octosuite.egg-info/SOURCES.txt deleted file mode 100644 index d1d90bf..0000000 --- a/octosuite.egg-info/SOURCES.txt +++ /dev/null @@ -1,18 +0,0 @@ -LICENSE -README.md -setup.py -octosuite/__init__.py -octosuite/banners.py -octosuite/colors.py -octosuite/csv_loggers.py -octosuite/helper.py -octosuite/log_roller.py -octosuite/main.py -octosuite/message_prefixes.py -octosuite/octosuite.py -octosuite.egg-info/PKG-INFO -octosuite.egg-info/SOURCES.txt -octosuite.egg-info/dependency_links.txt -octosuite.egg-info/entry_points.txt -octosuite.egg-info/requires.txt -octosuite.egg-info/top_level.txt \ No newline at end of file diff --git a/octosuite.egg-info/dependency_links.txt b/octosuite.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/octosuite.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/octosuite.egg-info/entry_points.txt b/octosuite.egg-info/entry_points.txt deleted file mode 100644 index ef7d5ff..0000000 --- a/octosuite.egg-info/entry_points.txt +++ /dev/null @@ -1,2 +0,0 @@ -[console_scripts] -octosuite = octosuite.main:octosuite diff --git a/octosuite.egg-info/requires.txt b/octosuite.egg-info/requires.txt deleted file mode 100644 index d4d1a3e..0000000 --- a/octosuite.egg-info/requires.txt +++ /dev/null @@ -1,3 +0,0 @@ -requests -rich -psutil diff --git a/octosuite.egg-info/top_level.txt b/octosuite.egg-info/top_level.txt deleted file mode 100644 index 194678a..0000000 --- a/octosuite.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -octosuite diff --git a/octosuite/colors.py b/octosuite/colors.py index 1ddb230..c6188f9 100644 --- a/octosuite/colors.py +++ b/octosuite/colors.py @@ -29,7 +29,7 @@ xprint(system_tree) print("\n") while True: try: - color_chooser = input(f"[?] Welcome, would you like to enable colors for this session? (yes/no) ").lower() + color_chooser = input(f"[PROMPT] Welcome, would you like to enable colors for this session? (yes/no) ").lower() if color_chooser == "yes": header_title = "bold white" red = "[red]" @@ -44,8 +44,8 @@ while True: header_title = red = white = green = red_bold = white_bold = green_bold = reset = "" break else: - print(f"\n[!] Your response '{color_chooser}' is invalid (expected yes or no)") + print(f"\n[INVALID] Your response '{color_chooser}' is invalid (expected yes or no)") except KeyboardInterrupt: - exit(f"[!] Process interrupted with Ctrl+C.") + exit(f"[WARNING] Process interrupted with Ctrl+C.") diff --git a/octosuite/csv_loggers.py b/octosuite/csv_loggers.py index d7b2d3b..93db373 100644 --- a/octosuite/csv_loggers.py +++ b/octosuite/csv_loggers.py @@ -2,445 +2,583 @@ import os import csv import logging from rich import print as xprint -from octosuite.log_roller import LogRoller -from octosuite.message_prefixes import MessagePrefix +from octosuite.log_roller import prompt_log_csv, logged_to_csv, logging_skipped +from octosuite.message_prefixes import PROMPT, WARNING, POSITIVE, NEGATIVE, INFO -# CsvLogger -# This class holds the methods for creating .csv files of each functionality in main -class CsvLogger: - # .csv for organization' profile - 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', '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()['has_repository_projects'], response.json()['created_at'], response.json()['updated_at']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{response.json()['name']}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(org_profile_fields) - write_csv.writerow(org_profile_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") +# csv_loggers.py +# This class holds the functions for creating .csv files of each functionality in main +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', + '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()['has_repository_projects'], response.json()['created_at'], + response.json()['updated_at']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{response.json()['name']}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(org_profile_fields) + write_csv.writerow(org_profile_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") - # 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?', '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'], response.json()['blog'], response.json()['location'], response.json()['followers'], response.json()['following'], response.json()['twitter_username'], response.json()['public_gists'], response.json()['public_repos'], response.json()['company'], response.json()['hireable'], response.json()['site_admin'], response.json()['created_at'], response.json()['updated_at']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{response.json()['login']}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(user_profile_fields) - write_csv.writerow(user_profile_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") +# 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?', + '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'], + response.json()['blog'], response.json()['location'], response.json()['followers'], + response.json()['following'], response.json()['twitter_username'], + response.json()['public_gists'], response.json()['public_repos'], response.json()['company'], + response.json()['hireable'], response.json()['site_admin'], response.json()['created_at'], + response.json()['updated_at']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{response.json()['login']}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(user_profile_fields) + write_csv.writerow(user_profile_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") - # create .csv for repository profile - def log_repo_profile(response): - repo_profile_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?', 'Has projects?', 'Has issues?', 'Has downloads?', 'Pushed at', 'Created at', 'Updated at'] - repo_profile_row = [response.json()['name'], response.json()['id'], response.json()['description'], response.json()['forks'], response.json()['stargazers_count'], response.json()['watchers'], response.json()['license'], response.json()['default_branch'], response.json()['visibility'], response.json()['language'], response.json()['open_issues'], response.json()['topics'], response.json()['homepage'], response.json()['clone_url'], response.json()['ssh_url'], response.json()['fork'], response.json()['allow_forking'], response.json()['private'], response.json()['archived'], response.json()['is_template'], response.json()['has_wiki'], response.json()['has_pages'], response.json()['has_projects'], response.json()['has_issues'], response.json()['has_downloads'], response.json()['pushed_at'], response.json()['created_at'], response.json()['updated_at']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{response.json()['name']}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(repo_profile_fields) - write_csv.writerow(repo_profile_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - +# create .csv for repository profile +def log_repo_profile(response): + repo_profile_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?', + 'Has projects?', 'Has issues?', 'Has downloads?', 'Pushed at', 'Created at', 'Updated at'] + repo_profile_row = [response.json()['name'], response.json()['id'], response.json()['description'], + response.json()['forks'], response.json()['stargazers_count'], response.json()['watchers'], + response.json()['license'], response.json()['default_branch'], response.json()['visibility'], + response.json()['language'], response.json()['open_issues'], response.json()['topics'], + response.json()['homepage'], response.json()['clone_url'], response.json()['ssh_url'], + response.json()['fork'], response.json()['allow_forking'], response.json()['private'], + response.json()['archived'], response.json()['is_template'], response.json()['has_wiki'], + response.json()['has_pages'], response.json()['has_projects'], response.json()['has_issues'], + response.json()['has_downloads'], response.json()['pushed_at'], response.json()['created_at'], + response.json()['updated_at']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{response.json()['name']}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(repo_profile_fields) + write_csv.writerow(repo_profile_row) - # create .csv for repository path contents - def log_repo_path_contents(content, repo_name): - path_content_fields = ['Filename', 'Size (bytes)', 'Type', 'Path', 'SHA', 'URL'] - path_content_row = [content['name'], content['size'], content['type'], content['path'], content['sha'], content['html_url']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{content['name']}_content_from_{repo_name}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(path_content_fields) - write_csv.writerow(path_content_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") - - # create .csv for repository stargazer - def log_repo_stargazers(stargazer, repo_name): - user_follower_fields = ['Profile photo', 'Username', 'ID', 'Node ID', 'Gravatar ID', 'Account type', 'Is site admin?', 'URL'] - user_follower_row = [stargazer['avatar_url'], stargazer['login'], stargazer['id'], stargazer['node_id'], stargazer['gravatar_id'], stargazer['type'], stargazer['site_admin'], stargazer['html_url']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{stargazer['login']}_stargazer_of_{repo_name}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(user_follower_fields) - write_csv.writerow(user_follower_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") - # create .csv for repository forks - def log_repo_forks(fork, count): - repo_fork_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?', 'Has projects?', 'Has issues?', 'Has downloads?', 'Pushed at', 'Created at', 'Updated at'] - repo_fork_row = [fork['full_name'], fork['id'], fork['description'], fork['forks'], fork['stargazers_count'], fork['watchers'], fork['license'], fork['default_branch'], fork['visibility'], fork['language'], fork['open_issues'], fork['topics'], fork['homepage'], fork['clone_url'], fork['ssh_url'], fork['fork'], fork['allow_forking'], fork['private'], fork['archived'], fork['is_template'], fork['has_wiki'], fork['has_pages'], fork['has_projects'], fork['has_issues'], fork['has_downloads'], fork['pushed_at'], fork['created_at'], fork['updated_at']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{fork['name']}_fork_{count}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(repo_fork_fields) - write_csv.writerow(repo_fork_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - # create .csv for repository issues - def log_repo_issues(issue, repo_name): - repo_issue_fields = ['Title', 'ID', 'Node ID', 'Number', 'State', 'Reactions', 'Comments', 'Milestone', 'Assignee', 'Assignees', 'Author association', 'Labels', 'Is locked?', 'Lock reason', 'Closed at', 'Created at', 'Updated at'] - repo_issue_row = [issue['title'], issue['id'], issue['node_id'], issue['number'], issue['state'], issue['reactions'], issue['comments'], issue['milestone'], issue['assignee'], issue['assignees'], issue['author_association'], issue['labels'], issue['locked'], issue['active_lock_reason'], issue['closed_at'], issue['created_at'], issue['updated_at']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{repo_name}_issue_{issue['id']}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(repo_issue_fields) - write_csv.writerow(repo_issue_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") +# create .csv for repository path contents +def log_repo_path_contents(content, repo_name): + path_content_fields = ['Filename', 'Size (bytes)', 'Type', 'Path', 'SHA', 'URL'] + path_content_row = [content['name'], content['size'], content['type'], content['path'], content['sha'], + content['html_url']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{content['name']}_content_from_{repo_name}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(path_content_fields) + write_csv.writerow(path_content_row) - - # create .csv for repository releases - def log_repo_releases(release, repo_name): - repo_release_fields = ['Name', 'ID', 'Node ID', 'Tag', 'Branch', 'Assets', 'Is draft?', 'Is prerelease?', 'Created at', 'Published at'] - repo_release_row = [release['name'], release['id'], release['node_id'], release['tag_name'], release['target_commitish'], release['assets'], release['draft'], release['prerelease'], release['created_at'], release['published_at']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{repo_name}_release_{release['name']}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(repo_release_fields) - write_csv.writerow(repo_release_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - - # Create .csv file for repository contributors - def log_repo_contributors(contributor, repo_name): - repo_contributor_fields = ['Profile photo', 'Username', 'ID', 'Node ID', 'Gravatar ID', 'Account type', 'Is site admin?', 'URL'] - repo_contributor_row = [contributor['avatar_url'], contributor['login'], contributor['id'], contributor['node_id'], contributor['gravatar_id'], contributor['type'], contributor['site_admin'], contributor['html_url']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{contributor['login']}_contributor_of_{repo_name}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(repo_contributor_fields) - write_csv.writerow(repo_contributor_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - print(f"{MessagePrefix.info} {LogRoller.logging_skipped}\n") - - - # Create .csv for organization' events - def log_repo_events(event, organization): - org_event_fields = ['ID', 'Type', 'Created at', 'Payload'] - org_event_row = [event['id'], event['type'], event['created_at'], event['payload']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{organization}_event_{event['id']}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(org_event_fields) - write_csv.writerow(org_event_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - - # Create .csv for organization' repositories - def log_org_repos(repository, organization): - 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?', 'Has projects?', 'Has issues?', 'Has downloads?', 'Pushed at', 'Created at', 'Updated at'] - org_repo_row = [repository['full_name'], repository['id'], repository['description'], repository['forks'], repository['stargazers_count'], repository['watchers'], repository['license'], repository['default_branch'], repository['visibility'], repository['language'], repository['open_issues'], repository['topics'], repository['homepage'], repository['clone_url'], repository['ssh_url'], repository['fork'], repository['allow_forking'], repository['private'], repository['archived'], repository['is_template'], repository['has_wiki'], repository['has_pages'], repository['has_projects'], repository['has_issues'], repository['has_downloads'], repository['pushed_at'], repository['created_at'], repository['updated_at']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{repository['name']}_repository_of_{organization}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(org_repo_fields) - write_csv.writerow(org_repo_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - - # .csv for user' repositories - def log_user_repos(repository, username): - user_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?', 'Has projects?', 'Has issues?', 'Has downloads?', 'Pushed at', 'Created at', 'Updated at'] - user_repo_row = [repository['full_name'], repository['id'], repository['description'], repository['forks'], repository['stargazers_count'], repository['watchers'], repository['license'], repository['default_branch'], repository['visibility'], repository['language'], repository['open_issues'], repository['topics'], repository['homepage'], repository['clone_url'], repository['ssh_url'], repository['fork'], repository['allow_forking'], repository['private'], repository['archived'], repository['is_template'], repository['has_wiki'], repository['has_pages'], repository['has_projects'], repository['has_issues'], repository['has_downloads'], repository['pushed_at'], repository['created_at'], repository['updated_at']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{repository['name']}_{username}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(user_repo_fields) - write_csv.writerow(user_repo_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - - # .csv for user events - def log_user_events(event): - user_event_fields = ['Actor', 'Type', 'Repository', 'Created at', 'Payload'] - user_event_row = [event['actor']['login'], event['type'], event['repo']['name'], event['created_at'], event['payload']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{event['actor']['login']}_event_{event['id']}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(user_event_fields) - write_csv.writerow(user_event_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - - # .csv for user gists - def log_user_gists(gist): - user_gist_fields = ['ID', 'Node ID', 'About', 'Comments', 'Files', 'Git Push URL', 'Is public?', 'Is truncated?', 'Updated at'] - user_gist_row = [gist['id'], gist['node_id'], gist['description'], gist['comments'], gist['files'], gist['git_push_url'], gist['public'], gist['truncated'], gist['updated_at']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{gist['id']}_gists_{gist['owner']['login']}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(user_gist_fields) - write_csv.writerow(user_gist_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - - # .csv for user followers - def log_user_followers(follower, username): - user_follower_fields = ['Profile photo', 'Username', 'ID', 'Node ID', 'Gravatar ID', 'Account type', 'Is site admin?', 'URL'] - user_follower_row = [follower['avatar_url'], follower['login'], follower['id'], follower['node_id'], follower['gravatar_id'], follower['type'], follower['site_admin'], follower['html_url']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(f"output/{follower['login']}_follower_of_{username}.csv", 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(user_follower_fields) - write_csv.writerow(user_follower_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - - # .csv for user following - def log_user_following(user, username): - user_following_fields = ['Profile photo', 'Username', 'ID', 'Node ID', 'Gravatar ID', 'Account type', 'Is site admin?', 'URL'] - user_following_row = [user['avatar_url'], user['login'], user['id'], user['node_id'], user['gravatar_id'], user['type'], user['site_admin'], user['html_url']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{user['login']}_followed_by_{username}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(user_following_fields) - write_csv.writerow(user_following_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - - # .csv for user' subscriptions - def log_user_subscriptions(repository, username): - user_subscription_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?', 'Has projects?', 'Has issues?', 'Has downloads?', 'Pushed at', 'Created at', 'Updated at'] - user_subscription_row = [repository['name'], repository['id'], repository['description'], repository['forks'], repository['stargazers_count'], repository['watchers'], repository['license'], repository['default_branch'], repository['visibility'], repository['language'], repository['open_issues'], repository['topics'], repository['homepage'], repository['clone_url'], repository['ssh_url'], repository['fork'], repository['allow_forking'], repository['private'], repository['archived'], repository['is_template'], repository['has_wiki'], repository['has_pages'], repository['has_projects'], repository['has_issues'], repository['has_downloads'], repository['pushed_at'], repository['created_at'], repository['updated_at']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{username}_subscriptions_{repository['name']}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(user_subscription_fields) - write_csv.writerow(user_subscription_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - - # .csv for user organizations - def log_user_orgs(organization, 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']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{organization['login']}_{username}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(user_org_fields) - write_csv.writerow(user_org_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") - # Create .csv for user search - def log_users_search(user, query): - user_search_fields = ['Profile photo', 'Username', 'ID', 'Node ID', 'Gravatar ID', 'Account type', 'Is site admin?', 'URL'] - user_search_row = [user['avatar_url'], user['login'], user['id'], user['node_id'], user['gravatar_id'], user['type'], user['site_admin'], user['html_url']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{user['login']}_user_search_result_for_{query}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(user_search_fields) - write_csv.writerow(user_search_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - - # Create .csv for repository search - def log_repos_search(repository, query): - repo_search_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?', 'Has projects?', 'Has issues?', 'Has downloads?', 'Pushed at', 'Created at', 'Updated at'] - repo_search_row = [repository['full_name'], repository['id'], repository['description'], repository['forks'], repository['stargazers_count'], repository['watchers'], repository['license'], repository['default_branch'], repository['visibility'], repository['language'], repository['open_issues'], repository['topics'], repository['homepage'], repository['clone_url'], repository['ssh_url'], repository['fork'], repository['allow_forking'], repository['private'], repository['archived'], repository['is_template'], repository['has_wiki'], repository['has_pages'], repository['has_projects'], repository['has_issues'], repository['has_downloads'], repository['pushed_at'], repository['created_at'], repository['updated_at']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{repository['name']}_repository_search_result_for_{query}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(repo_search_fields) - write_csv.writerow(repo_search_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - - # Create .csv for topic search - def log_topics_search(topic, query): - topic_search_fields = ['Name', 'Score', 'Curated', 'Featured', 'Display name', 'Created by', 'Created at', 'Updated at'] - topic_search_row = [topic['name'], topic['score'], topic['curated'], topic['featured'], topic['display_name'], topic['created_by'], topic['created_at'], topic['updated_at']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{topic['name']}_topic_search_result_for_{query}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(topic_search_fields) - write_csv.writerow(topic_search_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - - # Create .csv for issues search - def log_issues_search(issue, query): - issue_search_fields = ['Title', 'ID', 'Node ID', 'Number', 'State', 'Reactions', 'Comments', 'Milestone', 'Assignee', 'Assignees', 'Author association', 'Labels', 'Is locked?', 'Lock reason', 'Closed at', 'Created at', 'Updated at'] - issue_search_row = [issue['title'], issue['id'], issue['node_id'], issue['number'], issue['state'], issue['reactions'], issue['comments'], issue['milestone'], issue['assignee'], issue['assignees'], issue['author_association'], issue['labels'], issue['locked'], issue['active_lock_reason'], issue['closed_at'], issue['created_at'], issue['updated_at']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{issue['id']}_issue_search_result_for_{query}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(issue_search_fields) - write_csv.writerow(issue_search_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") - - - # Create .csv for commits search - def log_commits_search(commit, query): - commit_search_fields = ['SHA', 'Author', 'Username', 'Email', 'Committer', 'Repository', 'URL', 'Description'] - commit_search_row = [commit['commit']['tree']['sha'], commit['commit']['author']['name'], commit['author']['login'], commit['commit']['author']['email'], commit['commit']['committer']['name'], commit['repository']['full_name'], commit['html_url'], commit['commit']['message']] - xprint(f"\n{MessagePrefix.prompt} {LogRoller.prompt_log_csv}", end="");prompt = input().lower() - if prompt == "yes": - with open(os.path.join("output", f"{commit['commit']['tree']['sha']}_commit_search_result_for_{query}.csv"), 'w') as file: - write_csv = csv.writer(file) - write_csv.writerow(commit_search_fields) - write_csv.writerow(commit_search_row) - - logging.info(LogRoller.logged_to_csv.format(file.name)) - xprint(f"{MessagePrefix.positive} {LogRoller.logged_to_csv.format(file.name)}") - - else: - logging.info(LogRoller.logging_skipped.format(prompt)) - xprint(f"{MessagePrefix.info} {LogRoller.logging_skipped.format(prompt)}") + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# create .csv for repository stargazer +def log_repo_stargazers(stargazer, repo_name): + user_follower_fields = ['Profile photo', 'Username', 'ID', 'Node ID', 'Gravatar ID', 'Account type', + 'Is site admin?', 'URL'] + user_follower_row = [stargazer['avatar_url'], stargazer['login'], stargazer['id'], stargazer['node_id'], + stargazer['gravatar_id'], stargazer['type'], stargazer['site_admin'], stargazer['html_url']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{stargazer['login']}_stargazer_of_{repo_name}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(user_follower_fields) + write_csv.writerow(user_follower_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# create .csv for repository forks +def log_repo_forks(fork, count): + repo_fork_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?', + 'Has projects?', 'Has issues?', 'Has downloads?', 'Pushed at', 'Created at', 'Updated at'] + repo_fork_row = [fork['full_name'], fork['id'], fork['description'], fork['forks'], fork['stargazers_count'], + fork['watchers'], fork['license'], fork['default_branch'], fork['visibility'], fork['language'], + fork['open_issues'], fork['topics'], fork['homepage'], fork['clone_url'], fork['ssh_url'], + fork['fork'], fork['allow_forking'], fork['private'], fork['archived'], fork['is_template'], + fork['has_wiki'], fork['has_pages'], fork['has_projects'], fork['has_issues'], + fork['has_downloads'], fork['pushed_at'], fork['created_at'], fork['updated_at']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{fork['name']}_fork_{count}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(repo_fork_fields) + write_csv.writerow(repo_fork_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# create .csv for repository issues +def log_repo_issues(issue, repo_name): + repo_issue_fields = ['Title', 'ID', 'Node ID', 'Number', 'State', 'Reactions', 'Comments', 'Milestone', 'Assignee', + 'Assignees', 'Author association', 'Labels', 'Is locked?', 'Lock reason', 'Closed at', + 'Created at', 'Updated at'] + repo_issue_row = [issue['title'], issue['id'], issue['node_id'], issue['number'], issue['state'], + issue['reactions'], issue['comments'], issue['milestone'], issue['assignee'], issue['assignees'], + issue['author_association'], issue['labels'], issue['locked'], issue['active_lock_reason'], + issue['closed_at'], issue['created_at'], issue['updated_at']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{repo_name}_issue_{issue['id']}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(repo_issue_fields) + write_csv.writerow(repo_issue_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# create .csv for repository releases +def log_repo_releases(release, repo_name): + repo_release_fields = ['Name', 'ID', 'Node ID', 'Tag', 'Branch', 'Assets', 'Is draft?', 'Is prerelease?', + 'Created at', 'Published at'] + repo_release_row = [release['name'], release['id'], release['node_id'], release['tag_name'], + release['target_commitish'], release['assets'], release['draft'], release['prerelease'], + release['created_at'], release['published_at']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{repo_name}_release_{release['name']}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(repo_release_fields) + write_csv.writerow(repo_release_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# Create .csv file for repository contributors +def log_repo_contributors(contributor, repo_name): + repo_contributor_fields = ['Profile photo', 'Username', 'ID', 'Node ID', 'Gravatar ID', 'Account type', + 'Is site admin?', 'URL'] + repo_contributor_row = [contributor['avatar_url'], contributor['login'], contributor['id'], contributor['node_id'], + contributor['gravatar_id'], contributor['type'], contributor['site_admin'], + contributor['html_url']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{contributor['login']}_contributor_of_{repo_name}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(repo_contributor_fields) + write_csv.writerow(repo_contributor_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + print(f"{INFO} {logging_skipped}\n") + + +# Create .csv for organization' events +def log_repo_events(event, organization): + org_event_fields = ['ID', 'Type', 'Created at', 'Payload'] + org_event_row = [event['id'], event['type'], event['created_at'], event['payload']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{organization}_event_{event['id']}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(org_event_fields) + write_csv.writerow(org_event_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# Create .csv for organization' repositories +def log_org_repos(repository, organization): + 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?', + 'Has projects?', 'Has issues?', 'Has downloads?', 'Pushed at', 'Created at', 'Updated at'] + org_repo_row = [repository['full_name'], repository['id'], repository['description'], repository['forks'], + repository['stargazers_count'], repository['watchers'], repository['license'], + repository['default_branch'], repository['visibility'], repository['language'], + repository['open_issues'], repository['topics'], repository['homepage'], repository['clone_url'], + repository['ssh_url'], repository['fork'], repository['allow_forking'], repository['private'], + repository['archived'], repository['is_template'], repository['has_wiki'], repository['has_pages'], + repository['has_projects'], repository['has_issues'], repository['has_downloads'], + repository['pushed_at'], repository['created_at'], repository['updated_at']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{repository['name']}_repository_of_{organization}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(org_repo_fields) + write_csv.writerow(org_repo_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# .csv for user' repositories +def log_user_repos(repository, username): + user_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?', + 'Has projects?', 'Has issues?', 'Has downloads?', 'Pushed at', 'Created at', 'Updated at'] + user_repo_row = [repository['full_name'], repository['id'], repository['description'], repository['forks'], + repository['stargazers_count'], repository['watchers'], repository['license'], + repository['default_branch'], repository['visibility'], repository['language'], + repository['open_issues'], repository['topics'], repository['homepage'], repository['clone_url'], + repository['ssh_url'], repository['fork'], repository['allow_forking'], repository['private'], + repository['archived'], repository['is_template'], repository['has_wiki'], repository['has_pages'], + repository['has_projects'], repository['has_issues'], repository['has_downloads'], + repository['pushed_at'], repository['created_at'], repository['updated_at']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{repository['name']}_{username}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(user_repo_fields) + write_csv.writerow(user_repo_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# .csv for user events +def log_user_events(event): + user_event_fields = ['Actor', 'Type', 'Repository', 'Created at', 'Payload'] + user_event_row = [event['actor']['login'], event['type'], event['repo']['name'], event['created_at'], + event['payload']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{event['actor']['login']}_event_{event['id']}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(user_event_fields) + write_csv.writerow(user_event_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# .csv for user gists +def log_user_gists(gist): + user_gist_fields = ['ID', 'Node ID', 'About', 'Comments', 'Files', 'Git Push URL', 'Is public?', 'Is truncated?', + 'Updated at'] + user_gist_row = [gist['id'], gist['node_id'], gist['description'], gist['comments'], gist['files'], + gist['git_push_url'], gist['public'], gist['truncated'], gist['updated_at']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{gist['id']}_gists_{gist['owner']['login']}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(user_gist_fields) + write_csv.writerow(user_gist_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# .csv for user followers +def log_user_followers(follower, username): + user_follower_fields = ['Profile photo', 'Username', 'ID', 'Node ID', 'Gravatar ID', 'Account type', + 'Is site admin?', 'URL'] + user_follower_row = [follower['avatar_url'], follower['login'], follower['id'], follower['node_id'], + follower['gravatar_id'], follower['type'], follower['site_admin'], follower['html_url']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(f"output/{follower['login']}_follower_of_{username}.csv", 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(user_follower_fields) + write_csv.writerow(user_follower_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# .csv for user following +def log_user_following(user, username): + user_following_fields = ['Profile photo', 'Username', 'ID', 'Node ID', 'Gravatar ID', 'Account type', + 'Is site admin?', 'URL'] + user_following_row = [user['avatar_url'], user['login'], user['id'], user['node_id'], user['gravatar_id'], + user['type'], user['site_admin'], user['html_url']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{user['login']}_followed_by_{username}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(user_following_fields) + write_csv.writerow(user_following_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# .csv for user' subscriptions +def log_user_subscriptions(repository, username): + user_subscription_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?', 'Has projects?', 'Has issues?', 'Has downloads?', 'Pushed at', + 'Created at', 'Updated at'] + user_subscription_row = [repository['name'], repository['id'], repository['description'], repository['forks'], + repository['stargazers_count'], repository['watchers'], repository['license'], + repository['default_branch'], repository['visibility'], repository['language'], + repository['open_issues'], repository['topics'], repository['homepage'], + repository['clone_url'], repository['ssh_url'], repository['fork'], + repository['allow_forking'], repository['private'], repository['archived'], + repository['is_template'], repository['has_wiki'], repository['has_pages'], + repository['has_projects'], repository['has_issues'], repository['has_downloads'], + repository['pushed_at'], repository['created_at'], repository['updated_at']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{username}_subscriptions_{repository['name']}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(user_subscription_fields) + write_csv.writerow(user_subscription_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# .csv for user organizations +def log_user_orgs(organization, 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']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{organization['login']}_{username}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(user_org_fields) + write_csv.writerow(user_org_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# Create .csv for user search +def log_users_search(user, query): + user_search_fields = ['Profile photo', 'Username', 'ID', 'Node ID', 'Gravatar ID', 'Account type', 'Is site admin?', + 'URL'] + user_search_row = [user['avatar_url'], user['login'], user['id'], user['node_id'], user['gravatar_id'], + user['type'], user['site_admin'], user['html_url']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{user['login']}_user_search_result_for_{query}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(user_search_fields) + write_csv.writerow(user_search_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# Create .csv for repository search +def log_repos_search(repository, query): + repo_search_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?', + 'Has projects?', 'Has issues?', 'Has downloads?', 'Pushed at', 'Created at', 'Updated at'] + repo_search_row = [repository['full_name'], repository['id'], repository['description'], repository['forks'], + repository['stargazers_count'], repository['watchers'], repository['license'], + repository['default_branch'], repository['visibility'], repository['language'], + repository['open_issues'], repository['topics'], repository['homepage'], repository['clone_url'], + repository['ssh_url'], repository['fork'], repository['allow_forking'], repository['private'], + repository['archived'], repository['is_template'], repository['has_wiki'], + repository['has_pages'], repository['has_projects'], repository['has_issues'], + repository['has_downloads'], repository['pushed_at'], repository['created_at'], + repository['updated_at']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{repository['name']}_repository_search_result_for_{query}.csv"), + 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(repo_search_fields) + write_csv.writerow(repo_search_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + # Create .csv for topic search + + +def log_topics_search(topic, query): + topic_search_fields = ['Name', 'Score', 'Curated', 'Featured', 'Display name', 'Created by', 'Created at', + 'Updated at'] + topic_search_row = [topic['name'], topic['score'], topic['curated'], topic['featured'], topic['display_name'], + topic['created_by'], topic['created_at'], topic['updated_at']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{topic['name']}_topic_search_result_for_{query}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(topic_search_fields) + write_csv.writerow(topic_search_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# Create .csv for issues search +def log_issues_search(issue, query): + issue_search_fields = ['Title', 'ID', 'Node ID', 'Number', 'State', 'Reactions', 'Comments', 'Milestone', + 'Assignee', 'Assignees', 'Author association', 'Labels', 'Is locked?', 'Lock reason', + 'Closed at', 'Created at', 'Updated at'] + issue_search_row = [issue['title'], issue['id'], issue['node_id'], issue['number'], issue['state'], + issue['reactions'], issue['comments'], issue['milestone'], issue['assignee'], + issue['assignees'], issue['author_association'], issue['labels'], issue['locked'], + issue['active_lock_reason'], issue['closed_at'], issue['created_at'], issue['updated_at']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{issue['id']}_issue_search_result_for_{query}.csv"), 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(issue_search_fields) + write_csv.writerow(issue_search_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") + + +# Create .csv for commits search +def log_commits_search(commit, query): + commit_search_fields = ['SHA', 'Author', 'Username', 'Email', 'Committer', 'Repository', 'URL', 'Description'] + commit_search_row = [commit['commit']['tree']['sha'], commit['commit']['author']['name'], commit['author']['login'], + commit['commit']['author']['email'], commit['commit']['committer']['name'], + commit['repository']['full_name'], commit['html_url'], commit['commit']['message']] + xprint(f"\n{PROMPT} {prompt_log_csv}", end=""); + prompt = input().lower() + if prompt == "yes": + with open(os.path.join("output", f"{commit['commit']['tree']['sha']}_commit_search_result_for_{query}.csv"), + 'w') as file: + write_csv = csv.writer(file) + write_csv.writerow(commit_search_fields) + write_csv.writerow(commit_search_row) + + logging.info(logged_to_csv.format(file.name)) + xprint(f"{POSITIVE} {logged_to_csv.format(file.name)}") + + else: + logging.info(logging_skipped.format(prompt)) + xprint(f"{INFO} {logging_skipped.format(prompt)}") diff --git a/octosuite/helper.py b/octosuite/helper.py index 7c07283..9c0c10e 100644 --- a/octosuite/helper.py +++ b/octosuite/helper.py @@ -2,163 +2,168 @@ from rich.table import Table from rich import print as xprint from octosuite.colors import white, green, white_bold, green_bold, header_title, reset -# Help -# This class holds the help text for available commands. -class Help: - usage_text = 'Use syntax {} to get started with %s{}%s.' % (green_bold, reset) - usage_text_1 = '%sUse {} to view all available subcommands.%s' % (white, reset) - usage_text_2 = "%sThe {} command works with subcommands. %s" % (white, reset) +# helper.py +# This file holds the help text for available commands. +usage_text = 'Use syntax {} to get started with %s{}%s.' % (green_bold, reset) +usage_text_1 = '%sUse {} to view all available subcommands.%s' % (white, reset) +usage_text_2 = "%sThe {} command works with subcommands. %s" % (white, reset) - def org(): - xprint( - Help.usage_text_2.format(f"{green_bold}org{reset}") + Help.usage_text_1.format( - f"{green_bold}help:org{reset}")) - def repo(): - xprint(Help.usage_text_2.format(f"{green_bold}repo{reset}") + Help.usage_text_1.format( - f"{green_bold}help:repo{reset}")) +def org(): + xprint(usage_text_2.format(f"{green_bold}org{reset}") + usage_text_1.format(f"{green_bold}help:org{reset}")) - def user(): - xprint(Help.usage_text_2.format(f"{green_bold}user{reset}") + Help.usage_text_1.format( - f"{green_bold}help:user{reset}")) - def search(): - xprint(Help.usage_text_2.format(f"{green_bold}search{reset}") + Help.usage_text_1.format( - f"{green_bold}help:search{reset}")) +def repo(): + xprint(usage_text_2.format(f"{green_bold}repo{reset}") + usage_text_1.format(f"{green_bold}help:repo{reset}")) - def source(): - xprint(Help.usage_text_2.format(f"{green_bold}source{reset}") + Help.usage_text_1.format( - f"{green_bold}help:source{reset}")) - def logs(): - xprint(Help.usage_text_2.format(f"{green_bold}logs{reset}") + Help.usage_text_1.format( - f"{green_bold}help:logs{reset}")) +def user(): + xprint(usage_text_2.format(f"{green_bold}user{reset}") + usage_text_1.format(f"{green_bold}help:user{reset}")) - def csv(): - xprint( - Help.usage_text_2.format(f"{green_bold}csv{reset}") + Help.usage_text_1.format( - f"{green_bold}help:csv{reset}")) - def source_command(): - source_cmd_table = Table(show_header=True, header_style=header_title) - source_cmd_table.add_column("Command", style="dim") - source_cmd_table.add_column("Description") - source_cmd_table.add_row("zipball", "Download source code Zipball") - source_cmd_table.add_row("tarball", "Download source code Tarball") +def search(): + xprint(usage_text_2.format(f"{green_bold}search{reset}") + usage_text_1.format(f"{green_bold}help:search{reset}")) - syntax = f"{green}source:{reset}" - xprint(f"{Help.usage_text.format(syntax, 'source code downloads')}") - xprint(source_cmd_table) - def search_command(): - search_cmd_table = Table(show_header=True, header_style=header_title) - search_cmd_table.add_column("Command", style="dim") - search_cmd_table.add_column("Description") - search_cmd_table.add_row("users", "Search user(s)") - search_cmd_table.add_row("repos", "Search repositor[y][ies]") - search_cmd_table.add_row("topics", "Search topic(s)") - search_cmd_table.add_row("issues", "Search issue(s)") - search_cmd_table.add_row("commits", "Search commit(s)") +def source(): + xprint(usage_text_2.format(f"{green_bold}source{reset}") + usage_text_1.format(f"{green_bold}help:source{reset}")) - syntax = f"{green}search:{reset}" - xprint(f"{Help.usage_text.format(syntax, 'target discovery')}") - xprint(search_cmd_table) - 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("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("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)") - user_cmd_table.add_row("followers", "Return a target's followers") - user_cmd_table.add_row("following", "Return a list of users the target is following") - user_cmd_table.add_row("subscriptions", "Return a target's subscriptions") +def logs(): + xprint(usage_text_2.format(f"{green_bold}logs{reset}") + usage_text_1.format(f"{green_bold}help:logs{reset}")) - syntax = f"{green}user:{reset}" - xprint(f"{Help.usage_text.format(syntax, 'user investigation(s)')}") - xprint(user_cmd_table) - 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") +def csv(): + xprint(usage_text_2.format(f"{green_bold}csv{reset}") + usage_text_1.format(f"{green_bold}help:csv{reset}")) - syntax = f"{green}org:{reset}" - xprint(f"{Help.usage_text.format(syntax, 'organization investigation(s)')}") - xprint(org_cmd_table) - def repo_command(): - repo_cmd_table = Table(show_header=True, header_style=header_title) - repo_cmd_table.add_column("Command", style="dim") - repo_cmd_table.add_column("Description") - repo_cmd_table.add_row("profile", "Get a repository's info") - repo_cmd_table.add_row("issues", "Return a repository's issues") - repo_cmd_table.add_row("forks", "Return a repository's forks") - repo_cmd_table.add_row("releases", "Return a repository's releases") - repo_cmd_table.add_row("stargazers", "Return a repository's stargazers") - repo_cmd_table.add_row("contributors", "Return a repository's contributors") - repo_cmd_table.add_row("path_contents", "List contents in a path of a repository") +def source_command(): + source_cmd_table = Table(show_header=True, header_style=header_title) + source_cmd_table.add_column("Command", style="dim") + source_cmd_table.add_column("Description") + source_cmd_table.add_row("zipball", "Download source code Zipball") + source_cmd_table.add_row("tarball", "Download source code Tarball") - syntax = f"{green}repo:{reset}" - xprint(f"{Help.usage_text.format(syntax, 'repository investigation(s)')}") - xprint(repo_cmd_table) + syntax = f"{green}source:{reset}" + xprint(f"{usage_text.format(syntax, 'source code downloads')}") + xprint(source_cmd_table) - def logs_command(): - logs_cmd_table = Table(show_header=True, header_style=header_title) - logs_cmd_table.add_column("Command", style="dim") - logs_cmd_table.add_column("Description") - logs_cmd_table.add_row("view", "View logs") - logs_cmd_table.add_row("read", "Read log") - logs_cmd_table.add_row("delete", "Delete log") - logs_cmd_table.add_row("clear", "clear logs") - syntax = f"{green}logs:{reset}" - xprint(f"{Help.usage_text.format(syntax, 'log(s) management')}") - xprint(logs_cmd_table) +def search_command(): + search_cmd_table = Table(show_header=True, header_style=header_title) + search_cmd_table.add_column("Command", style="dim") + search_cmd_table.add_column("Description") + search_cmd_table.add_row("users", "Search user(s)") + search_cmd_table.add_row("repos", "Search repositor[y][ies]") + search_cmd_table.add_row("topics", "Search topic(s)") + search_cmd_table.add_row("issues", "Search issue(s)") + search_cmd_table.add_row("commits", "Search commit(s)") - def csv_command(): - csv_cmd_table = Table(show_header=True, header_style=header_title) - csv_cmd_table.add_column("Command", style="dim") - csv_cmd_table.add_column("Description") - csv_cmd_table.add_row("view", "View csv files") - csv_cmd_table.add_row("read", "Read csv") - csv_cmd_table.add_row("delete", "Delete csv") - csv_cmd_table.add_row("clear", "clear csv files") + syntax = f"{green}search:{reset}" + xprint(f"{usage_text.format(syntax, 'target discovery')}") + xprint(search_cmd_table) - syntax = f"{green}csv:{reset}" - xprint(f"{Help.usage_text.format(syntax, 'csv management')}") - xprint(csv_cmd_table) - def help_command(): - core_cmd_table = Table(show_header=True, header_style=header_title) - core_cmd_table.add_column("Command", style="dim", width=12) - core_cmd_table.add_column("Description") - core_cmd_table.add_row("help", "Help menu") - core_cmd_table.add_row("exit", "Close session") - core_cmd_table.add_row("clear", "Clear screen") - core_cmd_table.add_row("about", "Program's info") - core_cmd_table.add_row("author", "Developer's info") +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("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("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)") + user_cmd_table.add_row("followers", "Return a target's followers") + user_cmd_table.add_row("following", "Return a list of users the target is following") + user_cmd_table.add_row("subscriptions", "Return a target's subscriptions") - help_sub_cmd_table = Table(show_header=True, header_style=header_title) - help_sub_cmd_table.add_column("Command", style="dim", width=12) - 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("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") - help_sub_cmd_table.add_row("source", "List all source code download commands (for developers)") + syntax = f"{green}user:{reset}" + xprint(f"{usage_text.format(syntax, 'user investigation(s)')}") + xprint(user_cmd_table) - syntax = f"{green}help:{reset}" - xprint(core_cmd_table) - xprint(f"\n\n{Help.usage_text.format(syntax, 'octosuite')}") - xprint(help_sub_cmd_table) \ No newline at end of file + +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") + + syntax = f"{green}org:{reset}" + xprint(f"{usage_text.format(syntax, 'organization investigation(s)')}") + xprint(org_cmd_table) + + +def repo_command(): + repo_cmd_table = Table(show_header=True, header_style=header_title) + repo_cmd_table.add_column("Command", style="dim") + repo_cmd_table.add_column("Description") + repo_cmd_table.add_row("profile", "Get a repository's info") + repo_cmd_table.add_row("issues", "Return a repository's issues") + repo_cmd_table.add_row("forks", "Return a repository's forks") + repo_cmd_table.add_row("releases", "Return a repository's releases") + repo_cmd_table.add_row("stargazers", "Return a repository's stargazers") + repo_cmd_table.add_row("contributors", "Return a repository's contributors") + repo_cmd_table.add_row("path_contents", "List contents in a path of a repository") + + syntax = f"{green}repo:{reset}" + xprint(f"{usage_text.format(syntax, 'repository investigation(s)')}") + xprint(repo_cmd_table) + + +def logs_command(): + logs_cmd_table = Table(show_header=True, header_style=header_title) + logs_cmd_table.add_column("Command", style="dim") + logs_cmd_table.add_column("Description") + logs_cmd_table.add_row("view", "View logs") + logs_cmd_table.add_row("read", "Read log") + logs_cmd_table.add_row("delete", "Delete log") + logs_cmd_table.add_row("clear", "clear logs") + + syntax = f"{green}logs:{reset}" + xprint(f"{usage_text.format(syntax, 'log(s) management')}") + xprint(logs_cmd_table) + + +def csv_command(): + csv_cmd_table = Table(show_header=True, header_style=header_title) + csv_cmd_table.add_column("Command", style="dim") + csv_cmd_table.add_column("Description") + csv_cmd_table.add_row("view", "View csv files") + csv_cmd_table.add_row("read", "Read csv") + csv_cmd_table.add_row("delete", "Delete csv") + csv_cmd_table.add_row("clear", "clear csv files") + + syntax = f"{green}csv:{reset}" + xprint(f"{usage_text.format(syntax, 'csv management')}") + xprint(csv_cmd_table) + + +def help_command(): + core_cmd_table = Table(show_header=True, header_style=header_title) + core_cmd_table.add_column("Command", style="dim", width=12) + core_cmd_table.add_column("Description") + core_cmd_table.add_row("help", "Help menu") + core_cmd_table.add_row("exit", "Close session") + core_cmd_table.add_row("clear", "Clear screen") + core_cmd_table.add_row("about", "Program's info") + core_cmd_table.add_row("author", "Developer's info") + + help_sub_cmd_table = Table(show_header=True, header_style=header_title) + help_sub_cmd_table.add_column("Command", style="dim", width=12) + 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("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") + help_sub_cmd_table.add_row("source", "List all source code download commands (for developers)") + + syntax = f"{green}help:{reset}" + xprint(core_cmd_table) + xprint(f"\n\n{usage_text.format(syntax, 'octosuite')}") + xprint(help_sub_cmd_table) diff --git a/octosuite/log_roller.py b/octosuite/log_roller.py index c0ba3cc..749be36 100644 --- a/octosuite/log_roller.py +++ b/octosuite/log_roller.py @@ -2,24 +2,23 @@ # cases (they're being used by logging to be written to log files, and being printed out to the screen). -class LogRoller: - ctrl_c = "Session terminated with Ctrl+C." - error = "An error occurred: {}" - session_opened = "Opened new session on {}:{}" - session_closed = "Session closed at {}." - viewing_logs = "Viewing logs" - viewing_csv = "Viewing CSV file(s)..." - deleted_log = "Deleted log: {}" - reading_log = "Reading log: {}" - reading_csv = 'Reading csv: {}' - deleted_csv = 'Deleted csv: {}' - 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: @{}" - repo_or_user_not_found = "Repository or User not found: {}, @{}" - prompt_log_csv = "Do you wish to log this output to a CSV file? (yes/no) " - logged_to_csv = "Output logged: {}" - logging_skipped = "Logging skipped: {}" - limit_output = "Limit '{}' output to how many? (1-100) " +ctrl_c = "Session terminated with Ctrl+C." +error = "An error occurred: {}" +session_opened = "Opened new session on {}:{}" +session_closed = "Session closed at {}." +viewing_logs = "Viewing logs" +viewing_csv = "Viewing CSV file(s)..." +deleted_log = "Deleted log: {}" +reading_log = "Reading log: {}" +reading_csv = 'Reading csv: {}' +deleted_csv = 'Deleted csv: {}' +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: @{}" +repo_or_user_not_found = "Repository or User not found: {}, @{}" +prompt_log_csv = "Do you wish to log this output to a CSV file? (yes/no) " +logged_to_csv = "Output logged: {}" +logging_skipped = "Logging skipped: {}" +limit_output = "Limit '{}' output to how many? (1-100) " diff --git a/octosuite/main.py b/octosuite/main.py index 915c60c..df3bbeb 100644 --- a/octosuite/main.py +++ b/octosuite/main.py @@ -1,5 +1,5 @@ # import everything from the octosuite.py file -from octosuite.octosuite import * +from octosuite.octosuite import * # I drifted away from the 'pythonic way' here def octosuite(): @@ -8,9 +8,9 @@ def octosuite(): run.on_start() except KeyboardInterrupt: - logging.warning(LogRoller.ctrl_c) - xprint(f"{MessagePrefix.warning} {LogRoller.ctrl_c}") + logging.warning(ctrl_c) + xprint(f"{WARNING} {ctrl_c}") except Exception as e: - logging.error(LogRoller.error.format(e)) - xprint(f"{MessagePrefix.error} {LogRoller.error.format(e)}") + logging.error(error.format(e)) + xprint(f"{ERROR} {error.format(e)}") diff --git a/octosuite/message_prefixes.py b/octosuite/message_prefixes.py index f65d0e4..98e98aa 100644 --- a/octosuite/message_prefixes.py +++ b/octosuite/message_prefixes.py @@ -1,14 +1,13 @@ from octosuite.colors import red, white, green, reset """ -MessagePrefix *Even here, I couldn't think of a good name.* The Attributes class holds the signs/symbols that show what +message prefixes that show what a notification in OctoSuite might be all about. This might not be very important or necessary in some cases, -but I think it's better to know the severity of the notifications you get in a program. +but I think it's better to know the severity of the notifications you get in a program. """ -class MessagePrefix: - prompt = f"{white}[{green}?{white}]{reset}" - warning = f"{white}[{red}!{white}]{reset}" - error = f"{white}[{red}x{white}]{reset}" - positive = f"{white}[{green}+{white}]{reset}" - negative = f"{white}[{red}-{white}]{reset}" - info = f"{white}[{green}*{white}]{reset}" +PROMPT = f"{white}[{green}PROMPT{white}]{reset}" +WARNING = f"{white}[{red}WARNING{white}]{reset}" +ERROR = f"{white}[{red}ERROR{white}]{reset}" +POSITIVE = f"{white}[{green}POSITIVE{white}]{reset}" +NEGATIVE = f"{white}[{red}NEGATIVE{white}]{reset}" +INFO = f"{white}[{green}INFO{white}]{reset}" \ No newline at end of file diff --git a/octosuite/octosuite.py b/octosuite/octosuite.py index 0063e78..d4aef4d 100644 --- a/octosuite/octosuite.py +++ b/octosuite/octosuite.py @@ -12,12 +12,13 @@ from rich.tree import Tree from rich.table import Table from datetime import datetime from rich import print as xprint -from octosuite.helper import Help -from octosuite.log_roller import LogRoller -from octosuite.csv_loggers import CsvLogger -from octosuite.message_prefixes import MessagePrefix from octosuite.banners import version_tag, ascii_banner +from octosuite.message_prefixes import PROMPT, WARNING, ERROR, POSITIVE, NEGATIVE, INFO # wondering why I name all the variables instead of just using the * wildcard?, because it's the pythonic way lol from octosuite.colors import red, white, green, white_bold, green_bold, header_title, reset +# 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.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, deleted_log, reading_log, reading_csv, deleted_csv, file_downloading, file_downloaded, info_not_found, user_not_found, org_not_found, repo_or_user_not_found, prompt_log_csv, logged_to_csv, logging_skipped, limit_output +from octosuite.csv_loggers import log_org_profile, log_user_profile, log_repo_profile, log_repo_path_contents, log_repo_contributors, log_repo_startgazers, log_repo_forks, log_repo_issues, log_repo_releases, log_org_repos, log_org_events, log_user_repos, log_user_gists, log_user_orgs, log_user_events, log_user_subscriptions, log_user_following, log_user_followers, log_repos_search, log_users_search, log_topics_search, log_issues_search, log_commits_search class Octosuite: @@ -30,23 +31,23 @@ class Octosuite: ("clear", self.clear_screen), ("about", self.about), ("author", self.author), - ("help", Help.help_command), - ("help:source", Help.source_command), - ("help:search", Help.search_command), - ("help:user", Help.user_command), - ("help:repo", Help.repo_command), - ("help:logs", Help.logs_command), - ("help:csv", Help.csv_command), - ("help:org", Help.org_command), - ("source", Help.source), + ("help", help_command), + ("help:source", source_command), + ("help:search", search_command), + ("help:user", user_command), + ("help:repo", repo_command), + ("help:logs", logs_command), + ("help:csv", csv_command), + ("help:org", org_command), + ("source", source), ("source:tarball", self.download_tarball), ("source:zipball", self.download_zipball), - ("org", Help.org), + ("org", org), ("org:events", self.org_events), ("org:profile", self.org_profile), ("org:repos", self.org_repos), ("org:member", self.org_member), - ("repo", Help.repo), + ("repo", repo), ("repo:path_contents", self.path_contents), ("repo:profile", self.repo_profile), ("repo:contributors", self.repo_contributors), @@ -54,7 +55,7 @@ class Octosuite: ("repo:forks", self.repo_forks), ("repo:issues", self.repo_issues), ("repo:releases", self.repo_releases), - ("user", Help.user), + ("user", user), ("user:repos", self.user_repos), ("user:gists", self.user_gists), ("user:orgs", self.user_orgs), @@ -64,18 +65,18 @@ class Octosuite: ("user:follows", self.user_follows), ("user:following", self.user_following), ("user:subscriptions", self.user_subscriptions), - ("search", Help.search), + ("search", search), ("search:users", self.users_search), ("search:repos", self.repos_search), ("search:topics", self.topics_search), ("search:issues", self.issues_search), ("search:commits", self.commits_search), - ("logs", Help.logs), + ("logs", logs), ("logs:view", self.view_logs), ("logs:read", self.read_log), ("logs:delete", self.delete_log), ("logs:clear", self.clear_logs), - ("csv", Help.csv), + ("csv", csv), ("csv:view", self.view_csv), ("csv:read", self.read_csv), ("csv:delete", self.delete_csv), @@ -313,7 +314,7 @@ class Octosuite: logging.basicConfig(filename=f".logs/{now_formatted}.log", format="[%(asctime)s] [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S%p", level=logging.DEBUG) # Log the start of a session - logging.info(LogRoller.session_opened.format(platform.node(), getpass.getuser())) + logging.info(session_opened.format(platform.node(), getpass.getuser())) # Check for updates @@ -365,13 +366,13 @@ class Octosuite: organization = input() response = requests.get(f"{self.endpoint}/orgs/{organization}") if response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.org_not_found.format(organization)}") + xprint(f"{NEGATIVE} {org_not_found.format(organization)}") elif response.status_code == 200: org_profile_tree = Tree("\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) - CsvLogger.log_org_profile(response) + log_org_profile(response) else: xprint(response.json()) @@ -382,13 +383,13 @@ class Octosuite: username = input() response = requests.get(f"{self.endpoint}/users/{username}") if response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.userNotFound.format(username)}") + xprint(f"{NEGATIVE} {user_not_found.format(username)}") elif response.status_code == 200: user_profile_tree = Tree("\n" + response.json()['name']) for attr in self.profile_attrs: user_profile_tree.add(f"{self.profile_attr_dict[attr]}: {response.json()[attr]}") xprint(user_profile_tree) - CsvLogger.log_user_profile(response) + log_user_profile(response) else: xprint(response.json()) @@ -401,13 +402,13 @@ class Octosuite: username = input() response = requests.get(f"{self.endpoint}/repos/{username}/{repo_name}") if response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.repo_or_user_not_found.format(repo_name, username)}") + xprint(f"{NEGATIVE} {repo_or_user_not_found.format(repo_name, username)}") elif response.status_code == 200: repo_profile_tree = Tree("\n" + response.json()['full_name']) for attr in self.repo_attrs: repo_profile_tree.add(f"{self.repo_attr_dict[attr]}: {response.json()[attr]}") xprint(repo_profile_tree) - CsvLogger.log_repo_profile(response) + log_repo_profile(response) else: xprint(response.json()) @@ -422,15 +423,15 @@ class Octosuite: path_name = input() response = requests.get(f"{self.endpoint}/repos/{username}/{repo_name}/contents/{path_name}") if response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.info_not_found.format(repo_name, username, path_name)}") + xprint(f"{NEGATIVE} {info_not_found.format(repo_name, username, path_name)}") elif response.status_code == 200: for content_count, content in enumerate(response.json(), start=1): path_contents_tree = Tree("\n" + content['name']) for attr in self.path_attrs: path_contents_tree.add(f"{self.path_attr_dict[attr]}: {content[attr]}") xprint(path_contents_tree) - CsvLogger.log_repo_path_contents(content, repo_name) - xprint(MessagePrefix.info, f"Found {content_count} file(s) in {repo_name}/{path_name}.") + log_repo_path_contents(content, repo_name) + xprint(INFO, f"Found {content_count} file(s) in {repo_name}/{path_name}.") else: xprint(response.json()) @@ -441,18 +442,18 @@ class Octosuite: repo_name = input() xprint(f"{white}>> @{green}Owner{white} (username) ", end="") username = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("contributors"), end="") + xprint(PROMPT, limit_output.format("contributors"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/repos/{username}/{repo_name}/contributors?per_page={limit}") if response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.repo_or_user_not_found.format(repo_name, username)}") + xprint(f"{NEGATIVE} {repo_or_user_not_found.format(repo_name, username)}") elif response.status_code == 200: for contributor in response.json(): contributor_tree = Tree("\n" + contributor['login']) for attr in self.user_attrs: contributor_tree.add(f"{self.user_attr_dict[attr]}: {contributor[attr]}") xprint(contributor_tree) - CsvLogger.log_repo_contributors(contributor, repo_name) + log_repo_contributors(contributor, repo_name) else: xprint(response.json()) @@ -463,20 +464,20 @@ class Octosuite: repo_name = input() xprint(f"{white}>> @{green}Owner{white} (username){reset} ", end="") username = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("repository stargazers"), end="") + xprint(PROMPT, limit_output.format("repository stargazers"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/repos/{username}/{repo_name}/stargazers?per_page={limit}") if response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.repo_or_user_not_found.format(repo_name, username)}") + xprint(f"{NEGATIVE} {repo_or_user_not_found.format(repo_name, username)}") elif response.json() == {}: - xprint(f"{MessagePrefix.negative} Repository does not have any stargazers -> ({repo_name})") + xprint(f"{NEGATIVE} Repository does not have any stargazers -> ({repo_name})") elif response.status_code == 200: for stargazer in response.json(): stargazer_tree = Tree("\n" + stargazer['login']) for attr in self.user_attrs: stargazer_tree.add(f"{self.user_attr_dict[attr]}: {stargazer[attr]}") xprint(stargazer_tree) - CsvLogger.log_repo_stargazers(stargazer, repo_name) + log_repo_stargazers(stargazer, repo_name) else: xprint(response.json()) @@ -487,20 +488,20 @@ class Octosuite: repo_name = input() xprint(f"{white}@{green}Owner{white} (username):{reset} ", end="") username = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("repository forks"), end="") + xprint(PROMPT, limit_output.format("repository forks"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/repos/{username}/{repo_name}/forks?per_page={limit}") if response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.repo_or_user_not_found.format(repo_name, username)}") + xprint(f"{NEGATIVE} {repo_or_user_not_found.format(repo_name, username)}") elif response.json() == {}: - xprint(f"{MessagePrefix.negative} Repository does not have forks -> ({repo_name})") + xprint(f"{NEGATIVE} Repository does not have forks -> ({repo_name})") elif response.status_code == 200: for count, fork in enumerate(response.json()): fork_tree = Tree("\n" + fork['full_name']) for attr in self.repo_attrs: fork_tree.add(f"{self.repo_attr_dict[attr]}: {fork[attr]}") xprint(fork_tree) - CsvLogger.log_repo_forks(fork, count) + log_repo_forks(fork, count) else: xprint(response.json()) @@ -510,13 +511,13 @@ class Octosuite: repo_name = input() xprint(f"{white}>> @{green}Owner{white} (username){reset} ", end="") username = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("repository issues"), end="") + xprint(PROMPT, limit_output.format("repository issues"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/repos/{username}/{repo_name}/issues?per_page={limit}") if response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.repo_or_user_not_found.format(repo_name, username)}") + xprint(f"{NEGATIVE} {repo_or_user_not_found.format(repo_name, username)}") elif response.json() == []: - xprint(f"{MessagePrefix.negative} Repository does not have open issues -> ({repo_name})") + xprint(f"{NEGATIVE} Repository does not have open issues -> ({repo_name})") elif response.status_code == 200: for issue in response.json(): issues_tree = Tree("\n" + issue['title']) @@ -524,7 +525,7 @@ class Octosuite: issues_tree.add(f"{self.repo_issues_attr_dict[attr]}: {issue[attr]}") xprint(issues_tree) xprint(issue['body']) - CsvLogger.log_repo_issues(issue, repo_name) + log_repo_issues(issue, repo_name) else: xprint(response.json()) @@ -535,13 +536,13 @@ class Octosuite: repo_name = input() xprint(f"{white}>> @{green}Owner{white} (username){reset} ", end="") username = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("repository releases"), end="") + xprint(PROMPT, limit_output.format("repository releases"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/repos/{username}/{repo_name}/releases?per_page={limit}") if response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.repo_or_user_not_found.format(repo_name, username)}") + xprint(f"{NEGATIVE} {repo_or_user_not_found.format(repo_name, username)}") elif response.json() == []: - xprint(f"{MessagePrefix.negative} Repository does not have releases -> ({repo_name})") + xprint(f"{NEGATIVE} Repository does not have releases -> ({repo_name})") elif response.status_code == 200: for release in response.json(): releases_tree = Tree("\n" + release['name']) @@ -549,7 +550,7 @@ class Octosuite: releases_tree.add(f"{self.repo_releases_attr_dict[attr]}: {release[attr]}") xprint(releases_tree) xprint(release['body']) - CsvLogger.log_repo_releases(release, repo_name) + log_repo_releases(release, repo_name) else: xprint(response.json()) @@ -558,18 +559,18 @@ class Octosuite: def org_repos(self): xprint(f"{white}@{green}organization{white} (username):{reset} ", end="") organization = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("organization repositories"), end="") + xprint(PROMPT, limit_output.format("organization repositories"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/orgs/{organization}/repos?per_page={limit}") if response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.org_not_found.format(organization)}") + xprint(f"{NEGATIVE} {org_not_found.format(organization)}") elif response.status_code == 200: for repository in response.json(): repos_tree = Tree("\n" + repository['full_name']) for attr in self.repo_attrs: repos_tree.add(f"{self.repo_attr_dict[attr]}: {repository[attr]}") xprint(repos_tree) - CsvLogger.log_org_repos(repository, organization) + log_org_repos(repository, organization) else: xprint(response.json()) @@ -578,11 +579,11 @@ class Octosuite: def org_events(self): xprint(f"{white}@{green}organization{white} (username):{reset} ", end="") organization = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("organization repositories"), end="") + xprint(PROMPT, limit_output.format("organization repositories"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/orgs/{organization}/events?per_page={limit}") if response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.org_not_found.format(organization)}") + xprint(f"{NEGATIVE} {org_not_found.format(organization)}") elif response.status_code == 200: for event in response.json(): events_tree = Tree("\n" + event['id']) @@ -590,7 +591,7 @@ class Octosuite: events_tree.add(f"Created at: {event['created_at']}") xprint(events_tree) xprint(event['payload']) - CsvLogger.log_org_events(event, organization) + log_org_events(event, organization) else: xprint(response.json()) @@ -603,27 +604,27 @@ class Octosuite: username = input() response = requests.get(f"{self.endpoint}/orgs/{organization}/public_members/{username}") if response.status_code == 204: - xprint(f"{MessagePrefix.positive} User ({username}) is a public member of the organization -> ({organization})") + xprint(f"{POSITIVE} User ({username}) is a public member of the organization -> ({organization})") else: - xprint(f"{MessagePrefix.negative} {response.json()['message']}") + xprint(f"{NEGATIVE} {response.json()['message']}") # Fetching user repositories def user_repos(self): xprint(f"{white}@{green}username:{reset} ", end="") username = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("repositories"), end="") + xprint(PROMPT, limit_output.format("repositories"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/users/{username}/repos?per_page={limit}") if response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.userNotFound.format(username)}") + xprint(f"{NEGATIVE} {user_not_found.format(username)}") elif response.status_code == 200: for repository in response.json(): repos_tree = Tree("\n" + repository['full_name']) for attr in self.repo_attrs: repos_tree.add(f"{self.repo_attr_dict[attr]}: {repository[attr]}") xprint(repos_tree) - CsvLogger.log_user_repos(repository, username) + log_user_repos(repository, username) else: xprint(response.json()) @@ -632,20 +633,20 @@ class Octosuite: def user_gists(self): xprint(f"{white}@{green}username:{reset} ", end="") username = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format('gists'), end="") + xprint(PROMPT, limit_output.format('gists'), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/users/{username}/gists?per_page={limit}") if response.json() == []: - xprint(f"{MessagePrefix.negative} User does not have gists.") + xprint(f"{NEGATIVE} User does not have gists.") elif response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.userNotFound.format(username)}") + xprint(f"{NEGATIVE} {user_not_found.format(username)}") elif response.status_code == 200: for gist in response.json(): gists_tree = Tree("\n" + gist['id']) for attr in self.gists_attrs: gists_tree.add(f"{self.gists_attr_dict[attr]}: {gist[attr]}") xprint(gists_tree) - CsvLogger.log_user_gists(gist) + log_user_gists(gist) else: xprint(response.json()) @@ -654,20 +655,20 @@ class Octosuite: def user_orgs(self): xprint(f"{white}@{green}username:{reset} ", end="") username = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("user organizations"), end="") + xprint(PROMPT, limit_output.format("user organizations"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/users/{username}/orgs?per_page={limit}") if response.json() == []: - xprint(f"{MessagePrefix.negative} User ({username}) does not (belong to/own) any organizations.") + xprint(f"{NEGATIVE} User ({username}) does not (belong to/own) any organizations.") elif response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.userNotFound.format(username)}") + 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 attr in self.user_orgs_attrs: org_tree.add(f"{self.user_orgs_attr_dict[attr]}: {organization[attr]}") xprint(org_tree) - CsvLogger.log_user_orgs(organization, username) + log_user_orgs(organization, username) else: xprint(response.json()) @@ -676,11 +677,11 @@ class Octosuite: def user_events(self): xprint(f"{white}@{green}username:{reset} ", end="") username = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("events"), end="") + xprint(PROMPT, limit_output.format("events"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/users/{username}/events/public?per_page={limit}") if response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.userNotFound.format(username)}") + xprint(f"{NEGATIVE} {user_not_found.format(username)}") elif response.status_code == 200: for event in response.json(): events_tree = Tree("\n" + event['id']) @@ -690,7 +691,7 @@ class Octosuite: events_tree.add(f"Created at: {event['created_at']}") xprint(events_tree) xprint(event['payload']) - CsvLogger.log_user_events(event) + log_user_events(event) else: xprint(response.json()) @@ -699,20 +700,20 @@ class Octosuite: def user_subscriptions(self): xprint(f"{white}@{green}username:{reset} ", end="") username = input().lower() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("user subscriptions"), end="") + xprint(PROMPT, limit_output.format("user subscriptions"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/users/{username}/subscriptions?per_page={limit}") if response.json() == []: - xprint(f"{MessagePrefix.negative} User does not have any subscriptions.") + xprint(f"{NEGATIVE} User does not have any subscriptions.") elif response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.userNotFound.format(username)}") + xprint(f"{NEGATIVE} {user_not_found.format(username)}") elif response.status_code == 200: for repository in response.json(): subscriptions_tree =Tree("\n" + repository['full_name']) for attr in self.repo_attrs: subscriptions_tree.add(f"{self.repo_attr_dict[attr]}: {repository[attr]}") xprint(subscriptions_tree) - CsvLogger.log_user_subscriptions(repository, username) + log_user_subscriptions(repository, username) else: xprint(response.json()) @@ -721,20 +722,20 @@ class Octosuite: def user_following(self): xprint(f"{white}@{green}username:{reset} ", end="") username = input().lower() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("user' following"), end="") + xprint(PROMPT, limit_output.format("user' following"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/users/{username}/following?per_page={limit}") if response.json() == []: - xprint(f"{MessagePrefix.negative} User ({username})does not follow anyone.") + xprint(f"{NEGATIVE} User ({username})does not follow anyone.") elif response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.userNotFound.format(username)}") + xprint(f"{NEGATIVE} {user_not_found.format(username)}") elif response.status_code == 200: for user in response.json(): following_tree = Tree("\n" + user['login']) for attr in self.user_attrs: following_tree.add(f"{self.user_attr_dict[attr]}: {user[attr]}") xprint(following_tree) - CsvLogger.log_user_following(user, username) + log_user_following(user, username) else: xprint(response.json()) @@ -743,20 +744,20 @@ class Octosuite: def user_followers(self): xprint(f"{white}@{green}username:{reset} ", end="") username = input().lower() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("user followers"), end="") + xprint(PROMPT, limit_output.format("user followers"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/users/{username}/followers?per_page={limit}") if response.json() == []: - xprint(f"{MessagePrefix.negative} User ({username})does not have followers.") + xprint(f"{NEGATIVE} User ({username})does not have followers.") elif response.status_code == 404: - xprint(f"{MessagePrefix.negative} {LogRoller.userNotFound.format(username)}") + xprint(f"{NEGATIVE} {user_not_found.format(username)}") elif response.status_code == 200: for follower in response.json(): followers_tree = Tree("\n" + follower['login']) for attr in self.user_attrs: followers_tree.add(f"{self.user_attr_dict[attr]}: {follower[attr]}") xprint(followers_tree) - CsvLogger.log_user_followers(follower, username) + log_user_followers(follower, username) else: xprint(response.json()) @@ -769,16 +770,16 @@ class Octosuite: user_b = input() response = requests.get(f"{self.endpoint}/users/{user_a}/following/{user_b}") if response.status_code == 204: - xprint(f"{MessagePrefix.positive} @{user_a} FOLLOWS @{user_b}") + xprint(f"{POSITIVE} @{user_a} FOLLOWS @{user_b}") else: - xprint(f"{MessagePrefix.negative} @{user_a} DOES NOT FOLLOW @{user_b}") + xprint(f"{NEGATIVE} @{user_a} DOES NOT FOLLOW @{user_b}") # User search def users_search(self): xprint(f"{white}@{green}query{white} (eg. john):{reset} ", end="") query = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("user search"), end="") + xprint(PROMPT, limit_output.format("user search"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/search/users?q={query}&per_page={limit}").json() for user in response['items']: @@ -786,14 +787,14 @@ class Octosuite: for attr in self.user_attrs: users_search_tree.add(f"{self.user_attr_dict[attr]}: {user[attr]}") xprint(users_search_tree) - CsvLogger.log_users_search(user, query) + log_users_search(user, query) # Repository search def repos_search(self): xprint(f"{white}%{green}query{white} (eg. git):{reset} ", end="") query = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("repositor[y][ies] search"), end="") + xprint(PROMPT, limit_output.format("repositor[y][ies] search"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/search/repositories?q={query}&per_page={limit}").json() for repository in response['items']: @@ -801,14 +802,14 @@ class Octosuite: for attr in self.repo_attrs: repos_search_tree.add(f"{self.repo_attr_dict[attr]}: {repository[attr]}") xprint(repos_search_tree) - CsvLogger.log_repos_search(repository, query) + log_repos_search(repository, query) # Topics search def topics_search(self): xprint(f"{white}#{green}query{white} (eg. osint):{reset} ", end="") query = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("topic(s) search"), end="") + xprint(PROMPT, limit_output.format("topic(s) search"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/search/topics?q={query}&per_page={limit}").json() for topic in response['items']: @@ -816,14 +817,14 @@ class Octosuite: for attr in self.topic_attrs: topics_search_tree.add(f"{self.topic_attr_dict[attr]}: {topic[attr]}") xprint(topics_search_tree) - CsvLogger.log_topics_search(topic, query) + log_topics_search(topic, query) # Issue search def issues_search(self): xprint(f"{white}!{green}Query{white} (eg. error):{reset} ", end="") query = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("issue(s) search"), end="") + xprint(PROMPT, limit_output.format("issue(s) search"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/search/issues?q={query}&per_page={limit}").json() for issue in response['items']: @@ -832,14 +833,14 @@ class Octosuite: issues_search_tree.add(f"{self.repo_issues_attr_dict[attr]}: {issue[attr]}") xprint(issues_search_tree) xprint(issue['body']) - CsvLogger.log_issues_search(issue, query) + log_issues_search(issue, query) # Commits search def commits_search(self): xprint(f"{white}:{green}Query{white} (eg. filename:index.php):{reset} ", end="") query = input() - xprint(MessagePrefix.prompt, LogRoller.limit_output.format("commit(s) search"), end="") + xprint(PROMPT, limit_output.format("commit(s) search"), end="") limit = int(input()) response = requests.get(f"{self.endpoint}/search/commits?q={query}&per_page={limit}").json() for commit in response['items']: @@ -852,12 +853,12 @@ class Octosuite: commits_search_tree.add(f"URL: {commit['html_url']}") xprint(commits_search_tree) xprint(commit['commit']['message']) - CsvLogger.log_commits_search(commit, query) + log_commits_search(commit, query) # View csv files def view_csv(self): - logging.info(LogRoller.viewing_csv) + logging.info(viewing_csv) csv_files = os.listdir("output") csv_table = Table(show_header=True, header_style=header_title) csv_table.add_column("CSV", style="dim") @@ -872,7 +873,7 @@ class Octosuite: xprint(f"{green}csv {white}(filename):{reset} ", end="") csv_file = input() with open(os.path.join("output", csv_file + ".csv"), "r") as file: - logging.info(LogRoller.reading_csv.format(csv_file)) + logging.info(reading_csv.format(csv_file)) text = Text(file.read()) xprint(text) @@ -882,24 +883,24 @@ class Octosuite: xprint(f"{green}csv {white}(filename):{reset} ", end="") csv_file = input() os.remove(os.path.join("output", csv_file)) - logging.info(LogRoller.deleted_csv.format(csv_file)) - xprint(f"{MessagePrefix.positive} {LogRoller.deleted_csv.format(csv_file)}") + logging.info(deleted_csv.format(csv_file)) + xprint(f"{POSITIVE} {deleted_csv.format(csv_file)}") # Clear csv def clear_csv(self): - xprint(f"{MessagePrefix.prompt} This will clear all {len(os.listdir('output'))} csv files, continue? (yes/no) ", end="") + xprint(f"{PROMPT} This will clear all {len(os.listdir('output'))} csv files, continue? (yes/no) ", end="") prompt = input().lower() if prompt == "yes": shutil.rmtree("output", ignore_errors=True) - xprint(f"{MessagePrefix.info} CSV files cleared successfully!") + xprint(f"{INFO} CSV files cleared successfully!") else: pass # View octosuite log files def view_logs(self): - logging.info(LogRoller.viewing_logs) + logging.info(viewing_logs) logs = os.listdir(".logs") logs_table = Table(show_header=True, header_style=header_title) logs_table.add_column("Log", style="dim") @@ -914,7 +915,7 @@ class Octosuite: xprint(f"{green}log date{white} (eg. 2022-04-27 10:09:36AM):{reset} ", end="") log_file = input() with open(os.path.join(".logs", log_file + ".log"), "r") as log: - logging.info(LogRoller.reading_log.format(log_file)) + logging.info(reading_log.format(log_file)) xprint("\n" + log.read()) @@ -923,18 +924,18 @@ class Octosuite: xprint(f"{green}log date{white} (eg. 2022-04-27 10:09:36AM):{reset} ", end="") log_file = input() os.remove(os.path.join(".logs", log_file)) - logging.info(LogRoller.deleted_log.format(log_file)) - xprint(f"{MessagePrefix.positive} {LogRoller.deleted_log.format(log_file)}") + logging.info(deleted_log.format(log_file)) + xprint(f"{POSITIVE} {deleted_log.format(log_file)}") # Clear logs def clear_logs(self): - xprint(f"{MessagePrefix.prompt} This will clear all {len(os.listdir('.logs'))} logs and close the current session, continue? (yes/no) ", end="") + xprint(f"{PROMPT} This will clear all {len(os.listdir('.logs'))} logs and close the current session, continue? (yes/no) ", end="") prompt = input().lower() if prompt == "yes": shutil.rmtree(".logs", ignore_errors=True) - xprint(f"{MessagePrefix.info} Logs cleared successfully!") - xprint(f"{MessagePrefix.info} {LogRoller.session_closed.format(datetime.now())}") + xprint(f"{INFO} Logs cleared successfully!") + xprint(f"{INFO} {session_closed.format(datetime.now())}") exit() else: pass @@ -942,28 +943,28 @@ class Octosuite: # Downloading release tarball def download_tarball(self): - logging.info(LogRoller.file_downloading.format(f"octosuite.v{version_tag}.tar")) - xprint(MessagePrefix.info, LogRoller.file_downloading.format(f"octosuite.v{version_tag}.tar")) + logging.info(file_downloading.format(f"octosuite.v{version_tag}.tar")) + xprint(INFO, file_downloading.format(f"octosuite.v{version_tag}.tar")) data = requests.get(f"{self.endpoint}/repos/bellingcat/octosuite/tarball/{version_tag}") with open(os.path.join("downloads", f"octosuite.v{version_tag}.tar"), "wb") as file: file.write(data.content) file.close() - logging.info(LogRoller.file_downloaded.format(f"octosuite.v{version_tag}.tar")) - xprint(MessagePrefix.positive, LogRoller.file_downloaded.format(f"octosuite.v{version_tag}.tar")) + logging.info(file_downloaded.format(f"octosuite.v{version_tag}.tar")) + xprint(POSITIVE, file_downloaded.format(f"octosuite.v{version_tag}.tar")) # Downloading release zipball def download_zipball(self): - logging.info(LogRoller.file_downloading.format(f"octosuite.v{version_tag}.zip")) - xprint(MessagePrefix.info, LogRoller.file_downloading.format(f"octosuite.v{version_tag}.zip")) + logging.info(file_downloading.format(f"octosuite.v{version_tag}.zip")) + xprint(INFO, file_downloading.format(f"octosuite.v{version_tag}.zip")) data = requests.get(f"{self.endpoint}/repos/rly0nheart/octosuite/zipball/{version_tag}") with open(os.path.join("downloads", f"octosuite.v{version_tag}.zip"), "wb") as file: file.write(data.content) file.close() - logging.info(LogRoller.file_downloaded.format(f"octosuite.v{version_tag}.zip")) - xprint(MessagePrefix.positive, LogRoller.file_downloaded.format(f"octosuite.v{version_tag}.zip")) + logging.info(file_downloaded.format(f"octosuite.v{version_tag}.zip")) + xprint(POSITIVE, file_downloaded.format(f"octosuite.v{version_tag}.zip")) # Author info @@ -997,11 +998,11 @@ GitHub REST API documentation: https://docs.github.com/rest # Close session def exit_session(self): - xprint(f"{MessagePrefix.prompt} This will close the current session, continue? (yes/no) ", end="") + xprint(f"{PROMPT} This will close the current session, continue? (yes/no) ", end="") prompt = input().lower() if prompt == "yes": - logging.info(LogRoller.session_closed.format(datetime.now())) - xprint(f"{MessagePrefix.info} {LogRoller.session_closed.format(datetime.now())}") + logging.info(session_closed.format(datetime.now())) + xprint(f"{INFO} {session_closed.format(datetime.now())}") exit() else: pass