diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad4a1f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,176 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/swaggerspy.py b/swaggerspy.py index c633b64..366d7e7 100644 --- a/swaggerspy.py +++ b/swaggerspy.py @@ -1,52 +1,55 @@ -#!/usr/bin/env python3 from concurrent.futures import ThreadPoolExecutor, as_completed import requests, sys, re from colorama import Fore, Style +import argparse + +SEARCH_TERM = "" +QUIET = False regex_patterns = { - 'google_api' : r'AIza[0-9A-Za-z-_]{35}', - 'firebase' : r'AAAA[A-Za-z0-9_-]{7}:[A-Za-z0-9_-]{140}', - 'google_captcha' : r'6L[0-9A-Za-z-_]{38}|^6[0-9a-zA-Z_-]{39}$', - 'google_oauth' : r'ya29\.[0-9A-Za-z\-_]+', - 'amazon_aws_access_key_id' : r'A[SK]IA[0-9A-Z]{16}', - 'amazon_mws_auth_toke' : r'amzn\\.mws\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', - 'amazon_aws_url' : r's3\.amazonaws.com[/]+|[a-zA-Z0-9_-]*\.s3\.amazonaws.com', - 'amazon_aws_url2' : r"(" \ - r"[a-zA-Z0-9-\.\_]+\.s3\.amazonaws\.com" \ - r"|s3://[a-zA-Z0-9-\.\_]+" \ - r"|s3-[a-zA-Z0-9-\.\_\/]+" \ - r"|s3.amazonaws.com/[a-zA-Z0-9-\.\_]+" \ - r"|s3.console.aws.amazon.com/s3/buckets/[a-zA-Z0-9-\.\_]+)", - 'facebook_access_token' : r'EAACEdEose0cBA[0-9A-Za-z]+', - 'authorization_basic' : r'basic [a-zA-Z0-9=:_\+\/-]{5,100}', - 'authorization_bearer' : r'bearer [a-zA-Z0-9_\-\.=:_\+\/]{5,100}', - 'mailgun_api_key' : r'key-[0-9a-zA-Z]{32}', - 'twilio_api_key' : r'SK[0-9a-fA-F]{32}', - 'twilio_account_sid' : r'AC[a-zA-Z0-9_\-]{32}', - 'twilio_app_sid' : r'AP[a-zA-Z0-9_\-]{32}', - 'paypal_braintree_access_token' : r'access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}', - 'square_oauth_secret' : r'sq0csp-[ 0-9A-Za-z\-_]{43}|sq0[a-z]{3}-[0-9A-Za-z\-_]{22,43}', - 'square_access_token' : r'sqOatp-[0-9A-Za-z\-_]{22}|EAAA[a-zA-Z0-9]{60}', - 'stripe_standard_api' : r'sk_live_[0-9a-zA-Z]{24}', - 'stripe_restricted_api' : r'rk_live_[0-9a-zA-Z]{24}', - 'github_access_token' : r'[a-zA-Z0-9_-]*:[a-zA-Z0-9_\-]+@github\.com*', - 'rsa_private_key' : r'-----BEGIN RSA PRIVATE KEY-----', - 'ssh_dsa_private_key' : r'-----BEGIN DSA PRIVATE KEY-----', - 'ssh_dc_private_key' : r'-----BEGIN EC PRIVATE KEY-----', - 'pgp_private_block' : r'-----BEGIN PGP PRIVATE KEY BLOCK-----', - 'json_web_token' : r'ey[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$', - 'slack_token' : r"\"api_token\":\"(xox[a-zA-Z]-[a-zA-Z0-9-]+)\"", - 'SSH_privKey' : r"([-]+BEGIN [^\s]+ PRIVATE KEY[-]+[\s]*[^-]*[-]+END [^\s]+ PRIVATE KEY[-]+)", - 'Heroku API KEY' : r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', - 'possible_Creds' : r'(?i)(" \ + "google_api": r"AIza[0-9A-Za-z-_]{35}", + "firebase": r"AAAA[A-Za-z0-9_-]{7}:[A-Za-z0-9_-]{140}", + "google_captcha": r"6L[0-9A-Za-z-_]{38}|^6[0-9a-zA-Z_-]{39}$", + "google_oauth": r"ya29\.[0-9A-Za-z\-_]+", + "amazon_aws_access_key_id": r"A[SK]IA[0-9A-Z]{16}", + "amazon_mws_auth_toke": r"amzn\\.mws\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + "amazon_aws_url": r"s3\.amazonaws.com[/]+|[a-zA-Z0-9_-]*\.s3\.amazonaws.com", + "amazon_aws_url2": r"(" + r"[a-zA-Z0-9-\.\_]+\.s3\.amazonaws\.com" + r"|s3://[a-zA-Z0-9-\.\_]+" + r"|s3-[a-zA-Z0-9-\.\_\/]+" + r"|s3.amazonaws.com/[a-zA-Z0-9-\.\_]+" + r"|s3.console.aws.amazon.com/s3/buckets/[a-zA-Z0-9-\.\_]+)", + "facebook_access_token": r"EAACEdEose0cBA[0-9A-Za-z]+", + "authorization_basic": r"basic [a-zA-Z0-9=:_\+\/-]{5,100}", + "authorization_bearer": r"bearer [a-zA-Z0-9_\-\.=:_\+\/]{5,100}", + "mailgun_api_key": r"key-[0-9a-zA-Z]{32}", + "twilio_api_key": r"SK[0-9a-fA-F]{32}", + "twilio_account_sid": r"AC[a-zA-Z0-9_\-]{32}", + "twilio_app_sid": r"AP[a-zA-Z0-9_\-]{32}", + "paypal_braintree_access_token": r"access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}", + "square_oauth_secret": r"sq0csp-[ 0-9A-Za-z\-_]{43}|sq0[a-z]{3}-[0-9A-Za-z\-_]{22,43}", + "square_access_token": r"sqOatp-[0-9A-Za-z\-_]{22}|EAAA[a-zA-Z0-9]{60}", + "stripe_standard_api": r"sk_live_[0-9a-zA-Z]{24}", + "stripe_restricted_api": r"rk_live_[0-9a-zA-Z]{24}", + "github_access_token": r"[a-zA-Z0-9_-]*:[a-zA-Z0-9_\-]+@github\.com*", + "rsa_private_key": r"-----BEGIN RSA PRIVATE KEY-----", + "ssh_dsa_private_key": r"-----BEGIN DSA PRIVATE KEY-----", + "ssh_dc_private_key": r"-----BEGIN EC PRIVATE KEY-----", + "pgp_private_block": r"-----BEGIN PGP PRIVATE KEY BLOCK-----", + "json_web_token": r"ey[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$", + "slack_token": r"\"api_token\":\"(xox[a-zA-Z]-[a-zA-Z0-9-]+)\"", + "SSH_privKey": r"([-]+BEGIN [^\s]+ PRIVATE KEY[-]+[\s]*[^-]*[-]+END [^\s]+ PRIVATE KEY[-]+)", + "possible_Creds": r'(?i)(" \ r"password\s*[`=:\"]+\s*[^\s]+|" \ r"password is\s*[`=:\"]*\s*[^\s]+|" \ r"pwd\s*[`=:\"]*\s*[^\s]+|" \ r"passwd\s*[`=:\"]+\s*[^\s]+)', - 'email': r"[\w\.-]+@[\w\.-]+\.\w+", - 'ip': r"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)", + "email": r"[\w\.-]+@[\w\.-]+\.\w+", + "ip": r"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)", } + def check_regex(data, regex_patterns): for pattern_name, pattern in regex_patterns.items(): match = re.search(pattern, data) @@ -54,27 +57,47 @@ def check_regex(data, regex_patterns): return pattern_name, match.group() return None, None + def process_url(url, regex_patterns): + global SEARCH_TERM + try: response = requests.get(url) if response.status_code == 200: data = response.text matching_contents = [] - for line in data.split('\n'): + if SEARCH_TERM not in data: + return matching_contents + for line in data.split("\n"): pattern_name, matched_content = check_regex(line, regex_patterns) if pattern_name: matching_contents.append((pattern_name, matched_content)) + if len(matching_contents) == 0: + pass + else: + if not QUIET: + print(f"[+] Found matches: {len(matching_contents)}") return matching_contents + else: + print(f"[-] SwaggerHub API error: {response.status_code}, {response.text}") + if response.status_code == 429: # Too many requests + import time + + if not QUIET: + print("[-] Backing off for ten seconds") + time.sleep(10) except Exception as e: print(f"Error processing URL {url}: {e}") return [] + def make_request(url, search_term, page): headers = {"accept": "application/json"} response = requests.get(url.format(search_term, page + 1), headers=headers) json_data = response.json() return json_data + def parse_data_api(json_api_data): url_list = [] try: @@ -87,11 +110,14 @@ def parse_data_api(json_api_data): print(f"Error parsing JSON data: {e}") return url_list + def get_urls(search_term): base_url = "https://app.swaggerhub.com/apiproxy/specs?sort=BEST_MATCH&order=DESC&query={}&page={}&limit=100" session = requests.Session() - response = session.get(base_url.format(search_term, 0), headers={"accept": "application/json"}) + response = session.get( + base_url.format(search_term, 0), headers={"accept": "application/json"} + ) json_response = response.json() total_apis = int(json_response.get("totalCount", 0)) pages_to_go_through = total_apis // 100 @@ -111,45 +137,112 @@ def get_urls(search_term): return urls_to_go_through + def print_colored(text, color): print(f"{color}{text}{Style.RESET_ALL}") -if __name__ == "__main__": - print_colored(''' + +def main(): + global SEARCH_TERM + global QUIET + + parser = argparse.ArgumentParser( + description="AUTOMATED OSINT ON SWAGGERHUB", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="Example: python3 swaggerspy.py test.com", + ) + + parser.add_argument("searchterm", help="Search term (more accurate with domains)") + parser.add_argument( + "-t", + "--threads", + type=int, + default=10, + help="Number of threads to use (default 10)", + ) + parser.add_argument( + "-q", "--quiet", action="store_true", help="Suppress output messages" + ) + parser.add_argument( + "-o", "--outfile", type=str, default="", help="Log results to JSON" + ) + + args = parser.parse_args() + + QUIET = args.quiet + + if not QUIET: + print_colored( + """ █▀▀ █ █ █▀█ █▀▀ █▀▀ █▀▀ █▀█ █▀▀ █▀█ █ █ ▀▀█ █▄█ █▀█ █ █ █ █ █▀▀ █▀▄ ▀▀█ █▀▀ █ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀ ▀ by Alisson Moretto (UndeadSec) - AUTOMATED OSINT ON SWAGGERHUB''', Fore.MAGENTA) - - if len(sys.argv) != 2: - print_colored("\nUsage: python3 swaggerspy.py searchterm (more accurate with domains).\n $ python3 swaggerspy.py test.com", Fore.RED) - sys.exit(1) - - search_term = sys.argv[1] - result_urls = get_urls(search_term) + AUTOMATED OSINT ON SWAGGERHUB\n""", + Fore.MAGENTA, + ) + + if QUIET and not args.outfile: + print("[!] Quiet mode specified without --outfile") + print("[*] This will result in a bunch of useless API calls, exiting...") + return 0 + + SEARCH_TERM = args.searchterm + result_urls = get_urls(args.searchterm) + result_data = { + "search_term": args.searchterm, + "search_results:": result_urls, + "matches": [], + } if result_urls: - print("\n[*] Found {} urls".format(len(result_urls))) + print("[*] Found {} urls".format(len(result_urls))) else: - print("\n[!] No results") + print("[!] No results") + return 2 - with ThreadPoolExecutor(max_workers=25) as executor: - future_to_url = {executor.submit(process_url, url, regex_patterns): url for url in result_urls} + with ThreadPoolExecutor(max_workers=args.threads) as executor: + future_to_url = { + executor.submit(process_url, url, regex_patterns): url + for url in result_urls + } for future in as_completed(future_to_url): url = future_to_url[future] - print_colored(f"\nURL: {url}", Fore.CYAN) + if not QUIET: + print(f"[*] Searching for matches in {url}") try: matching_contents = future.result() - if matching_contents: for pattern_name, content in matching_contents: - print_colored(f"[*] {pattern_name}: {content}", Fore.GREEN) + result_data["matches"].append([url, pattern_name, content]) else: - print_colored("[!] No matches found", Fore.YELLOW) + pass except Exception as e: - print_colored(f"Error processing URL {url}: {e}", Fore.RED) + print(f"Error processing URL {url}: {e}") + + if not QUIET: + print("Displaying results") + for result in result_data["matches"]: + print(f"{result[0]}, {result[1]}, {result[2]}") + + if not QUIET: + print( + "\nThanks for using SwaggerSpy! Consider following me on X, GitHub and LinkedIn: @UndeadSec, https://github.com/UndeadSec, https://linkedin.com/in/alissonmoretto\n", + Fore.MAGENTA, + ) + + print(f"[+] Completed with {len(result_data['matches'])} results") - print_colored("\nThanks for using SwaggerSpy! Consider following me on X, GitHub and LinkedIn: @UndeadSec, https://github.com/UndeadSec, https://linkedin.com/in/alissonmoretto\n", Fore.MAGENTA) \ No newline at end of file + if args.outfile: + with open(args.outfile, "w") as outfile: + import json + + outfile.write(json.dumps(result_data, indent=2)) + + return 0 + + +if __name__ == "__main__": + main()