├── popup.png ├── pixel_pig.png ├── icons ├── icon128.png └── icon48.png ├── com.webtrufflehog.json ├── manifest.json ├── background.js ├── README.md ├── popup.html ├── popup.js ├── setup.sh └── native_host.py /popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c3l3si4n/webtrufflehog/HEAD/popup.png -------------------------------------------------------------------------------- /pixel_pig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c3l3si4n/webtrufflehog/HEAD/pixel_pig.png -------------------------------------------------------------------------------- /icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c3l3si4n/webtrufflehog/HEAD/icons/icon128.png -------------------------------------------------------------------------------- /icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c3l3si4n/webtrufflehog/HEAD/icons/icon48.png -------------------------------------------------------------------------------- /com.webtrufflehog.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.webtrufflehog", 3 | "description": "Native messaging host for WebTruffleHog", 4 | "path": "REPLACE_ME", 5 | "type": "stdio", 6 | "allowed_origins": [ 7 | "chrome-extension://akoofbljmjeodfmdpjndmmnifglppjdi/" 8 | ] 9 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "WebTruffleHog", 4 | "version": "1.0", 5 | "description": "Scans web traffic for exposed secrets using TruffleHog", 6 | "permissions": [ 7 | "webRequest", 8 | "nativeMessaging", 9 | "storage", 10 | "tabs" 11 | ], 12 | "host_permissions": [ 13 | "" 14 | ], 15 | "background": { 16 | "service_worker": "background.js" 17 | }, 18 | "action": { 19 | "default_popup": "popup.html", 20 | "default_icon": { 21 | "48": "icons/icon48.png", 22 | "128": "icons/icon128.png" 23 | } 24 | }, 25 | "icons": { 26 | "48": "icons/icon48.png", 27 | "128": "icons/icon128.png" 28 | } 29 | } -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | let port = null; 2 | let statusCheckInterval = null; 3 | 4 | function connectNativeHost() { 5 | port = chrome.runtime.connectNative('com.webtrufflehog'); 6 | 7 | port.onMessage.addListener((response) => { 8 | if (response.findings && response.findings.length > 0) { 9 | // Add timestamp and URL to each finding 10 | const findings = response.findings.map(finding => ({ 11 | ...finding, 12 | timestamp: Date.now(), 13 | url: response.url 14 | })); 15 | 16 | console.log('Secrets found:', findings); 17 | chrome.storage.local.set({ 18 | [`findings_${response.id}`]: findings 19 | }); 20 | } else if (response.status !== undefined) { 21 | // Store queue size in storage 22 | chrome.storage.local.set({ queueSize: response.status }); 23 | } 24 | }); 25 | 26 | port.onDisconnect.addListener(() => { 27 | console.error('Disconnected from native host:', chrome.runtime.lastError); 28 | port = null; 29 | if (statusCheckInterval) { 30 | clearInterval(statusCheckInterval); 31 | } 32 | }); 33 | 34 | // Start periodic status checks 35 | statusCheckInterval = setInterval(() => { 36 | if (port) { 37 | port.postMessage({ status: 'check' }); 38 | } 39 | }, 2000); // Check every 2 seconds 40 | } 41 | 42 | // Listen for web requests 43 | chrome.webRequest.onCompleted.addListener( 44 | (details) => { 45 | if (!port) { 46 | connectNativeHost(); 47 | } 48 | 49 | // Filter out binary formats and images 50 | const contentType = details.responseHeaders?.find(h => 51 | h.name.toLowerCase() === 'content-type' 52 | )?.value || ''; 53 | 54 | if (contentType.includes('text/')|| 55 | contentType.includes('script/') || 56 | contentType.includes('application/')) { 57 | 58 | port.postMessage({ 59 | id: details.requestId, 60 | url: details.url, 61 | type: details.type 62 | }); 63 | } 64 | }, 65 | { urls: [""] }, 66 | ["responseHeaders"] 67 | ); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebTruffleHog 2 | 3 | ![WebTruffleHog Logo](icons/icon128.png) 4 | 5 | WebTruffleHog is a Chrome/Chromium extension that scans web traffic in real-time for exposed secrets using TruffleHog. It helps security professionals, bug bounty hunters and developers identify potential security risks by detecting sensitive information like API keys, passwords, and tokens that might be accidentally exposed in web responses. 6 | ## Key Features 7 | 8 | - 🔍 **Real-time Scanning**: Automatically scans all web traffic for exposed secrets as you browse 9 | - 🔌 **Native TruffleHog Integration**: Leverages the secret detection capabilities of TruffleHog 10 | - 👀 **User-friendly Interface**: UI showing detected secrets with found URL, secret contents, and verification status 11 | - ⚡ **High Performance**: Queue system and URL caching to handle large volumes of traffic without lagging your browser 12 | 13 | ## How it works? 14 | 15 | WebTruffleHog leverages Chrome's webRequest API, specifically the onCompleted event listener, to analyze completed HTTP requests. Upon event triggering, the extension establishes communication with a native host process through Chrome's Native Messaging protocol. 16 | 17 | The native host functions as a wrapper for the TruffleHog scanner, executing in an isolated process for security and stability. When receiving a URL, it initiates a fresh HTTP request to fetch and analyze the content, as direct access to Chrome's cache or local storage is architecturally restricted. While this approach ensures accurate scanning, it introduces limitations for authenticated sessions and dynamically generated content. 18 | 19 | The implementation requires a Native Messaging Host manifest in Chrome's configuration directory (typically ~/.config/google-chrome on Linux systems), which establishes the bridge between the extension and the TruffleHog scanner process. 20 | 21 | 22 | 23 | ## 🚀 Getting Started 24 | 25 | ### Prerequisites 26 | 27 | - Linux-based operating system 28 | - Chrome or Chromium-based browser 29 | - Python 3.x 30 | - TruffleHog (must be available in system PATH) 31 | 32 | ## Installation 33 | 34 | 1. Clone this repository: 35 | ```bash 36 | git clone https://github.com/c3l3si4n/webtrufflehog.git 37 | ``` 38 | 2. Run the setup script pointing to your Chrome/Chromium config directory. (You can find that in chrome://version) 39 | ```bash 40 | sudo ./setup.sh --chrome-dir /path/to/chrome/config 41 | ``` 42 | 3. Reload your browser 43 | 4. Open chrome://extensions 44 | 5. Enable "Developer mode" 45 | 6. Click "Load unpacked" 46 | 7. Select the "/opt/webtrufflehog" directory and load the extension 47 | 8. Now you should see the extension icon in the toolbar. The tool works passively and is always scanning all visited URLs on your browser. 48 | 49 | # Usage 50 | 51 | The tool works passively and is always scanning all visited URLs on your browser. 52 | You can view the results in the extension popup by clicking the icon in the toolbar. 53 | 54 | ![Extension Popup](popup.png) 55 | 56 | # Troubleshooting 57 | 58 | - If the tool is not working, you can check the logs in /opt/webtrufflehog/webtrufflehog.log 59 | - Results are saved in /opt/webtrufflehog/results.json 60 | - If you want to remove the tool, you can disable the extension in chrome://extensions and remove the native messaging host in your chrome directory. 61 | 62 | 63 | # Warning 64 | 65 | While I do this, I don't advise using this tool on your main browser. It probably takes quite a bit of performance overhead and could lag your browser, specially in high-traffic environments. 66 | 67 | # Credits 68 | 69 | - [TruffleHog](https://github.com/trufflesecurity/trufflehog) and Dylan from TruffleSec -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TruffleHog 5 | 140 | 141 | 142 |
143 |

WebTruffleHog Results

144 |
145 |
146 | 147 | Queue: 0 148 |
149 | 150 |
151 |
152 |
153 | 154 |
155 | 156 | 157 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | function formatDate(timestamp) { 2 | return new Date(timestamp).toLocaleString(); 3 | } 4 | 5 | function truncateString(str, length) { 6 | if (str.length <= length) return str; 7 | return str.substring(0, length) + '...'; 8 | } 9 | 10 | function createFindingCard(finding, url) { 11 | const card = document.createElement('div'); 12 | card.className = 'finding-card'; 13 | 14 | const urlElement = document.createElement('div'); 15 | urlElement.className = 'finding-url'; 16 | urlElement.textContent = url; 17 | card.appendChild(urlElement); 18 | 19 | if (finding.Raw) { 20 | const rawDetail = document.createElement('div'); 21 | rawDetail.className = 'finding-detail'; 22 | rawDetail.textContent = truncateString(finding.Raw, 200); 23 | card.appendChild(rawDetail); 24 | } 25 | 26 | const typeElement = document.createElement('div'); 27 | typeElement.className = 'finding-type'; 28 | typeElement.textContent = finding.DetectorName || 'Unknown Type'; 29 | card.appendChild(typeElement); 30 | 31 | const verificationElement = document.createElement('div'); 32 | verificationElement.style.padding = '4px 8px'; 33 | verificationElement.style.borderRadius = '4px'; 34 | verificationElement.style.display = 'inline-block'; 35 | verificationElement.style.marginTop = '8px'; 36 | verificationElement.style.marginLeft = '8px'; 37 | verificationElement.style.fontSize = '12px'; 38 | 39 | if (finding.Verified) { 40 | verificationElement.style.backgroundColor = '#90EE90'; 41 | verificationElement.style.color = '#006400'; 42 | verificationElement.textContent = 'Verified'; 43 | } else { 44 | verificationElement.style.backgroundColor = '#D3D3D3'; 45 | verificationElement.style.color = '#696969'; 46 | verificationElement.textContent = 'Unverified'; 47 | } 48 | card.appendChild(verificationElement); 49 | 50 | return card; 51 | } 52 | 53 | function updateQueueStatus(queueSize) { 54 | const queueStatus = document.getElementById('queueStatus'); 55 | const queueSizeElement = document.getElementById('queueSize'); 56 | 57 | queueSizeElement.textContent = `Queue: ${queueSize}`; 58 | 59 | if (queueSize > 0) { 60 | queueStatus.classList.add('active'); 61 | } else { 62 | queueStatus.classList.remove('active'); 63 | } 64 | } 65 | 66 | function updateFindings() { 67 | chrome.storage.local.get(null, (items) => { 68 | const findingsList = document.getElementById('findingsList'); 69 | findingsList.innerHTML = ''; 70 | 71 | let totalFindings = 0; 72 | const findings = []; 73 | 74 | // Update queue status if available 75 | if ('queueSize' in items) { 76 | updateQueueStatus(items.queueSize); 77 | } 78 | 79 | // Collect all findings 80 | Object.entries(items).forEach(([key, value]) => { 81 | if (key.startsWith('findings_')) { 82 | findings.push(...value); 83 | totalFindings += value.length; 84 | } 85 | }); 86 | 87 | // Update stats 88 | document.getElementById('findingsCount').textContent = 89 | `${totalFindings} secrets found`; 90 | 91 | if (findings.length === 0) { 92 | const noFindings = document.createElement('div'); 93 | noFindings.className = 'no-findings'; 94 | noFindings.textContent = 'No secrets found yet'; 95 | findingsList.appendChild(noFindings); 96 | return; 97 | } 98 | 99 | // Sort findings by timestamp (newest first) 100 | findings.sort((a, b) => b.timestamp - a.timestamp); 101 | 102 | // Create cards for each finding 103 | findings.forEach(finding => { 104 | const card = createFindingCard(finding, finding.url); 105 | findingsList.appendChild(card); 106 | }); 107 | }); 108 | } 109 | 110 | // Update findings when popup opens 111 | document.addEventListener('DOMContentLoaded', updateFindings); 112 | 113 | // Listen for changes in storage 114 | chrome.storage.onChanged.addListener(updateFindings); -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # Exit on any error 4 | 5 | # Colors for output 6 | RED='\033[0;31m' 7 | GREEN='\033[0;32m' 8 | YELLOW='\033[1;33m' 9 | NC='\033[0m' # No Color 10 | 11 | # Default paths 12 | DEFAULT_INSTALL_DIR="/opt/webtrufflehog" 13 | 14 | # Function to print error and exit 15 | error_exit() { 16 | echo -e "${RED}Error: $1${NC}" >&2 17 | exit 1 18 | } 19 | 20 | # Function to print warning 21 | warning() { 22 | echo -e "${YELLOW}Warning: $1${NC}" 23 | } 24 | 25 | # Function to print success 26 | success() { 27 | echo -e "${GREEN}$1${NC}" 28 | } 29 | 30 | # make sure script is run as root 31 | if [ "$EUID" -ne 0 ]; then 32 | error_exit "Please run this script as root" 33 | fi 34 | 35 | # Parse command line arguments 36 | CHROME_DIR="" 37 | 38 | while [[ "$#" -gt 0 ]]; do 39 | case $1 in 40 | --chrome-dir) CHROME_DIR="$2"; shift ;; 41 | *) error_exit "Unknown parameter: $1" ;; 42 | esac 43 | shift 44 | done 45 | 46 | # Check if chrome directory is provided 47 | if [ -z "$CHROME_DIR" ]; then 48 | error_exit "Please provide your Chrome/Chromium config directory using --chrome-dir\n\nTo find your Chrome config directory:\n1. Open Chrome/Chromium\n2. Go to chrome://version\n3. Look for 'Profile Path'\n4. Use the parent directory of that path\n\nExample:\nIf Profile Path is '/home/user/.config/google-chrome/Default'\nUse: --chrome-dir '/home/user/.config/google-chrome'" 49 | fi 50 | 51 | # check if native messaging hosts directory exists 52 | if [ ! -d "$CHROME_DIR/NativeMessagingHosts" ]; then 53 | error_exit "Native messaging hosts directory does not exist. Trying parent directory..." 54 | CHROME_DIR=$(dirname "$CHROME_DIR") 55 | if [ ! -d "$CHROME_DIR/NativeMessagingHosts" ]; then 56 | error_exit "Native messaging hosts directory does not exist. Please create it first." 57 | fi 58 | fi 59 | 60 | # Check if trufflehog is installed 61 | if ! command -v trufflehog &> /dev/null; then 62 | error_exit "trufflehog is not installed. Please install it first and add it to your system PATH." 63 | fi 64 | 65 | # Set installation directory for native messaging host 66 | INSTALL_NMH_DIR="$CHROME_DIR/NativeMessagingHosts" 67 | 68 | # Create installation directories 69 | echo "Creating installation directories..." 70 | mkdir -p "$DEFAULT_INSTALL_DIR" || error_exit "Failed to create installation directory" 71 | mkdir -p "$INSTALL_NMH_DIR" || error_exit "Failed to create native messaging hosts directory" 72 | 73 | # Copy files to installation directory 74 | echo "Copying files to installation directory..." 75 | cp native_host.py "$DEFAULT_INSTALL_DIR/" || error_exit "Failed to copy native_host.py" 76 | cp manifest.json "$DEFAULT_INSTALL_DIR/" || error_exit "Failed to copy manifest.json" 77 | cp -r icons "$DEFAULT_INSTALL_DIR/" || error_exit "Failed to copy icons" 78 | cp popup.html "$DEFAULT_INSTALL_DIR/" || error_exit "Failed to copy popup.html" 79 | cp popup.js "$DEFAULT_INSTALL_DIR/" || error_exit "Failed to copy popup.js" 80 | cp background.js "$DEFAULT_INSTALL_DIR/" || error_exit "Failed to copy background.js" 81 | 82 | # Make native host executable 83 | chmod +x "$DEFAULT_INSTALL_DIR/native_host.py" || error_exit "Failed to make native_host.py executable" 84 | 85 | # Update com.webtrufflehog.json with correct path 86 | echo "Configuring native messaging host..." 87 | sed "s/REPLACE_ME/$(echo -n $DEFAULT_INSTALL_DIR | sed 's/\//\\\//g')\/native_host.py/g" com.webtrufflehog.json > "$INSTALL_NMH_DIR/com.webtrufflehog.json" || error_exit "Failed to create native messaging host configuration" 88 | 89 | # Set correct permissions 90 | chmod 644 "$INSTALL_NMH_DIR/com.webtrufflehog.json" || error_exit "Failed to set permissions on native messaging host configuration" 91 | 92 | # Create results directory and file with correct permissions 93 | mkdir -p /tmp/webtrufflehog || error_exit "Failed to create results directory" 94 | touch /tmp/results.json || error_exit "Failed to create results file" 95 | chmod 644 /tmp/results.json || error_exit "Failed to set permissions on results file" 96 | 97 | # Verify installation 98 | echo "Verifying installation..." 99 | if [ ! -x "$DEFAULT_INSTALL_DIR/native_host.py" ]; then 100 | error_exit "Native host is not executable" 101 | fi 102 | 103 | if [ ! -f "$INSTALL_NMH_DIR/com.webtrufflehog.json" ]; then 104 | error_exit "Native messaging host configuration not found" 105 | fi 106 | 107 | # Installation complete 108 | success "Installation completed successfully!" 109 | echo 110 | echo "Installation details:" 111 | echo "- Main files installed to: $DEFAULT_INSTALL_DIR" 112 | echo "- Native messaging host configuration: $INSTALL_NMH_DIR/com.webtrufflehog.json" 113 | echo "- Results file location: /tmp/results.json" 114 | echo 115 | echo "Usage:" 116 | echo "- Load the extension in Chrome/Chromium by going to chrome://extensions/" 117 | echo "- Enable Developer mode" 118 | echo "- Click 'Load unpacked' and select: $DEFAULT_INSTALL_DIR" 119 | echo 120 | warning "Note: You may need to restart Chrome for the changes to take effect" -------------------------------------------------------------------------------- /native_host.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import json 4 | import struct 5 | import subprocess 6 | import tempfile 7 | import queue 8 | import threading 9 | import requests 10 | import os 11 | import hashlib 12 | from urllib.parse import urlparse 13 | 14 | # Global queues and caches 15 | url_queue = queue.Queue() 16 | result_queue = queue.Queue() 17 | url_cache = set() # Store URL hashes 18 | result_cache = {} # Store content hash -> findings mapping 19 | 20 | 21 | # save all stderr to a file 22 | sys.stderr = open('/tmp/webtrufflehog.log', 'w') 23 | 24 | # Helper function to send messages to the extension 25 | def send_message(message): 26 | try: 27 | encoded_content = json.dumps(message).encode('utf-8') 28 | sys.stdout.buffer.write(struct.pack('I', len(encoded_content))) 29 | sys.stdout.buffer.write(encoded_content) 30 | sys.stdout.buffer.flush() 31 | except Exception as e: 32 | print(f"Error sending message: {str(e)}", file=sys.stderr) 33 | 34 | # Helper function to read messages from the extension 35 | def read_message(): 36 | try: 37 | raw_length = sys.stdin.buffer.read(4) 38 | if not raw_length: 39 | return None 40 | message_length = struct.unpack('I', raw_length)[0] 41 | message = sys.stdin.buffer.read(message_length).decode('utf-8') 42 | return json.loads(message) 43 | except Exception: 44 | return None 45 | 46 | def get_url_hash(url): 47 | return hashlib.md5(url.encode()).hexdigest() 48 | 49 | def get_content_hash(content): 50 | return hashlib.md5(content.encode()).hexdigest() 51 | 52 | def download_url(url): 53 | try: 54 | response = requests.get(url, timeout=30) 55 | response.raise_for_status() 56 | return response.text 57 | except Exception as e: 58 | print(f"Error downloading {url}: {str(e)}", file=sys.stderr) 59 | return None 60 | 61 | def scan_with_trufflehog(content, temp_dir): 62 | if not content: 63 | return [] 64 | 65 | temp_file = os.path.join(temp_dir, 'scan_target.txt') 66 | try: 67 | with open(temp_file, 'w') as f: 68 | f.write(content) 69 | 70 | result = subprocess.run( 71 | ['trufflehog', 'filesystem', temp_file, '--json'], 72 | capture_output=True, 73 | text=True 74 | ) 75 | findings = [json.loads(line) for line in result.stdout.splitlines() if line.strip()] 76 | return findings 77 | except Exception as e: 78 | print(f"Error scanning with trufflehog: {str(e)}", file=sys.stderr) 79 | return [] 80 | finally: 81 | try: 82 | if os.path.exists(temp_file): 83 | os.remove(temp_file) 84 | except Exception as e: 85 | print(f"Error removing temp file: {str(e)}", file=sys.stderr) 86 | 87 | def worker(): 88 | while True: 89 | temp_dir = None 90 | try: 91 | temp_dir = tempfile.mkdtemp() 92 | job = url_queue.get() 93 | if job is None: # Poison pill 94 | break 95 | 96 | url = job['url'] 97 | job_id = job['id'] 98 | 99 | # Check URL cache 100 | url_hash = get_url_hash(url) 101 | if url_hash in url_cache: 102 | url_queue.task_done() 103 | continue 104 | 105 | content = download_url(url) 106 | if content: 107 | content_hash = get_content_hash(content) 108 | 109 | # Check result cache 110 | if content_hash in result_cache: 111 | findings = result_cache[content_hash] 112 | else: 113 | findings = scan_with_trufflehog(content, temp_dir) 114 | result_cache[content_hash] = findings 115 | 116 | if findings: 117 | result_queue.put({ 118 | 'id': job_id, 119 | 'url': url, 120 | 'findings': findings 121 | }) 122 | 123 | url_cache.add(url_hash) 124 | 125 | url_queue.task_done() 126 | except Exception as e: 127 | print(f"Worker error: {str(e)}", file=sys.stderr) 128 | finally: 129 | try: 130 | if temp_dir and os.path.exists(temp_dir): 131 | os.rmdir(temp_dir) 132 | except Exception as e: 133 | print(f"Error removing temp dir: {str(e)}", file=sys.stderr) 134 | 135 | def append_result(result,filename): 136 | with open(filename, 'a') as f: 137 | f.write(json.dumps(result) + '\n') 138 | 139 | def result_sender(): 140 | while True: 141 | try: 142 | result = result_queue.get() 143 | if result is None: # Poison pill 144 | break 145 | 146 | append_result(result, '/tmp/results.json') 147 | send_message(result) 148 | result_queue.task_done() 149 | except Exception as e: 150 | print(f"Result sender error: {str(e)}", file=sys.stderr) 151 | 152 | def main(): 153 | # Start worker threads 154 | num_workers = 10 155 | workers = [] 156 | for _ in range(num_workers): 157 | t = threading.Thread(target=worker) 158 | t.daemon = True 159 | t.start() 160 | workers.append(t) 161 | 162 | # Start result sender thread 163 | sender = threading.Thread(target=result_sender) 164 | sender.daemon = True 165 | sender.start() 166 | 167 | try: 168 | while True: 169 | message = read_message() 170 | if not message: 171 | break 172 | 173 | if 'url' in message: 174 | url_queue.put({ 175 | 'id': message.get('id'), 176 | 'url': message['url'] 177 | }) 178 | if 'status' in message: 179 | # return number of queue items 180 | send_message({'status': url_queue.qsize()}) 181 | finally: 182 | # Clean shutdown 183 | for _ in workers: 184 | url_queue.put(None) 185 | result_queue.put(None) 186 | 187 | for w in workers: 188 | w.join() 189 | sender.join() 190 | 191 | if __name__ == "__main__": 192 | main() 193 | --------------------------------------------------------------------------------