├── README.md ├── install.sh ├── jsexposures.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | JSEXPOSURES 2 | =========== 3 | 4 | ![image](https://github.com/user-attachments/assets/f98c981c-13bf-4e04-a77e-fc6801506559) 5 | 6 | **jsexposures** is a Python tool designed to scan JavaScript files for sensitive information exposure. It helps security researchers and bug bounty hunters identify potential leaks of API keys, tokens, passwords, sensitive comments, and other confidential data in JavaScript endpoints. 7 | 8 | ✨ Features 9 | ---------- 10 | 11 | - **Multi-Pattern Matching**: Utilizes an extensive set of regular expressions to detect a wide range of sensitive information, such as API keys, credentials, and JWT tokens. 12 | - **Sensitive Comment Detection**: Identifies comments that may indicate potential security issues or hidden sensitive information (`TODO`, `FIXME`, `password`, `secret`, etc.). 13 | - **Concurrent Requests**: Processes multiple URLs simultaneously to maximize efficiency and speed. 14 | - **Results Logging**: Saves findings in both JSON and text formats for easy review and analysis. 15 | - **Customizable Logging Levels**: Allows users to define the level of log verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`). 16 | 17 | 📦 Installation 18 | --------------- 19 | 20 | 1. Clone the repository: 21 | 22 | ``` bash 23 | git clone https://github.com/danielhidalgo2/jsexposures.git 24 | cd jsexposures 25 | ``` 26 | 27 | 1. (Optional) In the event of missing dependencies, there are two options: 28 | using a system-wide install (the script automatically detects Debian and Arch based OSs for the apropiate package manager): 29 | 30 | ``` bash 31 | chmod +x install.sh && ./install.sh 32 | ``` 33 | 34 | or if you are using a venv, via pip: 35 | 36 | ``` bash 37 | pip install -r requirements.txt 38 | ``` 39 | 40 | 📝 Usage 41 | -------- 42 | 43 | 1. **Prepare a text file** named `js_endpoints.txt` with URLs of JavaScript files to scan, one URL per line. 44 | 45 | 2. Run the tool with basic options: 46 | 47 | `python jsexposures.py --file js_endpoints.txt --max-workers 15 --log-level DEBUG` 48 | 49 | 3. **Check the output files** for results: 50 | 51 | - `exposure_results.txt`: Text file with a summary of found exposures. 52 | - `exposure_results.json`: JSON file with detailed information about each exposure. 53 | 54 | ### ⚙️ Command-Line Options 55 | 56 | ``` 57 | usage: jsexposures.py [-h] [--file FILE] [--max-workers MAX_WORKERS] [--log-level LOG_LEVEL] 58 | 59 | JS Exposures Finder - A tool to scan JavaScript files for sensitive information. 60 | 61 | optional arguments: 62 | -h, --help show this help message and exit 63 | --file FILE Path to the file containing JS URLs to scan (default: js_endpoints.txt) 64 | --max-workers MAX_WORKERS 65 | Number of concurrent threads to use for scanning (default: 10) 66 | --log-level LOG_LEVEL Set the logging level (default: INFO). Available levels: DEBUG, INFO, WARNING, ERROR. 67 | ``` 68 | 69 | ### 📜 Example Commands 70 | 71 | 1. **Basic Scan** with default options: 72 | 73 | `python jsexposures.py --file js_endpoints.txt` 74 | 75 | 2. **Detailed Scan** with 15 concurrent threads and more verbose output: 76 | 77 | `python jsexposures.py --file js_endpoints.txt --max-workers 15 --log-level DEBUG` 78 | 79 | 3. **Use a Custom File** and reduce concurrent threads: 80 | 81 | `python jsexposures.py --file custom_endpoints.txt --max-workers 5 --log-level INFO` 82 | 83 | 📂 Output Formats 84 | ----------------- 85 | 86 | The tool provides results in two formats for easier analysis: 87 | 88 | - **TXT Output (`exposure_results.txt`)**: Contains a summary of each match with the format: 89 | 90 | `Found an exposure: "YOUR_API_KEY_12345" (API Key) at URL "https://example.com/script.js" | Length: 16` 91 | 92 | - **JSON Output (`exposure_results.json`)**: Provides detailed information structured in the following format: 93 | 94 | `[ 95 | { 96 | "url": "https://example.com/script.js", 97 | "match": "YOUR_API_KEY_12345", 98 | "description": "API Key", 99 | "length": 16 100 | }, 101 | ... 102 | ]` 103 | 104 | 📌 Key Patterns Detected 105 | ------------------------ 106 | 107 | - **API Keys & Secrets**: 108 | 109 | - `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `GOOGLE_API_KEY`, etc. 110 | - **Sensitive Comments**: 111 | 112 | - `// TODO: Update credentials` 113 | - `/* FIXME: Hardcoded password here */` 114 | - `# Debug note: Remember to change the key` 115 | - **Credentials**: 116 | 117 | - Passwords, tokens, authorization headers, and more. 118 | 119 | ☕ Support the Project 120 | --------------------- 121 | 122 | If you find **jsexposures** useful and would like to support the project, consider [buying me a coffee](https://buymeacoffee.com/hidalg0d). Every bit of support is greatly appreciated and helps keep the project alive! 123 | 124 | 🧑‍💻 Author 125 | ------------ 126 | This tool was developed and maintained by **hidalg0d**. Feel free to reach out for questions, suggestions, or feedback. 127 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to install dependencies on Arch-based systems 4 | install_arch() { 5 | 6 | echo "[+] Arch based OS detected, installing via pacman..." 7 | # Update package list and install dependencies system-wide 8 | sudo pacman -Sy python-requests python-urllib3 9 | echo "[+] Dependency install finished" 10 | 11 | } 12 | 13 | #Function to install dependencies on Debian-based systems 14 | install_debian() { 15 | 16 | echo "[+] Debian based OS detected, installing via apt..." 17 | #Install systemwide packaged 18 | sudo apt install -y python3-requests python3-urllib3 19 | echo "[+] Dependecy install finished" 20 | } 21 | 22 | # Check the OS type 23 | if [[ -f /etc/os-release ]]; then 24 | source /etc/os-release 25 | 26 | if [[ "$ID" == "arch" || "$ID_LIKE" == *"arch"* ]]; then 27 | install_arch 28 | elif [[ "$ID" == "debian" || "$ID_LIKE" == *"debian"* ]]; then 29 | install_debian 30 | else 31 | echo "[!] Unsupported OS: Please install via pip" 32 | exit 1 33 | fi 34 | else 35 | echo "[!] Could not determine OS via /etc/os-release, please install via pip" 36 | exit 1 37 | fi 38 | 39 | echo "[+] System-wide install finished" 40 | -------------------------------------------------------------------------------- /jsexposures.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import re 3 | import concurrent.futures 4 | import logging 5 | import json 6 | import argparse 7 | import urllib3 8 | import time 9 | import signal 10 | import sys 11 | 12 | 13 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 14 | 15 | 16 | def configure_logging(level): 17 | numeric_level = getattr(logging, level.upper(), None) 18 | if not isinstance(numeric_level, int): 19 | raise ValueError(f'Invalid log level: {level}') 20 | logging.basicConfig(level=numeric_level, format='%(levelname)s: %(message)s') 21 | 22 | 23 | def signal_handler(sig, frame): 24 | print("\n[INFO] Exiting program gracefully...") 25 | sys.exit(0) 26 | 27 | 28 | signal.signal(signal.SIGINT, signal_handler) 29 | 30 | 31 | def print_banner(): 32 | banner = r""" 33 | ▄▄▄██▀▀▀ ██████ ▓█████ ▒██ ██▒ ██▓███ ▒█████ ██████ █ ██ ██▀███ ▓█████ ██████ 34 | ▒██ ▒██ ▒ ▓█ ▀ ▒▒ █ █ ▒░▓██░ ██▒▒██▒ ██▒▒██ ▒ ██ ▓██▒▓██ ▒ ██▒▓█ ▀ ▒██ ▒ 35 | ░██ ░ ▓██▄ ▒███ ░░ █ ░▓██░ ██▓▒▒██░ ██▒░ ▓██▄ ▓██ ▒██░▓██ ░▄█ ▒▒███ ░ ▓██▄ 36 | ▓██▄██▓ ▒ ██▒▒▓█ ▄ ░ █ █ ▒ ▒██▄█▓▒ ▒▒██ ██░ ▒ ██▒▓▓█ ░██░▒██▀▀█▄ ▒▓█ ▄ ▒ ██▒ 37 | ▓███▒ ▒██████▒▒░▒████▒▒██▒ ▒██▒▒██▒ ░ ░░ ████▓▒░▒██████▒▒▒▒█████▓ ░██▓ ▒██▒░▒████▒▒██████▒▒ 38 | ▒▓▒▒░ ▒ ▒▓▒ ▒ ░░░ ▒░ ░▒▒ ░ ░▓ ░▒▓▒░ ░ ░░ ▒░▒░▒░ ▒ ▒▓▒ ▒ ░░▒▓▒ ▒ ▒ ░ ▒▓ ░▒▓░░░ ▒░ ░▒ ▒▓▒ ▒ ░ 39 | ▒ ░▒░ ░ ░▒ ░ ░ ░ ░ ░░░ ░▒ ░░▒ ░ ░ ▒ ▒░ ░ ░▒ ░ ░░░▒░ ░ ░ ░▒ ░ ▒░ ░ ░ ░░ ░▒ ░ ░ 40 | ░ ░ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ░ ░ ▒ ░ ░ ░ ░░░ ░ ░ ░░ ░ ░ ░ ░ ░ 41 | ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ 42 | jsexposures - Search for exposures in JS files 43 | Author: hidalg0d 44 | """ 45 | print(banner) 46 | 47 | def load_urls_from_file(file_path): 48 | try: 49 | with open(file_path, 'r') as file: 50 | urls = [line.strip() for line in file if line.strip()] 51 | logging.info(f"Loaded {len(urls)} URLs from {file_path}.") 52 | return urls 53 | except FileNotFoundError: 54 | logging.error(f"File not found: {file_path}") 55 | return [] 56 | 57 | # Sensitive patterns for secrets and comments 58 | patterns = [ 59 | (re.compile(r'\bApiKey\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{6,})(?:"|\'|)', re.IGNORECASE), "API Key"), 60 | (re.compile(r'\bSECRET_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32,})(?:"|\'|)', re.IGNORECASE), "Django Secret Key"), 61 | (re.compile(r'\bSLACK_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{40,})(?:"|\'|)', re.IGNORECASE), "Slack Token"), 62 | (re.compile(r'\bMAPBOX_ACCESS_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{64})(?:"|\'|)', re.IGNORECASE), "Mapbox Access Token"), 63 | (re.compile(r'\bAWS_ACCESS_KEY_ID\s*:\s*(?:"|\'|)([A-Z0-9]{20})(?:"|\'|)', re.IGNORECASE), "AWS Access Key ID"), 64 | (re.compile(r'\bAWS_SECRET_ACCESS_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9/+=]{40})(?:"|\'|)', re.IGNORECASE), "AWS Secret Access Key"), 65 | (re.compile(r'\bGOOGLE_API_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{39})(?:"|\'|)', re.IGNORECASE), "Google API Key"), 66 | (re.compile(r'\bBearer\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "Bearer Token"), 67 | (re.compile(r'\bpassword\s*:\s*(?:"|\'|)([A-Za-z0-9-_!@#$%^&*]{8,})(?:"|\'|)', re.IGNORECASE), "Password"), 68 | (re.compile(r'\bclient_secret\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{40})(?:"|\'|)', re.IGNORECASE), "Client Secret"), 69 | (re.compile(r'\bauthorization\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "Authorization Header"), 70 | (re.compile(r'\bPRIVATE_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_=\n]+)(?:"|\'|)', re.IGNORECASE), "Private Key"), 71 | (re.compile(r'\bANACONDA_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "Anaconda Token"), 72 | (re.compile(r'\bANDROID_DOCS_DEPLOY_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "Android Docs Deploy Token"), 73 | (re.compile(r'\bandroid_sdk_license\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "Android SDK License"), 74 | (re.compile(r'\bANSIBLE_VAULT_PASSWORD\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{6,})(?:"|\'|)', re.IGNORECASE), "Ansible Vault Password"), 75 | (re.compile(r'\bAPI_KEY_MCM\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "API Key MCM"), 76 | (re.compile(r'\bAPI_KEY_SECRET\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "API Key Secret"), 77 | (re.compile(r'\bAPP_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "App Token"), 78 | (re.compile(r'\bAPPLE_ID_PASSWORD\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{6,})(?:"|\'|)', re.IGNORECASE), "Apple ID Password"), 79 | (re.compile(r'\bSSH_PRIVATE_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_=\n]+)(?:"|\'|)', re.IGNORECASE), "SSH Private Key"), 80 | (re.compile(r'\bJWT_SECRET\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32,})(?:"|\'|)', re.IGNORECASE), "JWT Secret"), 81 | (re.compile(r'\bTOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "Generic Token"), 82 | (re.compile(r'\bDB_PASSWORD\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{8,})(?:"|\'|)', re.IGNORECASE), "Database Password"), 83 | (re.compile(r'\bGITHUB_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{40})(?:"|\'|)', re.IGNORECASE), "GitHub Token"), 84 | (re.compile(r'\bTWILIO_AUTH_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32})(?:"|\'|)', re.IGNORECASE), "Twilio Auth Token"), 85 | (re.compile(r'\bSENDGRID_API_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "SendGrid API Key"), 86 | (re.compile(r'\bMAILGUN_API_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "Mailgun API Key"), 87 | (re.compile(r'\bPUSHER_APP_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20})(?:"|\'|)', re.IGNORECASE), "Pusher App Key"), 88 | (re.compile(r'\bFACEBOOK_SECRET\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32,})(?:"|\'|)', re.IGNORECASE), "Facebook Secret Key"), 89 | (re.compile(r'\bFACEBOOK_ACCESS_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{40,})(?:"|\'|)', re.IGNORECASE), "Facebook Access Token"), 90 | (re.compile(r'\bTWITTER_API_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32})(?:"|\'|)', re.IGNORECASE), "Twitter API Key"), 91 | (re.compile(r'\bTWITTER_API_SECRET_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{40})(?:"|\'|)', re.IGNORECASE), "Twitter API Secret Key"), 92 | (re.compile(r'\bTWITTER_BEARER_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{60,})(?:"|\'|)', re.IGNORECASE), "Twitter Bearer Token"), 93 | (re.compile(r'\bGITLAB_PERSONAL_ACCESS_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "GitLab Personal Access Token"), 94 | (re.compile(r'\bPINTEREST_ACCESS_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{40})(?:"|\'|)', re.IGNORECASE), "Pinterest Access Token"), 95 | (re.compile(r'\bHEROKU_API_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{40})(?:"|\'|)', re.IGNORECASE), "Heroku API Key"), 96 | (re.compile(r'\bHEROKU_SECRET\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{40})(?:"|\'|)', re.IGNORECASE), "Heroku Secret Key"), 97 | (re.compile(r'\bSTRIPE_SECRET_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32,})(?:"|\'|)', re.IGNORECASE), "Stripe Secret Key"), 98 | (re.compile(r'\bSTRIPE_PUBLISHABLE_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32})(?:"|\'|)', re.IGNORECASE), "Stripe Publishable Key"), 99 | (re.compile(r'\bSTRIPE_API_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32,})(?:"|\'|)', re.IGNORECASE), "Stripe API Key"), 100 | (re.compile(r'\bSTRIPE_LIVE_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32})(?:"|\'|)', re.IGNORECASE), "Stripe Live Key"), 101 | (re.compile(r'\bSENDGRID_SECRET\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "SendGrid Secret Key"), 102 | (re.compile(r'\bTWILIO_ACCOUNT_SID\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{34})(?:"|\'|)', re.IGNORECASE), "Twilio Account SID"), 103 | (re.compile(r'\bNPM_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{36})(?:"|\'|)', re.IGNORECASE), "NPM Token"), 104 | (re.compile(r'\bSONARQUBE_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{36})(?:"|\'|)', re.IGNORECASE), "SonarQube Token"), 105 | (re.compile(r'\bDROPBOX_ACCESS_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{40,})(?:"|\'|)', re.IGNORECASE), "Dropbox Access Token"), 106 | (re.compile(r'\bDOCKER_CONFIG\s*:\s*(?:"|\'|)([A-Za-z0-9-_=\n]+)(?:"|\'|)', re.IGNORECASE), "Docker Config"), 107 | (re.compile(r'\bFASTLY_API_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32})(?:"|\'|)', re.IGNORECASE), "Fastly API Key"), 108 | (re.compile(r'\bFIREBASE_API_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{40})(?:"|\'|)', re.IGNORECASE), "Firebase API Key"), 109 | (re.compile(r'\bFIREBASE_SECRET\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{40})(?:"|\'|)', re.IGNORECASE), "Firebase Secret"), 110 | (re.compile(r'\bGITLAB_PRIVATE_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "GitLab Private Token"), 111 | (re.compile(r'\bBITBUCKET_CLIENT_SECRET\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32})(?:"|\'|)', re.IGNORECASE), "BitBucket Client Secret"), 112 | (re.compile(r'\bBITBUCKET_ACCESS_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32})(?:"|\'|)', re.IGNORECASE), "BitBucket Access Token"), 113 | (re.compile(r'\bREDDIT_API_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32})(?:"|\'|)', re.IGNORECASE), "Reddit API Key"), 114 | (re.compile(r'\bREDDIT_SECRET_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{40})(?:"|\'|)', re.IGNORECASE), "Reddit Secret Key"), 115 | (re.compile(r'\bSHOPIFY_API_KEY\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32})(?:"|\'|)', re.IGNORECASE), "Shopify API Key"), 116 | (re.compile(r'\bSHOPIFY_SECRET\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32})(?:"|\'|)', re.IGNORECASE), "Shopify Secret Key"), 117 | (re.compile(r'\bSHOPIFY_ACCESS_TOKEN\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{32})(?:"|\'|)', re.IGNORECASE), "Shopify Access Token"), 118 | (re.compile(r'\bGITHUB_CLIENT_SECRET\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{40})(?:"|\'|)', re.IGNORECASE), "GitHub Client Secret"), 119 | (re.compile(r'\bGOOGLE_CLOUD_PROJECT\s*:\s*(?:"|\'|)([A-Za-z0-9-_]{20,})(?:"|\'|)', re.IGNORECASE), "Google Cloud Project ID"), 120 | (re.compile(r'\b(?:aws_access_key_id|aws_secret_access_key|api_key|password|client_secret|bearer_token|private_key|github_token|twilio_auth_token|sendgrid_api_key|mailgun_api_key|pusher_app_key)\s*:\s*(?:"|\'|)([A-Za-z0-9-_+=]{20,})(?:"|\'|)', re.IGNORECASE), "Sensitive Info"), 121 | ] 122 | 123 | 124 | 125 | def check_js_for_secrets_and_comments(url, retries=3): 126 | headers = { 127 | "User-Agent": "security-researcher" 128 | } 129 | attempts = 0 130 | while attempts < retries: 131 | try: 132 | response = requests.get(url, headers=headers, verify=False) 133 | response.raise_for_status() 134 | content = response.text 135 | results = [] 136 | found_matches = set() 137 | for pattern, description in patterns: 138 | matches = pattern.findall(content) 139 | for match in matches: 140 | if match not in found_matches: 141 | found_matches.add(match) 142 | results.append((url, match, description)) 143 | return results 144 | except requests.RequestException as e: 145 | logging.error(f"Error accessing {url} (attempt {attempts + 1}/{retries}): {e}") 146 | attempts += 1 147 | if attempts < retries: 148 | time.sleep(2) # Wait for 2 seconds before retrying 149 | return [] 150 | 151 | 152 | def log_results(results): 153 | with open('exposure_results.txt', 'a') as file: 154 | for url, match, description in results: 155 | file.write(f'Found an exposure: "{match}" ({description}) at URL "{url}" | Length: {len(match)}\n') 156 | 157 | 158 | def save_results_as_json(results): 159 | formatted_results = [{'url': url, 'match': match, 'description': description, 'length': len(match)} for url, match, description in results] 160 | with open('exposure_results.json', 'w') as json_file: 161 | json.dump(formatted_results, json_file, indent=4) 162 | 163 | 164 | def process_js_files(file_path, max_workers): 165 | try: 166 | urls = load_urls_from_file(file_path) 167 | if not urls: 168 | logging.warning("No URLs to process.") 169 | return 170 | 171 | all_results = [] 172 | logging.info(f"Processing {len(urls)} URLs.") 173 | 174 | with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: 175 | future_to_url = {executor.submit(check_js_for_secrets_and_comments, url): url for url in urls} 176 | 177 | for future in concurrent.futures.as_completed(future_to_url): 178 | url = future_to_url[future] 179 | try: 180 | results = future.result() 181 | if results: 182 | all_results.extend(results) 183 | logging.info(f"Found results at {url}.") 184 | except Exception as e: 185 | logging.error(f"Error processing {url}: {e}") 186 | 187 | if all_results: 188 | log_results(all_results) 189 | save_results_as_json(all_results) 190 | logging.info(f"Analysis complete. Found {len(all_results)} exposures.") 191 | else: 192 | logging.info("No exposures found.") 193 | except KeyboardInterrupt: 194 | logging.warning("Process interrupted by user. Exiting...") 195 | 196 | 197 | def main(): 198 | parser = argparse.ArgumentParser( 199 | description="JS Exposures Finder - A tool to scan JavaScript files for sensitive information.", 200 | epilog="Example usage: python jsexposures.py --file js_endpoints.txt --max-workers 15 --log-level DEBUG" 201 | ) 202 | parser.add_argument('--file', type=str, default='js_endpoints.txt', help='Path to the file containing JS URLs to scan (default: js_endpoints.txt)') 203 | parser.add_argument('--max-workers', type=int, default=10, help='Number of concurrent threads to use for scanning (default: 10)') 204 | parser.add_argument('--log-level', type=str, default='INFO', help='Set the logging level (default: INFO)') 205 | 206 | args = parser.parse_args() 207 | 208 | configure_logging(args.log_level) 209 | print_banner() 210 | process_js_files(args.file, args.max_workers) 211 | 212 | 213 | if __name__ == "__main__": 214 | main() 215 | 216 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | urllib3 3 | --------------------------------------------------------------------------------