├── LICENSE ├── README.md ├── background.js ├── content.js ├── icons ├── icon.svg ├── icon128.png ├── icon16.png ├── icon32.png └── icon48.png ├── manifest.json ├── package.json ├── popup.html ├── popup.js ├── screenshots ├── image1.png ├── image2.png ├── image3.png └── image4.png ├── scripts └── generate-icons.js └── styles.css /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 VulnCure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProxSec Browser Extension 2 | 3 |
4 | ProxSec Logo 5 |
6 |

Proxy Management for Security Professionals

7 | 8 | ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) 9 | ![Platform](https://img.shields.io/badge/Platform-Chrome%20|%20Firefox%20|%20Edge-lightgrey) 10 | ![Version](https://img.shields.io/badge/Version-1.0.0-purple) 11 |
12 | 13 | ## 🔍 Overview 14 | 15 | ProxSec is an open-source browser extension designed for bug bounty hunters and security professionals. It streamlines your workflow with efficient proxy management, bug bounty program tracking, and scope checking capabilities - all in a clean, minimalist interface. 16 | 17 | ## 📸 Screenshots 18 | 19 | Here's a visual tour of ProxSec's key features and interface: 20 | 21 | 22 | 23 | 30 | 37 | 38 | 39 | 46 | 53 | 54 |
24 |

25 | Dashboard Overview
26 | Dashboard Overview
27 | The main dashboard provides quick access to proxy controls and program status 28 |

29 |
31 |

32 | Proxy Management
33 | Proxy Configuration
34 | Easily configure and manage multiple proxy settings 35 |

36 |
40 |

41 | Program Management
42 | Program Settings
43 | Add and manage bug bounty programs with detailed scope settings 44 |

45 |
47 |

48 | Active Monitoring
49 | Active Status
50 | Real-time status monitoring and scope validation 51 |

52 |
55 | 56 | ## 💡 Why ProxSec? 57 | 58 | - **Streamlined Testing:** Quickly toggle proxies on/off without opening multiple tabs or navigating away from your work 59 | - **Stay In Scope:** Avoid out-of-scope testing with real-time domain validation 60 | - **Time Saving:** Manage all your proxies and programs in one central location 61 | - **Lightweight:** Minimal impact on browser performance 62 | - **Privacy-Focused:** Your data never leaves your browser 63 | 64 | ## ✨ Features 65 | 66 | ### 🔄 Proxy Management 67 | - **Quick Toggle:** Enable/disable proxies from any tab with a single click 68 | - **Multiple Configurations:** Store and switch between various proxy setups 69 | - **Protocol Support:** Compatible with HTTP and HTTPS proxies 70 | - **Credentials:** Securely store authentication details for proxies that require them 71 | - **Visual Status:** Clear indicators for active/inactive state 72 | 73 | ### 🎯 Bug Bounty Programs 74 | - **Program Tracking:** Store details of multiple bug bounty programs 75 | - **Scope Awareness:** Check if domains are in scope for your enrolled programs 76 | - **Domain Patterns:** Support for wildcard domain patterns (e.g., *.example.com) 77 | - **Program Notes:** Keep notes specific to each program 78 | 79 | ### 🔒 Security & Privacy 80 | - **Local Storage:** All data remains in your browser - nothing is sent to external servers 81 | - **Encrypted Storage:** Sensitive data is encrypted using browser's built-in storage encryption 82 | - **Open Source:** Transparent codebase with MIT license 83 | 84 | ## 🚀 Installation 85 | 86 | ### From Web Store 87 | *Coming soon to Chrome Web Store, Firefox Add-ons, and Edge Add-ons* 88 | 89 | ### Developer Installation 90 | 1. Clone this repository 91 | ```bash 92 | git clone https://github.com/aacle/proxsec.git 93 | ``` 94 | 95 | 2. Open your browser's extension management page: 96 | - Chrome: Navigate to `chrome://extensions/` 97 | - Firefox (Comming Soon) 98 | 99 | 3. Enable Developer Mode 100 | 101 | 4. Click "Load unpacked" (Chrome/Edge) select the extension directory 102 | 103 | ## 📖 How to Use 104 | 105 | ### Managing Proxies 106 | 1. Click the ProxSec icon in your browser toolbar 107 | 2. Use the quick toggle in the header to enable/disable your active proxy 108 | 3. Navigate to the "Proxies" tab to add, edit, or remove proxies 109 | 4. Fill in the required details for each proxy: 110 | - Name: A descriptive name 111 | - Protocol: HTTP or HTTPS 112 | - IP Address: Typically 127.0.0.1 for local proxies 113 | - Port: Common ports include 8080 for Burp Suite, 8090 for OWASP ZAP 114 | - Username/Password: If required by your proxy 115 | 116 | ### Tracking Bug Bounty Programs 117 | 1. Navigate to the "Programs" tab 118 | 2. Click "Add Program" to create a new entry 119 | 3. Enter the program details: 120 | - Name: Organization or program name 121 | - URL: Program URL 122 | - Scope Domains: List of in-scope domains, one per line 123 | - Notes: Any additional information 124 | 125 | ### Checking Scope 126 | 1. Visit any website 127 | 2. Open the extension 128 | 3. The dashboard will show if the current domain is in scope for any of your programs 129 | 130 | ## 🔄 Sample Workflow 131 | 132 | Here's a typical workflow example using ProxSec: 133 | 134 | 1. **Setup your testing proxies** 135 | ``` 136 | ✅ Add Burp Suite proxy (127.0.0.1:8080) 137 | ✅ Add OWASP ZAP proxy (127.0.0.1:8090) 138 | ``` 139 | 140 | 2. **Configure bug bounty programs** 141 | ``` 142 | ✅ Add "Example Corp" program 143 | ✅ Add domains: *.example.com, api.example.com 144 | ✅ Add program notes and details 145 | ``` 146 | 147 | 3. **Begin testing** 148 | ``` 149 | ✅ Navigate to target site 150 | ✅ Toggle proxy ON with one click 151 | ✅ Extension automatically checks if site is in scope 152 | ✅ Begin your testing 153 | ``` 154 | 155 | 4. **Switch tools effortlessly** 156 | ``` 157 | ✅ Toggle current proxy OFF 158 | ✅ Select different proxy configuration 159 | ✅ Toggle new proxy ON 160 | ✅ Continue testing without disruption 161 | ``` 162 | 163 | ## 🛠️ Coming Soon 164 | - Note-taking for vulnerabilities 165 | - Customizable recon tools 166 | - Vulnerability report templates 167 | 168 | ## 🤝 Contributing 169 | 170 | Contributions are welcome! Please feel free to submit a Pull Request. 171 | 172 | 1. Fork the repository 173 | 2. Create your feature branch: `git checkout -b feature/amazing-feature` 174 | 3. Commit your changes: `git commit -m 'Add some amazing feature'` 175 | 4. Push to the branch: `git push origin feature/amazing-feature` 176 | 5. Open a Pull Request 177 | 178 | ## 🔐 Security 179 | 180 | If you discover any security issues, please report them via GitHub Issues instead of opening a public issue. 181 | 182 | ## ⭐ Support 183 | 184 | If you find ProxSec helpful for your work: 185 | 186 | - Star the GitHub repository 187 | - Share with other security professionals 188 | - Report bugs or suggest features through GitHub Issues 189 | - Consider contributing to the codebase 190 | 191 | ## 📄 License 192 | 193 | This project is licensed under the MIT License - see the LICENSE file for details. 194 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | // ProxSec Background Script 2 | 3 | // Log helper 4 | function log(message) { 5 | console.log(`[ProxSec Background] ${message}`); 6 | } 7 | 8 | // Initialize proxy configurations when extension is installed 9 | chrome.runtime.onInstalled.addListener(() => { 10 | log('Extension installed or updated'); 11 | 12 | // Initialize storage 13 | chrome.storage.local.get(['proxies', 'programs'], (data) => { 14 | log('Initializing storage'); 15 | 16 | // Initialize proxies if not exists 17 | if (!data.proxies) { 18 | log('Proxies not found, initializing empty array'); 19 | chrome.storage.local.set({ 20 | proxies: [], 21 | activeProxy: null 22 | }); 23 | } else { 24 | log(`Found ${data.proxies.length} proxies`); 25 | // Check for any proxies in the old format and migrate them 26 | const updatedProxies = data.proxies.map(proxy => { 27 | if (proxy.url && (!proxy.protocol || !proxy.ip || !proxy.port)) { 28 | // Convert from old format to new format 29 | try { 30 | const urlObj = new URL(proxy.url); 31 | return { 32 | ...proxy, 33 | protocol: urlObj.protocol.replace(':', ''), 34 | ip: urlObj.hostname, 35 | port: parseInt(urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80)) 36 | }; 37 | } catch (e) { 38 | console.error('Error converting proxy format:', e); 39 | return proxy; 40 | } 41 | } 42 | return proxy; 43 | }); 44 | 45 | // Save the updated proxies back to storage if we made any changes 46 | if (JSON.stringify(updatedProxies) !== JSON.stringify(data.proxies)) { 47 | log('Updated proxy format, saving to storage'); 48 | chrome.storage.local.set({ proxies: updatedProxies }); 49 | } 50 | } 51 | 52 | // Initialize programs if not exists 53 | if (!data.programs) { 54 | log('Programs not found, initializing empty array'); 55 | chrome.storage.local.set({ programs: [] }); 56 | } else { 57 | log(`Found ${data.programs.length} programs`); 58 | } 59 | }); 60 | 61 | // Set up badge defaults 62 | chrome.action.setBadgeBackgroundColor({ color: '#9ca3af' }); // Grey 63 | chrome.action.setBadgeText({ text: "" }); 64 | 65 | log('Initial setup complete'); 66 | }); 67 | 68 | // Function to set the active proxy 69 | async function setActiveProxy(proxy) { 70 | if (!proxy) { 71 | // Clear proxy settings if no proxy is selected 72 | await clearProxySettings(); 73 | return { success: true, message: 'Proxy settings cleared' }; 74 | } 75 | 76 | try { 77 | // Initialize variables with safe defaults 78 | let scheme = null; 79 | let host = null; 80 | let port = null; 81 | 82 | // Safely extract protocol, IP and port - no using 'split' directly 83 | if (typeof proxy === 'object') { 84 | // Check if we have the new format with all required fields 85 | if (proxy.protocol && typeof proxy.protocol === 'string' && 86 | proxy.ip && typeof proxy.ip === 'string' && 87 | proxy.port !== undefined) { 88 | 89 | scheme = proxy.protocol; 90 | host = proxy.ip; 91 | 92 | // Convert port to number safely 93 | if (typeof proxy.port === 'number') { 94 | port = proxy.port; 95 | } else if (typeof proxy.port === 'string') { 96 | port = parseInt(proxy.port, 10); 97 | } 98 | } 99 | // Check if we have the old URL format 100 | else if (proxy.url && typeof proxy.url === 'string') { 101 | try { 102 | const urlObj = new URL(proxy.url); 103 | scheme = urlObj.protocol.replace(':', ''); 104 | host = urlObj.hostname; 105 | 106 | // Get port from URL or use defaults 107 | if (urlObj.port) { 108 | port = parseInt(urlObj.port, 10); 109 | } else { 110 | port = (urlObj.protocol === 'https:') ? 443 : 80; 111 | } 112 | } catch (e) { 113 | return { success: false, message: `Invalid proxy URL: ${e.message}` }; 114 | } 115 | } 116 | } 117 | 118 | // Validate all required values 119 | if (!scheme) { 120 | return { success: false, message: 'Invalid proxy: Missing protocol' }; 121 | } 122 | 123 | if (!host) { 124 | return { success: false, message: 'Invalid proxy: Missing host/IP address' }; 125 | } 126 | 127 | if (!port || isNaN(port) || port < 1 || port > 65535) { 128 | return { success: false, message: 'Invalid proxy: Missing or invalid port number' }; 129 | } 130 | 131 | // Create configuration object 132 | const config = { 133 | mode: 'fixed_servers', 134 | rules: { 135 | singleProxy: { 136 | scheme: scheme, 137 | host: host, 138 | port: port 139 | }, 140 | bypassList: ['localhost'] 141 | } 142 | }; 143 | 144 | // Apply proxy settings 145 | await chrome.proxy.settings.set({ 146 | value: config, 147 | scope: 'regular' 148 | }); 149 | 150 | // Update the active proxy in storage 151 | if (proxy.id) { 152 | await chrome.storage.local.set({ activeProxy: proxy.id }); 153 | } 154 | 155 | return { success: true, message: `Proxy ${proxy.name || 'unnamed'} activated` }; 156 | } catch (error) { 157 | console.error('Error setting proxy:', error); 158 | return { success: false, message: error.message || 'Unknown error occurred' }; 159 | } 160 | } 161 | 162 | // Function to clear proxy settings 163 | async function clearProxySettings() { 164 | try { 165 | await chrome.proxy.settings.set({ 166 | value: { mode: 'direct' }, 167 | scope: 'regular' 168 | }); 169 | await chrome.storage.local.set({ activeProxy: null }); 170 | return { success: true, message: 'Proxy settings cleared' }; 171 | } catch (error) { 172 | console.error('Error clearing proxy:', error); 173 | return { success: false, message: error.message }; 174 | } 175 | } 176 | 177 | // Function to test a proxy 178 | async function testProxy(proxy) { 179 | if (!proxy || typeof proxy !== 'object') { 180 | return { success: false, message: `Invalid proxy configuration` }; 181 | } 182 | 183 | try { 184 | // Create a temp fetch request through the proxy to check if it works 185 | // In a real implementation, this would need to be adapted depending on browser capabilities 186 | const controller = new AbortController(); 187 | const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout 188 | 189 | // For testing, we're just checking if we can connect to the proxy 190 | const response = await fetch('https://www.google.com', { 191 | signal: controller.signal, 192 | // We're simulating a proxy connection here 193 | // In a real extension, the proxy should already be configured via chrome.proxy API 194 | }); 195 | 196 | clearTimeout(timeoutId); 197 | 198 | if (response.ok) { 199 | return { success: true, message: 'Proxy is working' }; 200 | } else { 201 | return { success: false, message: `Proxy test failed: ${response.status} ${response.statusText}` }; 202 | } 203 | } catch (error) { 204 | return { success: false, message: `Proxy test failed: ${error.message}` }; 205 | } 206 | } 207 | 208 | // Function to check if a URL matches a scope pattern 209 | function isUrlInScope(url, scopePatterns) { 210 | try { 211 | // Convert URL to URL object 212 | const urlObj = new URL(url); 213 | const hostname = urlObj.hostname; 214 | 215 | log(`Checking if ${hostname} matches any patterns`); 216 | 217 | // Check each pattern 218 | return scopePatterns.some(pattern => { 219 | // Handle wildcard patterns 220 | if (pattern.includes('*')) { 221 | const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$'); 222 | const matches = regex.test(hostname); 223 | log(`Pattern: ${pattern} -> ${matches ? 'MATCH' : 'no match'}`); 224 | return matches; 225 | } 226 | // Handle exact matches 227 | const matches = hostname === pattern; 228 | log(`Pattern: ${pattern} -> ${matches ? 'MATCH' : 'no match'}`); 229 | return matches; 230 | }); 231 | } catch (e) { 232 | console.error('Error checking scope:', e); 233 | return false; 234 | } 235 | } 236 | 237 | // Function to check if a URL is in scope of any program 238 | async function checkUrlScope(url) { 239 | if (!url) { 240 | log('No URL provided for scope check'); 241 | return { inScope: false, programs: [] }; 242 | } 243 | 244 | log(`Checking scope for URL: ${url}`); 245 | 246 | try { 247 | const data = await chrome.storage.local.get(['programs']); 248 | const programs = data.programs || []; 249 | log(`Found ${programs.length} programs to check against`); 250 | 251 | const inScopePrograms = []; 252 | 253 | for (const program of programs) { 254 | // Skip disabled programs 255 | if (program.enabled === false) { 256 | log(`Skipping disabled program: ${program.name}`); 257 | continue; 258 | } 259 | 260 | if (program.scope) { 261 | // Split the scope string into an array of patterns 262 | const scopePatterns = program.scope.split('\n') 263 | .map(pattern => pattern.trim()) 264 | .filter(Boolean); 265 | 266 | log(`Checking program: ${program.name} with ${scopePatterns.length} patterns`); 267 | 268 | if (isUrlInScope(url, scopePatterns)) { 269 | log(`URL is in scope for program: ${program.name}`); 270 | inScopePrograms.push({ 271 | id: program.id, 272 | name: program.name 273 | }); 274 | } 275 | } else { 276 | log(`Program ${program.name} has no scope defined`); 277 | } 278 | } 279 | 280 | const result = { 281 | inScope: inScopePrograms.length > 0, 282 | programs: inScopePrograms 283 | }; 284 | 285 | log(`Scope check result: ${result.inScope ? 'IN SCOPE' : 'OUT OF SCOPE'} for ${inScopePrograms.length} programs`); 286 | return result; 287 | } catch (e) { 288 | console.error('Error in checkUrlScope:', e); 289 | return { inScope: false, programs: [] }; 290 | } 291 | } 292 | 293 | // Function to update the extension badge for a tab 294 | async function updateBadge(tabId, url) { 295 | if (!url || !url.startsWith('http')) return; 296 | 297 | try { 298 | const scopeResult = await checkUrlScope(url); 299 | 300 | if (scopeResult.inScope) { 301 | // In scope - set green badge with ✓ 302 | chrome.action.setBadgeBackgroundColor({ tabId, color: '#10b981' }); 303 | chrome.action.setBadgeText({ tabId, color: '#FFFFFF', text: '✓' }); 304 | 305 | // Set title to show program names 306 | if (scopeResult.programs && scopeResult.programs.length > 0) { 307 | const programNames = scopeResult.programs.map(p => p.name).join(', '); 308 | chrome.action.setTitle({ tabId, title: `In scope for: ${programNames}` }); 309 | } else { 310 | chrome.action.setTitle({ tabId, title: 'ProxSec: In scope' }); 311 | } 312 | } else { 313 | // Out of scope - set red badge with ✗ 314 | chrome.action.setBadgeBackgroundColor({ tabId, color: '#ef4444' }); 315 | chrome.action.setBadgeText({ tabId, color: '#FFFFFF', text: '✗' }); 316 | chrome.action.setTitle({ tabId, title: 'ProxSec: Out of scope' }); 317 | } 318 | 319 | log(`Badge updated for tab ${tabId}: ${scopeResult.inScope ? 'In scope' : 'Out of scope'}`); 320 | } catch (error) { 321 | log(`Error updating badge: ${error.message}`); 322 | 323 | // Reset badge on error 324 | chrome.action.setBadgeText({ tabId, text: '' }); 325 | } 326 | } 327 | 328 | // Update badges on all tabs when programs change 329 | function updateAllTabBadges() { 330 | log('Updating badges for all tabs'); 331 | 332 | chrome.tabs.query({ url: '' }, (tabs) => { 333 | for (const tab of tabs) { 334 | if (tab.url && tab.url.startsWith('http')) { 335 | updateBadge(tab.id, tab.url); 336 | updateTabIndicator(tab.id, tab.url); 337 | } 338 | } 339 | }); 340 | } 341 | 342 | // Listen for tab changes and URL updates 343 | chrome.tabs.onActivated.addListener(activeInfo => { 344 | updateBadge(activeInfo.tabId); 345 | }); 346 | 347 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 348 | if (changeInfo.url) { 349 | updateBadge(tabId, changeInfo.url); 350 | } 351 | }); 352 | 353 | // Listen for messages from popup or content scripts with better debugging 354 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 355 | log(`Received message: ${JSON.stringify(request)}`); 356 | 357 | if (request.action === 'setActiveProxy') { 358 | setActiveProxy(request.proxy) 359 | .then(sendResponse) 360 | .catch(error => sendResponse({ success: false, message: error.message })); 361 | return true; // Keep the channel open for async response 362 | } 363 | 364 | if (request.action === 'clearProxy') { 365 | clearProxySettings() 366 | .then(sendResponse) 367 | .catch(error => sendResponse({ success: false, message: error.message })); 368 | return true; 369 | } 370 | 371 | if (request.action === 'testProxy') { 372 | testProxy(request.proxy) 373 | .then(sendResponse) 374 | .catch(error => sendResponse({ success: false, message: error.message })); 375 | return true; 376 | } 377 | 378 | if (request.action === 'checkScope') { 379 | log(`Processing checkScope request for URL: ${request.url}`); 380 | 381 | checkUrlScope(request.url) 382 | .then(result => { 383 | log(`Sending scope check response: ${JSON.stringify(result)}`); 384 | sendResponse(result); 385 | }) 386 | .catch(error => { 387 | log(`Error in checkScope request: ${error.message}`); 388 | sendResponse({ inScope: false, programs: [], error: error.message }); 389 | }); 390 | 391 | return true; // Keep channel open for async response 392 | } 393 | 394 | if (request.action === 'openPopup') { 395 | chrome.action.openPopup(); 396 | sendResponse({ success: true }); 397 | return true; 398 | } 399 | }); 400 | 401 | // Update tabs when they change 402 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 403 | // Wait for the tab to be fully loaded 404 | if (changeInfo.status === 'complete' && tab.url) { 405 | log(`Tab updated with URL: ${tab.url}`); 406 | 407 | // Update badge 408 | updateBadge(tabId, tab.url); 409 | 410 | // Update content script 411 | checkUrlScope(tab.url).then(scopeResult => { 412 | log(`Sending scope update to tab ${tabId}: ${JSON.stringify(scopeResult)}`); 413 | 414 | // Send scope result to content script 415 | chrome.tabs.sendMessage(tabId, { 416 | action: 'updateScope', 417 | ...scopeResult 418 | }).catch(error => { 419 | // It's normal for this to fail if the content script hasn't loaded yet 420 | log(`Could not send scope update to tab ${tabId}: ${error.message}`); 421 | }); 422 | }); 423 | } 424 | }); 425 | 426 | // Function to forcibly inject the indicator using the scripting API 427 | async function injectIndicator(tabId) { 428 | log(`Forcibly injecting indicator into tab ${tabId}`); 429 | 430 | try { 431 | // Check if we have scripting permissions 432 | await chrome.scripting.executeScript({ 433 | target: { tabId }, 434 | func: () => { 435 | // Only add if not already present 436 | if (!document.getElementById('proxsec-indicator-forced')) { 437 | console.log('[ProxSec] Force-injecting indicator'); 438 | 439 | // Create the indicator 440 | const indicator = document.createElement('div'); 441 | indicator.id = 'proxsec-indicator-forced'; 442 | indicator.style.cssText = ` 443 | position: fixed; 444 | left: 20px; 445 | top: 20px; 446 | width: 30px; 447 | height: 30px; 448 | border-radius: 50%; 449 | background-color: ${result.inScope ? '#10b981' : '#ef4444'}; 450 | border: 3px solid white; 451 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); 452 | z-index: 2147483647; 453 | display: flex; 454 | align-items: center; 455 | justify-content: center; 456 | cursor: pointer; 457 | font-family: sans-serif; 458 | font-size: 12px; 459 | color: white; 460 | font-weight: bold; 461 | `; 462 | 463 | // Add shield icon inside indicator instead of 'P' text 464 | indicator.innerHTML = ` 465 | 466 | 467 | 468 | `; 469 | 470 | // Add tooltip 471 | const tooltip = document.createElement('div'); 472 | tooltip.style.cssText = ` 473 | position: absolute; 474 | top: 40px; 475 | left: 0; 476 | background-color: #1f2937; 477 | color: white; 478 | padding: 8px 12px; 479 | border-radius: 6px; 480 | font-size: 13px; 481 | white-space: nowrap; 482 | opacity: 0; 483 | transition: opacity 0.3s ease; 484 | pointer-events: none; 485 | box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); 486 | min-width: 120px; 487 | text-align: center; 488 | z-index: 2147483647; 489 | `; 490 | tooltip.textContent = 'ProxSec Status'; 491 | 492 | indicator.appendChild(tooltip); 493 | 494 | // Show tooltip on hover 495 | indicator.addEventListener('mouseenter', () => { 496 | tooltip.style.opacity = '1'; 497 | }); 498 | 499 | indicator.addEventListener('mouseleave', () => { 500 | tooltip.style.opacity = '0'; 501 | }); 502 | 503 | // Add to document 504 | document.body.appendChild(indicator); 505 | 506 | // Listen for messages from content script 507 | window.addEventListener('message', (event) => { 508 | if (event.data && event.data.type === 'proxsec-scope-update') { 509 | const indicator = document.getElementById('proxsec-indicator-forced'); 510 | if (indicator) { 511 | indicator.style.backgroundColor = event.data.inScope ? '#10b981' : '#ef4444'; 512 | 513 | const tooltip = indicator.querySelector('div'); 514 | if (tooltip) { 515 | if (event.data.inScope) { 516 | const programNames = event.data.programs.map(p => p.name).join(', '); 517 | tooltip.textContent = programNames ? `In scope for: ${programNames}` : 'In scope'; 518 | } else { 519 | tooltip.textContent = 'Out of scope'; 520 | } 521 | } 522 | } 523 | } 524 | }); 525 | 526 | // Send result to window 527 | window.postMessage({ 528 | type: 'proxsec-scope-update', 529 | inScope: result.inScope, 530 | programs: result.programs || [] 531 | }, '*'); 532 | } 533 | } 534 | }); 535 | 536 | return true; 537 | } catch (error) { 538 | log(`Error injecting indicator: ${error.message}`); 539 | return false; 540 | } 541 | } 542 | 543 | // Update indicator on tabs with scope information 544 | async function updateTabIndicator(tabId, url) { 545 | if (!url || !url.startsWith('http')) return; 546 | 547 | try { 548 | // Get scope information 549 | const scopeResult = await checkUrlScope(url); 550 | 551 | // Try to update with standard messaging first 552 | try { 553 | await chrome.tabs.sendMessage(tabId, { 554 | action: 'updateScope', 555 | ...scopeResult 556 | }); 557 | log(`Sent scope update via messaging to tab ${tabId}`); 558 | } catch (e) { 559 | log(`Messaging failed for tab ${tabId}, trying scripting API injection`); 560 | 561 | // If messaging fails, try to inject the indicator 562 | const injected = await injectIndicator(tabId); 563 | 564 | if (injected) { 565 | // Wait a moment for the injection to complete 566 | setTimeout(async () => { 567 | // Send the scope information via a page message 568 | await chrome.scripting.executeScript({ 569 | target: { tabId }, 570 | func: (scopeData) => { 571 | window.postMessage({ 572 | type: 'proxsec-scope-update', 573 | inScope: scopeData.inScope, 574 | programs: scopeData.programs.map(p => p.name) 575 | }, '*'); 576 | }, 577 | args: [scopeResult] 578 | }); 579 | log(`Sent scope update via scripting API to tab ${tabId}`); 580 | }, 500); 581 | } 582 | } 583 | } catch (error) { 584 | log(`Error updating tab indicator: ${error.message}`); 585 | } 586 | } 587 | 588 | // Listen for tabs to update 589 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 590 | // Wait for the tab to be fully loaded 591 | if (changeInfo.status === 'complete' && tab.url) { 592 | log(`Tab ${tabId} completed loading: ${tab.url}`); 593 | 594 | // Update badge 595 | updateBadge(tabId, tab.url); 596 | 597 | // Update indicator with scope 598 | updateTabIndicator(tabId, tab.url); 599 | } 600 | }); 601 | 602 | // Listen for storage changes to update badges 603 | chrome.storage.onChanged.addListener((changes, namespace) => { 604 | if (namespace === 'local' && changes.programs) { 605 | log('Programs changed, updating all badges'); 606 | updateAllTabBadges(); 607 | } 608 | }); -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | // ProxSec Content Script 2 | // Injects a scope indicator into the page 3 | 4 | // Logging helper 5 | function log(message) { 6 | console.log(`[ProxSec] ${message}`); 7 | } 8 | 9 | log('Content script loaded'); 10 | 11 | // Create the indicator 12 | function createIndicator() { 13 | log('Creating indicator'); 14 | 15 | // Check if indicator already exists 16 | if (document.getElementById('proxsec-indicator')) { 17 | log('Indicator already exists'); 18 | return document.getElementById('proxsec-indicator'); 19 | } 20 | 21 | try { 22 | // Create the indicator element 23 | const indicator = document.createElement('div'); 24 | indicator.id = 'proxsec-indicator'; 25 | indicator.style.cssText = ` 26 | position: fixed; 27 | left: 20px; 28 | top: 20px; 29 | width: 30px; 30 | height: 30px; 31 | border-radius: 50%; 32 | background-color: #6b7280; 33 | border: 3px solid white; 34 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); 35 | z-index: 2147483647; 36 | display: flex; 37 | align-items: center; 38 | justify-content: center; 39 | cursor: pointer; 40 | opacity: 0.9; 41 | transition: all 0.3s ease; 42 | font-family: sans-serif; 43 | font-size: 12px; 44 | color: white; 45 | font-weight: bold; 46 | `; 47 | 48 | // Add shield icon inside the indicator instead of "P" text 49 | indicator.innerHTML = ` 50 | 51 | 52 | 53 | `; 54 | 55 | // Add hover effect 56 | indicator.addEventListener('mouseenter', () => { 57 | indicator.style.opacity = '1'; 58 | indicator.style.transform = 'scale(1.1)'; 59 | }); 60 | 61 | indicator.addEventListener('mouseleave', () => { 62 | indicator.style.opacity = '0.9'; 63 | indicator.style.transform = 'scale(1)'; 64 | }); 65 | 66 | // Add tooltip 67 | const tooltip = document.createElement('div'); 68 | tooltip.style.cssText = ` 69 | position: absolute; 70 | top: 40px; 71 | left: 0; 72 | background-color: #1f2937; 73 | color: white; 74 | padding: 8px 12px; 75 | border-radius: 6px; 76 | font-size: 13px; 77 | white-space: nowrap; 78 | opacity: 0; 79 | transition: opacity 0.3s ease; 80 | pointer-events: none; 81 | box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); 82 | min-width: 120px; 83 | text-align: center; 84 | z-index: 2147483647; 85 | `; 86 | tooltip.textContent = 'ProxSec: Checking...'; 87 | 88 | indicator.appendChild(tooltip); 89 | 90 | // Show tooltip on hover 91 | indicator.addEventListener('mouseenter', () => { 92 | tooltip.style.opacity = '1'; 93 | }); 94 | 95 | indicator.addEventListener('mouseleave', () => { 96 | tooltip.style.opacity = '0'; 97 | }); 98 | 99 | // Add click event to open popup 100 | indicator.addEventListener('click', () => { 101 | chrome.runtime.sendMessage({ action: 'openPopup' }); 102 | }); 103 | 104 | // Add to document 105 | document.body.appendChild(indicator); 106 | log('Indicator added to document'); 107 | 108 | return indicator; 109 | } catch (error) { 110 | log(`Error creating indicator: ${error.message}`); 111 | return null; 112 | } 113 | } 114 | 115 | // Update indicator with scope information 116 | function updateIndicator(inScope, programs = []) { 117 | log(`Updating indicator: inScope=${inScope}, programs=${JSON.stringify(programs)}`); 118 | 119 | // Find the indicator 120 | const indicator = document.getElementById('proxsec-indicator') || 121 | document.getElementById('proxsec-indicator-forced'); 122 | 123 | if (!indicator) { 124 | log('Indicator not found, creating it'); 125 | const newIndicator = createIndicator(); 126 | if (!newIndicator) return; 127 | 128 | // Give it time to render before updating 129 | setTimeout(() => updateIndicator(inScope, programs), 100); 130 | return; 131 | } 132 | 133 | const tooltip = indicator.querySelector('div'); 134 | 135 | if (inScope && programs && programs.length > 0) { 136 | indicator.style.backgroundColor = '#10b981'; // Green 137 | const programNames = Array.isArray(programs) 138 | ? programs.map(p => typeof p === 'string' ? p : p.name).join(', ') 139 | : 'Unknown program'; 140 | tooltip.textContent = `In scope for: ${programNames}`; 141 | } else { 142 | indicator.style.backgroundColor = '#ef4444'; // Red 143 | tooltip.textContent = 'Out of scope'; 144 | } 145 | 146 | log('Indicator updated successfully'); 147 | } 148 | 149 | // Initialize the indicator 150 | function init() { 151 | log('Initializing content script'); 152 | 153 | // Create indicator (it will start in a neutral color) 154 | createIndicator(); 155 | 156 | // Send message to check if this URL is in scope 157 | chrome.runtime.sendMessage( 158 | { action: 'checkScope', url: window.location.href }, 159 | (response) => { 160 | if (chrome.runtime.lastError) { 161 | log(`Error sending checkScope message: ${chrome.runtime.lastError.message}`); 162 | return; 163 | } 164 | 165 | if (response) { 166 | log(`Received scope response: ${JSON.stringify(response)}`); 167 | updateIndicator(response.inScope, response.programs); 168 | } 169 | } 170 | ); 171 | 172 | // Periodically check if indicator still exists (pages might remove it) 173 | setInterval(() => { 174 | if (!document.getElementById('proxsec-indicator') && 175 | !document.getElementById('proxsec-indicator-forced')) { 176 | log('Indicator removed, recreating'); 177 | createIndicator(); 178 | } 179 | }, 5000); 180 | } 181 | 182 | // Listen for messages from the background script 183 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 184 | try { 185 | log(`Received message: ${JSON.stringify(message)}`); 186 | 187 | if (message && message.action === 'updateScope') { 188 | updateIndicator( 189 | message.inScope === true, 190 | Array.isArray(message.programs) ? message.programs : [] 191 | ); 192 | sendResponse({ success: true }); 193 | return true; 194 | } 195 | } catch (error) { 196 | log(`Error processing message: ${error.message}`); 197 | sendResponse({ success: false, error: error.message }); 198 | } 199 | 200 | return false; 201 | }); 202 | 203 | // Also listen for window messages (from scripting API) 204 | window.addEventListener('message', (event) => { 205 | try { 206 | if (event.data && event.data.type === 'proxsec-scope-update') { 207 | log(`Received window message: ${JSON.stringify(event.data)}`); 208 | updateIndicator( 209 | event.data.inScope === true, 210 | Array.isArray(event.data.programs) ? event.data.programs : [] 211 | ); 212 | } 213 | } catch (error) { 214 | log(`Error processing window message: ${error.message}`); 215 | } 216 | }, false); 217 | 218 | // Start initialization when the page is fully loaded 219 | if (document.readyState === 'complete') { 220 | init(); 221 | } else { 222 | window.addEventListener('load', init); 223 | } -------------------------------------------------------------------------------- /icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aacle/ProxSec/6b513bd67455d7818d9c79aa96dbb1dec3eecd0b/icons/icon128.png -------------------------------------------------------------------------------- /icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aacle/ProxSec/6b513bd67455d7818d9c79aa96dbb1dec3eecd0b/icons/icon16.png -------------------------------------------------------------------------------- /icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aacle/ProxSec/6b513bd67455d7818d9c79aa96dbb1dec3eecd0b/icons/icon32.png -------------------------------------------------------------------------------- /icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aacle/ProxSec/6b513bd67455d7818d9c79aa96dbb1dec3eecd0b/icons/icon48.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "ProxSec", 4 | "version": "1.0.0", 5 | "description": "An extension for infosec community", 6 | "permissions": [ 7 | "webRequest", 8 | "proxy", 9 | "storage", 10 | "tabs", 11 | "activeTab", 12 | "scripting" 13 | ], 14 | "host_permissions": [ 15 | "" 16 | ], 17 | "action": { 18 | "default_popup": "popup.html", 19 | "default_icon": { 20 | "16": "icons/icon16.png", 21 | "32": "icons/icon32.png", 22 | "48": "icons/icon48.png", 23 | "128": "icons/icon128.png" 24 | } 25 | }, 26 | "background": { 27 | "service_worker": "background.js" 28 | }, 29 | "content_scripts": [ 30 | { 31 | "matches": [""], 32 | "js": ["content.js"], 33 | "run_at": "document_end", 34 | "all_frames": false 35 | } 36 | ], 37 | "web_accessible_resources": [ 38 | { 39 | "resources": ["icons/*.png"], 40 | "matches": [""] 41 | } 42 | ], 43 | "icons": { 44 | "16": "icons/icon16.png", 45 | "32": "icons/icon32.png", 46 | "48": "icons/icon48.png", 47 | "128": "icons/icon128.png" 48 | } 49 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vulncure", 3 | "version": "1.0.0", 4 | "description": "A browser extension for bug bounty hunters and penetration testers", 5 | "scripts": { 6 | "generate-icons": "node scripts/generate-icons.js", 7 | "build": "npm run generate-icons", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [ 11 | "bug-bounty", 12 | "security", 13 | "penetration-testing", 14 | "proxy", 15 | "browser-extension" 16 | ], 17 | "author": "", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "fs-extra": "^10.1.0", 21 | "sharp": "^0.30.7" 22 | } 23 | } -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ProxSec 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 | 23 |
24 | 27 | 30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 |
38 |
39 | 40 |
41 | 42 | Ready 43 |
44 | 45 |
46 | 47 |
48 |
49 | 50 |
51 |
52 |

Active Proxy

53 |
54 |
55 |
56 |
57 | 58 |

No active proxy

59 |
60 |
61 |
62 | 63 |
64 |
65 |
66 | 67 | 68 |
69 |
70 |

Programs

71 |
72 |
73 |
74 |
75 |
0
76 |
Programs
77 |
78 |
79 |
0
80 |
In-Scope
81 |
82 |
83 |
84 |
85 | Out of Scope 86 |
87 |
88 |
89 | 90 |
91 |
92 |
93 |
94 |
95 | 96 | 97 |
98 |
99 |

Manage Proxies

100 | 101 |
102 | 103 |
104 | 105 | 106 | 154 |
155 | 156 | 157 |
158 |
159 |

Bug Bounty Programs

160 | 161 |
162 | 163 |
164 | 165 | 166 | 198 | 199 | 200 |
201 |
202 | 203 | 204 |
205 |

About ProxSec

206 |

207 | ProxSec is an open-source browser extension designed for bug bounty hunters and security professionals. 208 | It provides tools to enhance your workflow and optimize your security testing processes. 209 |

210 | 211 |
212 |

Security Information

213 |

214 | Your data stays with you: ProxSec doesn't send any data to external servers. 215 | All your configurations and settings are stored locally in your browser. 216 |

217 |

218 | Encrypted storage: All sensitive data stored by ProxSec is encrypted using your browser's built-in storage encryption. 219 |

220 |
221 | 222 |
223 |

Features

224 |
    225 |
  • Proxy management with support for HTTP/HTTPS
  • 226 |
  • Bug bounty program management
  • 227 |
  • Scope awareness for staying within program boundaries
  • 228 |
  • Secure credential storage for authenticated proxies
  • 229 |
230 |
231 | 232 |
233 |

Coming Soon

234 |
    235 |
  • Note-taking for vulnerabilities
  • 236 |
  • Customizable recon tools
  • 237 |
  • Vulnerability report templates
  • 238 |
239 |
240 | 241 |
242 |
243 | 244 | 245 | 246 | MIT License 247 |
248 |
249 | 250 | 251 | 252 | 253 | 254 | Open Source 255 |
256 |
257 |
258 |
259 | 260 |
261 |
Version 1.0.0
262 |
263 |
264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | // VulnCure Popup Script 2 | 3 | document.addEventListener('DOMContentLoaded', () => { 4 | // DOM Elements 5 | const addProxyButton = document.getElementById('add-proxy-btn'); 6 | const cancelProxyButton = document.getElementById('cancel-proxy-btn'); 7 | const proxyList = document.getElementById('proxy-list'); 8 | const proxyForm = document.getElementById('proxy-form'); 9 | const proxyFormContainer = document.getElementById('proxy-form-container'); 10 | const statusBar = document.getElementById('status-bar'); 11 | const formTitle = document.getElementById('form-title'); 12 | const tabs = document.querySelectorAll('.tab'); 13 | const tabContents = document.querySelectorAll('.tab-content'); 14 | 15 | // Program Management DOM Elements 16 | const addProgramButton = document.getElementById('add-program-btn'); 17 | const cancelProgramButton = document.getElementById('cancel-program-btn'); 18 | const programList = document.getElementById('program-list'); 19 | const programForm = document.getElementById('program-form'); 20 | const programFormContainer = document.getElementById('program-form-container'); 21 | const programFormTitle = document.getElementById('program-form-title'); 22 | const scopeStatusContainer = document.getElementById('scope-status-container'); 23 | 24 | // Program Form Fields 25 | const programNameInput = document.getElementById('program-name'); 26 | const programUrlInput = document.getElementById('program-url'); 27 | const programScopeInput = document.getElementById('program-scope'); 28 | const programNotesInput = document.getElementById('program-notes'); 29 | 30 | // Form fields 31 | const nameInput = document.getElementById('proxy-name'); 32 | const protocolSelect = document.getElementById('proxy-protocol'); 33 | const ipInput = document.getElementById('proxy-ip'); 34 | const portInput = document.getElementById('proxy-port'); 35 | const usernameInput = document.getElementById('proxy-username'); 36 | const passwordInput = document.getElementById('proxy-password'); 37 | 38 | // Icons 39 | const ICONS = { 40 | active: ``, 41 | inactive: ``, 42 | error: ``, 43 | edit: ``, 44 | delete: ``, 45 | test: ``, 46 | add: ``, 47 | cancel: ``, 48 | success: ``, 49 | info: ``, 50 | warning: ``, 51 | scope: ``, 52 | notes: `` 53 | }; 54 | 55 | // Current proxy being edited 56 | let currentProxyId = null; 57 | let proxies = []; 58 | let activeProxyId = null; 59 | 60 | // Program Management State 61 | let currentProgramId = null; 62 | let programs = []; 63 | let currentUrl = ''; 64 | 65 | // Initial load 66 | loadProxies(); 67 | loadPrograms(); 68 | checkCurrentUrl(); 69 | updateActiveProxyInfo(); 70 | 71 | // Update icon for add button 72 | addProxyButton.innerHTML = `Add Proxy`; 73 | cancelProxyButton.innerHTML = `Cancel`; 74 | if (addProgramButton) { 75 | const addIcon = ``; 76 | addProgramButton.innerHTML = `${addIcon} Add Program`; 77 | } 78 | if (cancelProgramButton) { 79 | cancelProgramButton.innerHTML = `Cancel`; 80 | } 81 | 82 | // Event Listeners 83 | addProxyButton.addEventListener('click', showAddProxyForm); 84 | cancelProxyButton.addEventListener('click', hideProxyForm); 85 | proxyForm.addEventListener('submit', saveProxy); 86 | 87 | // Program Management Event Listeners 88 | if (addProgramButton) { 89 | addProgramButton.addEventListener('click', showAddProgramForm); 90 | } 91 | 92 | if (cancelProgramButton) { 93 | cancelProgramButton.addEventListener('click', hideProgramForm); 94 | } 95 | 96 | if (programForm) { 97 | programForm.addEventListener('submit', saveProgram); 98 | } 99 | 100 | // Tab switching 101 | tabs.forEach(tab => { 102 | tab.addEventListener('click', () => { 103 | const tabName = tab.getAttribute('data-tab'); 104 | switchTab(tabName); 105 | }); 106 | }); 107 | 108 | // Dashboard DOM Elements and actions 109 | const dashboardAddProxyBtn = document.getElementById('dashboard-add-proxy'); 110 | const dashboardAddProgramBtn = document.getElementById('dashboard-add-program'); 111 | const checkCurrentUrlBtn = document.getElementById('check-current-url'); 112 | const toggleProxyBtn = document.getElementById('toggle-proxy'); 113 | const refreshStatsBtn = document.getElementById('refresh-stats'); 114 | const viewSettingsBtn = document.getElementById('view-settings'); 115 | 116 | // Dashboard stats elements 117 | const totalProxiesElement = document.getElementById('total-proxies'); 118 | const activeProxiesElement = document.getElementById('active-proxies'); 119 | const proxyChartBar = document.getElementById('proxy-active-bar'); 120 | const totalProgramsElement = document.getElementById('total-programs'); 121 | const inScopeCountElement = document.getElementById('in-scope-count'); 122 | const scopeStatusElement = document.getElementById('scope-status'); 123 | 124 | // Event Listeners 125 | // Dashboard Buttons 126 | if (dashboardAddProxyBtn) { 127 | dashboardAddProxyBtn.addEventListener('click', () => { 128 | showAddProxyForm(); 129 | // Switch to proxies tab after clicking 130 | switchTab('proxies'); 131 | }); 132 | } 133 | 134 | if (dashboardAddProgramBtn) { 135 | dashboardAddProgramBtn.addEventListener('click', () => { 136 | showAddProgramForm(); 137 | // Switch to programs tab after clicking 138 | switchTab('programs'); 139 | }); 140 | } 141 | 142 | if (checkCurrentUrlBtn) { 143 | checkCurrentUrlBtn.addEventListener('click', () => { 144 | checkCurrentUrl(); 145 | }); 146 | } 147 | 148 | if (toggleProxyBtn) { 149 | toggleProxyBtn.addEventListener('click', () => { 150 | toggleActiveProxy(); 151 | }); 152 | } 153 | 154 | // Quick proxy toggle in header 155 | const quickProxyToggle = document.getElementById('quick-proxy-toggle'); 156 | if (quickProxyToggle) { 157 | quickProxyToggle.addEventListener('click', () => { 158 | toggleActiveProxy(); 159 | }); 160 | } 161 | 162 | if (refreshStatsBtn) { 163 | refreshStatsBtn.addEventListener('click', () => { 164 | updateDashboardStats(); 165 | }); 166 | } 167 | 168 | if (viewSettingsBtn) { 169 | viewSettingsBtn.addEventListener('click', () => { 170 | // TODO: Implement settings view 171 | alert('Settings will be implemented in a future update!'); 172 | }); 173 | } 174 | 175 | // Initialize dashboard stats when the popup is opened 176 | updateDashboardStats(); 177 | 178 | // Update scope status when tab changes or proxy changes 179 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 180 | if (tabs.length > 0 && tabs[0].url) { 181 | const url = tabs[0].url; 182 | 183 | // First check if we have cached scope status 184 | chrome.storage.local.get(['currentUrl', 'currentScopeStatus'], (data) => { 185 | // If URL matches cached URL, use cached status 186 | if (data.currentUrl === url && data.currentScopeStatus) { 187 | updateScopeStatus(); 188 | updateScopeStatusIndicator( 189 | data.currentScopeStatus.inScope, 190 | data.currentScopeStatus.programs 191 | ); 192 | return; 193 | } 194 | 195 | // Otherwise check URL scope fresh 196 | chrome.runtime.sendMessage({ action: 'checkScope', url }, (result) => { 197 | if (chrome.runtime.lastError) { 198 | console.error('Error checking scope:', chrome.runtime.lastError); 199 | return; 200 | } 201 | 202 | if (result) { 203 | chrome.storage.local.set({ 204 | currentUrl: url, 205 | currentScopeStatus: result 206 | }); 207 | 208 | updateScopeStatus(); 209 | updateScopeStatusIndicator(result.inScope, result.programs); 210 | } 211 | }); 212 | }); 213 | } 214 | }); 215 | 216 | // Functions 217 | function loadProxies() { 218 | chrome.storage.local.get(['proxies', 'activeProxy'], (data) => { 219 | proxies = data.proxies || []; 220 | activeProxyId = data.activeProxy; 221 | 222 | // Check for any proxies in the old format and migrate them 223 | let needsUpdate = false; 224 | proxies = proxies.map(proxy => { 225 | if (proxy.url && (!proxy.protocol || !proxy.ip || !proxy.port)) { 226 | needsUpdate = true; 227 | // Convert from old format to new format 228 | try { 229 | const urlObj = new URL(proxy.url); 230 | return { 231 | ...proxy, 232 | protocol: urlObj.protocol.replace(':', ''), 233 | ip: urlObj.hostname, 234 | port: parseInt(urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80)) 235 | }; 236 | } catch (e) { 237 | console.error('Error converting proxy format:', e); 238 | return proxy; 239 | } 240 | } 241 | return proxy; 242 | }); 243 | 244 | // Save the updated proxies back to storage if we made any changes 245 | if (needsUpdate) { 246 | chrome.storage.local.set({ proxies }, () => { 247 | renderProxyList(); 248 | }); 249 | } else { 250 | renderProxyList(); 251 | } 252 | }); 253 | } 254 | 255 | function renderProxyList() { 256 | // Clear existing list 257 | proxyList.innerHTML = ''; 258 | 259 | if (proxies.length === 0) { 260 | // Show empty state 261 | proxyList.innerHTML = ` 262 |
263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 |

No proxies configured yet

274 |

Add your first proxy to get started

275 |
276 | `; 277 | updateActiveProxyInfo(); 278 | return; 279 | } 280 | 281 | // Add each proxy to the list 282 | proxies.forEach(proxy => { 283 | // Create proxy item 284 | const proxyItem = document.createElement('div'); 285 | proxyItem.className = 'proxy-item'; 286 | proxyItem.dataset.id = proxy.id; 287 | 288 | // Create name row with status 289 | const nameRow = document.createElement('div'); 290 | nameRow.className = 'proxy-name'; 291 | 292 | // Add name 293 | const nameSpan = document.createElement('span'); 294 | nameSpan.textContent = proxy.name; 295 | nameRow.appendChild(nameSpan); 296 | 297 | // Add status badge 298 | const statusBadge = document.createElement('span'); 299 | const status = proxy.status || 'Inactive'; 300 | let statusClass = 'status-inactive'; 301 | let statusLabel = 'Inactive'; 302 | 303 | if (status === 'Active') { 304 | statusClass = 'status-active'; 305 | statusLabel = 'Active'; 306 | } else if (status === 'Error') { 307 | statusClass = 'status-error'; 308 | statusLabel = 'Error'; 309 | } 310 | 311 | statusBadge.className = `proxy-status ${statusClass}`; 312 | statusBadge.innerHTML = `${status === 'Active' ? ICONS.active : (status === 'Error' ? ICONS.error : ICONS.inactive)} ${statusLabel}`; 313 | nameRow.appendChild(statusBadge); 314 | 315 | proxyItem.appendChild(nameRow); 316 | 317 | // Add URL details 318 | const details = document.createElement('p'); 319 | details.className = 'proxy-details'; 320 | 321 | let detailsText; 322 | if (proxy.protocol && proxy.ip && proxy.port) { 323 | detailsText = `${proxy.protocol}://${proxy.ip}:${proxy.port}`; 324 | } else if (proxy.url) { 325 | detailsText = proxy.url; 326 | } else { 327 | detailsText = 'Invalid proxy configuration'; 328 | } 329 | details.textContent = detailsText; 330 | proxyItem.appendChild(details); 331 | 332 | // Add actions (buttons and toggle switch) 333 | const actionsDiv = document.createElement('div'); 334 | actionsDiv.className = 'proxy-actions'; 335 | 336 | // Add action buttons (test, edit, delete) 337 | const actionButtons = document.createElement('div'); 338 | actionButtons.className = 'action-buttons'; 339 | 340 | // Test button 341 | const testButton = document.createElement('button'); 342 | testButton.className = 'btn-icon'; 343 | testButton.innerHTML = ICONS.test; 344 | testButton.title = 'Test proxy'; 345 | testButton.addEventListener('click', (e) => { 346 | e.stopPropagation(); 347 | testProxy(proxy.id); 348 | }); 349 | actionButtons.appendChild(testButton); 350 | 351 | // Edit button 352 | const editButton = document.createElement('button'); 353 | editButton.className = 'btn-icon'; 354 | editButton.innerHTML = ICONS.edit; 355 | editButton.title = 'Edit proxy'; 356 | editButton.addEventListener('click', (e) => { 357 | e.stopPropagation(); 358 | showEditProxyForm(proxy.id); 359 | }); 360 | actionButtons.appendChild(editButton); 361 | 362 | // Delete button 363 | const deleteButton = document.createElement('button'); 364 | deleteButton.className = 'btn-icon'; 365 | deleteButton.innerHTML = ICONS.delete; 366 | deleteButton.title = 'Delete proxy'; 367 | deleteButton.addEventListener('click', (e) => { 368 | e.stopPropagation(); 369 | deleteProxy(proxy.id); 370 | }); 371 | actionButtons.appendChild(deleteButton); 372 | 373 | actionsDiv.appendChild(actionButtons); 374 | 375 | // Add toggle switch 376 | const toggleDiv = document.createElement('div'); 377 | toggleDiv.className = 'toggle-container'; 378 | 379 | const toggleLabel = document.createElement('label'); 380 | toggleLabel.className = 'switch'; 381 | 382 | const toggleInput = document.createElement('input'); 383 | toggleInput.type = 'checkbox'; 384 | toggleInput.className = 'proxy-toggle'; 385 | toggleInput.checked = activeProxyId === proxy.id; 386 | 387 | const toggleSlider = document.createElement('span'); 388 | toggleSlider.className = 'slider round'; 389 | 390 | toggleLabel.appendChild(toggleInput); 391 | toggleLabel.appendChild(toggleSlider); 392 | 393 | const toggleText = document.createElement('span'); 394 | toggleText.className = 'toggle-label'; 395 | toggleText.textContent = activeProxyId === proxy.id ? 'Active' : 'Inactive'; 396 | 397 | toggleDiv.appendChild(toggleLabel); 398 | toggleDiv.appendChild(toggleText); 399 | actionsDiv.appendChild(toggleDiv); 400 | proxyItem.appendChild(actionsDiv); 401 | 402 | // Add event listeners 403 | toggleInput.addEventListener('change', (e) => toggleProxy(proxy.id, e.target.checked)); 404 | 405 | // Add to proxy list 406 | proxyList.appendChild(proxyItem); 407 | }); 408 | } 409 | 410 | function showAddProxyForm() { 411 | formTitle.textContent = 'Add Proxy'; 412 | nameInput.value = ''; 413 | protocolSelect.value = 'http'; 414 | ipInput.value = ''; 415 | portInput.value = ''; 416 | usernameInput.value = ''; 417 | passwordInput.value = ''; 418 | currentProxyId = null; 419 | proxyFormContainer.classList.remove('hidden'); 420 | 421 | // Add animation class 422 | proxyFormContainer.classList.add('fade-in'); 423 | setTimeout(() => { 424 | proxyFormContainer.classList.remove('fade-in'); 425 | }, 300); 426 | 427 | nameInput.focus(); 428 | } 429 | 430 | function showEditProxyForm(proxyId) { 431 | const proxy = proxies.find(p => p.id === proxyId); 432 | if (!proxy) return; 433 | 434 | formTitle.textContent = 'Edit Proxy'; 435 | nameInput.value = proxy.name; 436 | 437 | // Handle old format conversion for edit 438 | if (proxy.protocol && proxy.ip && proxy.port) { 439 | // New format 440 | protocolSelect.value = proxy.protocol; 441 | ipInput.value = proxy.ip; 442 | portInput.value = proxy.port; 443 | } else if (proxy.url) { 444 | // Try to parse old URL format 445 | try { 446 | const urlObj = new URL(proxy.url); 447 | protocolSelect.value = urlObj.protocol.replace(':', ''); 448 | ipInput.value = urlObj.hostname; 449 | portInput.value = urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80); 450 | } catch (e) { 451 | protocolSelect.value = 'http'; 452 | ipInput.value = ''; 453 | portInput.value = ''; 454 | } 455 | } else { 456 | protocolSelect.value = 'http'; 457 | ipInput.value = ''; 458 | portInput.value = ''; 459 | } 460 | 461 | usernameInput.value = proxy.username || ''; 462 | passwordInput.value = proxy.password || ''; 463 | currentProxyId = proxyId; 464 | proxyFormContainer.classList.remove('hidden'); 465 | 466 | // Add animation class 467 | proxyFormContainer.classList.add('fade-in'); 468 | setTimeout(() => { 469 | proxyFormContainer.classList.remove('fade-in'); 470 | }, 300); 471 | 472 | nameInput.focus(); 473 | } 474 | 475 | function hideProxyForm() { 476 | proxyFormContainer.classList.add('fade-out'); 477 | 478 | setTimeout(() => { 479 | proxyFormContainer.classList.add('hidden'); 480 | proxyFormContainer.classList.remove('fade-out'); 481 | }, 200); 482 | } 483 | 484 | function saveProxy(e) { 485 | e.preventDefault(); 486 | 487 | const name = nameInput.value.trim(); 488 | const protocol = protocolSelect.value; 489 | const ip = ipInput.value.trim(); 490 | const portValue = portInput.value.trim(); 491 | const port = parseInt(portValue); 492 | const username = usernameInput.value.trim(); 493 | const password = passwordInput.value.trim(); 494 | 495 | if (!name || !ip || !portValue) { 496 | showStatus('Name, IP address, and port are required', 'error'); 497 | return; 498 | } 499 | 500 | if (!isValidIP(ip)) { 501 | showStatus('Invalid IP address format', 'error'); 502 | return; 503 | } 504 | 505 | if (isNaN(port) || port < 1 || port > 65535) { 506 | showStatus('Port must be a number between 1 and 65535', 'error'); 507 | return; 508 | } 509 | 510 | if (currentProxyId) { 511 | // Edit existing proxy 512 | const index = proxies.findIndex(p => p.id === currentProxyId); 513 | if (index !== -1) { 514 | // Save in new format and remove old url property if it exists 515 | const { url, ...restOfProxy } = proxies[index]; 516 | proxies[index] = { 517 | ...restOfProxy, 518 | name, 519 | protocol, 520 | ip, 521 | port, 522 | username: username || null, 523 | password: password || null, 524 | lastUpdated: new Date().toISOString() 525 | }; 526 | } 527 | } else { 528 | // Add new proxy 529 | const newProxy = { 530 | id: generateId(), 531 | name, 532 | protocol, 533 | ip, 534 | port, 535 | username: username || null, 536 | password: password || null, 537 | status: 'Inactive', 538 | created: new Date().toISOString(), 539 | lastUpdated: new Date().toISOString() 540 | }; 541 | 542 | proxies.push(newProxy); 543 | } 544 | 545 | // Save to storage 546 | chrome.storage.local.set({ proxies }, () => { 547 | hideProxyForm(); 548 | renderProxyList(); 549 | showStatus(currentProxyId ? 'Proxy updated successfully' : 'Proxy added successfully', 'success'); 550 | }); 551 | } 552 | 553 | function deleteProxy(proxyId) { 554 | if (!confirm('Are you sure you want to delete this proxy?')) return; 555 | 556 | const index = proxies.findIndex(p => p.id === proxyId); 557 | if (index !== -1) { 558 | proxies.splice(index, 1); 559 | 560 | // If the active proxy is being deleted, clear it 561 | if (activeProxyId === proxyId) { 562 | activeProxyId = null; 563 | chrome.runtime.sendMessage({ action: 'clearProxy' }); 564 | } 565 | 566 | // Save to storage 567 | chrome.storage.local.set({ 568 | proxies, 569 | activeProxy: activeProxyId 570 | }, () => { 571 | renderProxyList(); 572 | showStatus('Proxy deleted successfully', 'success'); 573 | }); 574 | } 575 | } 576 | 577 | function testProxy(proxyId) { 578 | const proxy = proxies.find(p => p.id === proxyId); 579 | if (!proxy) return; 580 | 581 | showStatus('Testing proxy...', 'info'); 582 | 583 | // Make sure the proxy object is complete before testing 584 | if (!proxy.protocol && !proxy.ip && !proxy.port && !proxy.url) { 585 | showStatus('Invalid proxy configuration', 'error'); 586 | return; 587 | } 588 | 589 | chrome.runtime.sendMessage({ 590 | action: 'testProxy', 591 | proxy 592 | }, (response) => { 593 | if (response && response.success) { 594 | proxy.status = 'Active'; 595 | showStatus('Proxy test successful', 'success'); 596 | } else { 597 | proxy.status = 'Error'; 598 | const errorMsg = response ? response.message : 'Unknown error'; 599 | showStatus(`Proxy test failed: ${errorMsg}`, 'error'); 600 | } 601 | 602 | // Update storage 603 | chrome.storage.local.set({ proxies }, () => { 604 | renderProxyList(); 605 | }); 606 | }); 607 | } 608 | 609 | function toggleProxy(proxyId, isActive) { 610 | // Add logic to prevent toggling if a proxy is already active 611 | if (isActive && activeProxyId && activeProxyId !== proxyId) { 612 | showStatus('Another proxy is already active. Please deactivate it first.', 'warning'); 613 | return; 614 | } 615 | 616 | // Clear any existing active proxy 617 | if (isActive) { 618 | // First clear any existing proxies 619 | chrome.runtime.sendMessage({ action: 'clearProxy' }, (response) => { 620 | // Then activate the selected proxy 621 | const proxy = proxies.find(p => p.id === proxyId); 622 | if (!proxy) { 623 | showStatus('Proxy not found', 'error'); 624 | return; 625 | } 626 | 627 | chrome.runtime.sendMessage({ action: 'setActiveProxy', proxy }, (response) => { 628 | if (response && response.success) { 629 | // Set this proxy as active 630 | chrome.storage.local.set({ activeProxy: proxyId }, () => { 631 | activeProxyId = proxyId; 632 | showStatus(`Proxy "${proxy.name}" activated`, 'success'); 633 | renderProxyList(); 634 | updateActiveProxyInfo(); 635 | updateQuickToggleState(); 636 | }); 637 | } else { 638 | showStatus('Failed to activate proxy: ' + (response ? response.message : 'Unknown error'), 'error'); 639 | renderProxyList(); // Reset UI 640 | } 641 | }); 642 | }); 643 | } else { 644 | // Deactivate the proxy 645 | chrome.runtime.sendMessage({ action: 'clearProxy' }, (response) => { 646 | if (response && response.success) { 647 | chrome.storage.local.set({ activeProxy: null }, () => { 648 | activeProxyId = null; 649 | showStatus('Proxy deactivated', 'success'); 650 | renderProxyList(); 651 | updateActiveProxyInfo(); 652 | updateQuickToggleState(); 653 | }); 654 | } else { 655 | showStatus('Failed to deactivate proxy: ' + (response ? response.message : 'Unknown error'), 'error'); 656 | renderProxyList(); // Reset the UI 657 | } 658 | }); 659 | } 660 | } 661 | 662 | function showStatus(message, type = 'info') { 663 | // Get the appropriate icon 664 | let icon = ICONS.info; 665 | if (type === 'success') icon = ICONS.success; 666 | if (type === 'error') icon = ICONS.error; 667 | if (type === 'warning') icon = ICONS.warning; 668 | 669 | // Set message with icon 670 | statusBar.innerHTML = `${icon} ${message}`; 671 | statusBar.className = 'status-bar status-' + type; 672 | statusBar.style.opacity = 1; 673 | 674 | // Clear after 3 seconds 675 | setTimeout(() => { 676 | statusBar.style.opacity = 0; 677 | }, 3000); 678 | } 679 | 680 | // Helper functions 681 | function generateId() { 682 | return Date.now().toString(36) + Math.random().toString(36).substr(2); 683 | } 684 | 685 | function isValidIP(ip) { 686 | // Basic IPv4 validation 687 | const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; 688 | 689 | // Also allow hostnames 690 | const hostnameRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/; 691 | 692 | // Allow localhost 693 | if (ip === 'localhost') return true; 694 | 695 | return ipv4Regex.test(ip) || hostnameRegex.test(ip); 696 | } 697 | 698 | // Program Management Functions 699 | function loadPrograms() { 700 | chrome.storage.local.get(['programs'], (data) => { 701 | programs = data.programs || []; 702 | renderProgramList(); 703 | }); 704 | } 705 | 706 | function renderProgramList() { 707 | programList.innerHTML = ''; 708 | 709 | if (programs.length === 0) { 710 | programList.innerHTML = ` 711 |
712 | 713 | 714 | 715 | 716 | 717 |

No bug bounty programs added yet.

718 |

Click "Add Program" to get started.

719 |
720 | `; 721 | return; 722 | } 723 | 724 | programs.forEach(program => { 725 | const programItem = document.createElement('div'); 726 | programItem.className = 'program-item'; 727 | programItem.dataset.id = program.id; 728 | 729 | // Program name and status 730 | const nameRow = document.createElement('div'); 731 | nameRow.className = 'program-name'; 732 | 733 | const nameSpan = document.createElement('span'); 734 | nameSpan.textContent = program.name; 735 | nameRow.appendChild(nameSpan); 736 | 737 | // Scope status badge 738 | const scopeBadge = document.createElement('span'); 739 | 740 | // If program is disabled, show disabled status regardless of scope 741 | if (program.enabled === false) { 742 | scopeBadge.className = 'program-status status-disabled'; 743 | scopeBadge.innerHTML = `${ICONS.scope} Disabled`; 744 | } else { 745 | const isInScope = isUrlInScope(currentUrl, program.scope, program.enabled); 746 | scopeBadge.className = `program-status ${isInScope ? 'status-active' : 'status-inactive'}`; 747 | scopeBadge.innerHTML = `${ICONS.scope} ${isInScope ? 'In Scope' : 'Out of Scope'}`; 748 | } 749 | 750 | nameRow.appendChild(scopeBadge); 751 | 752 | programItem.appendChild(nameRow); 753 | 754 | // Program URL 755 | const urlDetails = document.createElement('p'); 756 | urlDetails.className = 'program-details'; 757 | urlDetails.textContent = program.url; 758 | programItem.appendChild(urlDetails); 759 | 760 | // Program actions 761 | const actionsDiv = document.createElement('div'); 762 | actionsDiv.className = 'program-actions'; 763 | 764 | // Toggle switch for enabled/disabled state 765 | const toggleDiv = document.createElement('div'); 766 | toggleDiv.className = 'toggle-container'; 767 | 768 | const toggleLabel = document.createElement('label'); 769 | toggleLabel.className = 'switch'; 770 | 771 | const toggleInput = document.createElement('input'); 772 | toggleInput.type = 'checkbox'; 773 | toggleInput.checked = program.enabled !== false; // Default to true if not set 774 | toggleInput.addEventListener('change', (e) => { 775 | e.stopPropagation(); 776 | toggleProgram(program.id, e.target.checked); 777 | }); 778 | 779 | const toggleSlider = document.createElement('span'); 780 | toggleSlider.className = 'slider round'; 781 | 782 | toggleLabel.appendChild(toggleInput); 783 | toggleLabel.appendChild(toggleSlider); 784 | 785 | const toggleText = document.createElement('span'); 786 | toggleText.className = 'toggle-label'; 787 | toggleText.textContent = program.enabled !== false ? 'Enabled' : 'Disabled'; 788 | 789 | toggleDiv.appendChild(toggleLabel); 790 | toggleDiv.appendChild(toggleText); 791 | 792 | actionsDiv.appendChild(toggleDiv); 793 | 794 | // Action buttons 795 | const actionButtons = document.createElement('div'); 796 | actionButtons.className = 'action-buttons'; 797 | 798 | // Edit button 799 | const editButton = document.createElement('button'); 800 | editButton.className = 'btn-icon'; 801 | editButton.innerHTML = ICONS.edit; 802 | editButton.title = 'Edit program'; 803 | editButton.addEventListener('click', (e) => { 804 | e.stopPropagation(); 805 | showEditProgramForm(program.id); 806 | }); 807 | actionButtons.appendChild(editButton); 808 | 809 | // Delete button 810 | const deleteButton = document.createElement('button'); 811 | deleteButton.className = 'btn-icon'; 812 | deleteButton.innerHTML = ICONS.delete; 813 | deleteButton.title = 'Delete program'; 814 | deleteButton.addEventListener('click', (e) => { 815 | e.stopPropagation(); 816 | deleteProgram(program.id); 817 | }); 818 | actionButtons.appendChild(deleteButton); 819 | 820 | actionsDiv.appendChild(actionButtons); 821 | programItem.appendChild(actionsDiv); 822 | 823 | programList.appendChild(programItem); 824 | }); 825 | } 826 | 827 | function showAddProgramForm() { 828 | programFormTitle.textContent = 'Add Program'; 829 | programNameInput.value = ''; 830 | programUrlInput.value = ''; 831 | programScopeInput.value = ''; 832 | programNotesInput.value = ''; 833 | currentProgramId = null; 834 | programFormContainer.classList.remove('hidden'); 835 | 836 | programFormContainer.classList.add('fade-in'); 837 | setTimeout(() => { 838 | programFormContainer.classList.remove('fade-in'); 839 | }, 300); 840 | 841 | programNameInput.focus(); 842 | } 843 | 844 | function showEditProgramForm(programId) { 845 | const program = programs.find(p => p.id === programId); 846 | if (!program) return; 847 | 848 | programFormTitle.textContent = 'Edit Program'; 849 | programNameInput.value = program.name; 850 | programUrlInput.value = program.url; 851 | programScopeInput.value = program.scope; 852 | programNotesInput.value = program.notes || ''; 853 | currentProgramId = programId; 854 | programFormContainer.classList.remove('hidden'); 855 | 856 | programFormContainer.classList.add('fade-in'); 857 | setTimeout(() => { 858 | programFormContainer.classList.remove('fade-in'); 859 | }, 300); 860 | 861 | programNameInput.focus(); 862 | } 863 | 864 | function hideProgramForm() { 865 | programFormContainer.classList.add('fade-out'); 866 | 867 | setTimeout(() => { 868 | programFormContainer.classList.add('hidden'); 869 | programFormContainer.classList.remove('fade-out'); 870 | }, 200); 871 | } 872 | 873 | function saveProgram(e) { 874 | e.preventDefault(); 875 | 876 | const name = programNameInput.value.trim(); 877 | const url = programUrlInput.value.trim(); 878 | const scope = programScopeInput.value.trim(); 879 | const notes = programNotesInput.value.trim(); 880 | 881 | if (!name || !url || !scope) { 882 | showStatus('Name, URL, and scope are required', 'error'); 883 | return; 884 | } 885 | 886 | if (!isValidUrl(url)) { 887 | showStatus('Invalid URL format', 'error'); 888 | return; 889 | } 890 | 891 | if (currentProgramId) { 892 | // Edit existing program 893 | const index = programs.findIndex(p => p.id === currentProgramId); 894 | if (index !== -1) { 895 | // Preserve the enabled state if it exists, default to true if not 896 | const enabled = programs[index].enabled !== undefined ? programs[index].enabled : true; 897 | 898 | programs[index] = { 899 | ...programs[index], 900 | name, 901 | url, 902 | scope, 903 | notes: notes || null, 904 | enabled, // Keep existing enabled state 905 | lastUpdated: new Date().toISOString() 906 | }; 907 | } 908 | } else { 909 | // Add new program 910 | const newProgram = { 911 | id: generateId(), 912 | name, 913 | url, 914 | scope, 915 | notes: notes || null, 916 | enabled: true, // Enable by default 917 | created: new Date().toISOString(), 918 | lastUpdated: new Date().toISOString() 919 | }; 920 | 921 | programs.push(newProgram); 922 | } 923 | 924 | // Save to storage 925 | chrome.storage.local.set({ programs }, () => { 926 | hideProgramForm(); 927 | renderProgramList(); 928 | checkCurrentUrl(); 929 | showStatus(currentProgramId ? 'Program updated successfully' : 'Program added successfully', 'success'); 930 | }); 931 | } 932 | 933 | function deleteProgram(programId) { 934 | if (confirm('Are you sure you want to delete this program?')) { 935 | programs = programs.filter(p => p.id !== programId); 936 | chrome.storage.local.set({ programs }, () => { 937 | renderProgramList(); 938 | updateDashboardStats(); 939 | checkCurrentUrl(); 940 | showStatus('Program deleted successfully', 'success'); 941 | }); 942 | } 943 | } 944 | 945 | // Toggle program enabled state 946 | function toggleProgram(programId, isEnabled) { 947 | const index = programs.findIndex(p => p.id === programId); 948 | if (index !== -1) { 949 | programs[index].enabled = isEnabled; 950 | 951 | // Update toggle label text immediately for better UX 952 | const programItem = document.querySelector(`.program-item[data-id="${programId}"]`); 953 | if (programItem) { 954 | const toggleText = programItem.querySelector('.toggle-label'); 955 | if (toggleText) { 956 | toggleText.textContent = isEnabled ? 'Enabled' : 'Disabled'; 957 | } 958 | } 959 | 960 | chrome.storage.local.set({ programs }, () => { 961 | renderProgramList(); 962 | 963 | // Check current URL scope with updated program state 964 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 965 | if (tabs[0] && tabs[0].url) { 966 | const url = tabs[0].url; 967 | 968 | // Send message to background script to recheck scope 969 | chrome.runtime.sendMessage({ action: 'checkScope', url }, (result) => { 970 | if (chrome.runtime.lastError) { 971 | console.error('Error checking scope:', chrome.runtime.lastError); 972 | showStatus('Error checking URL scope', 'error'); 973 | return; 974 | } 975 | 976 | if (result) { 977 | chrome.storage.local.set({ 978 | currentUrl: url, 979 | currentScopeStatus: result 980 | }); 981 | 982 | // Update UI with new scope results 983 | updateScopeStatus(); 984 | updateScopeStatusIndicator(result.inScope, result.programs); 985 | updateDashboardStats(); 986 | } else { 987 | console.warn('No result returned from checkScope'); 988 | showStatus('Unable to determine scope status', 'warning'); 989 | } 990 | }); 991 | } 992 | }); 993 | 994 | showStatus(`Program ${isEnabled ? 'enabled' : 'disabled'} successfully`, 'success'); 995 | }); 996 | } 997 | } 998 | 999 | function checkCurrentUrl() { 1000 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 1001 | if (tabs[0]) { 1002 | currentUrl = tabs[0].url; 1003 | updateScopeStatus(); 1004 | } 1005 | }); 1006 | } 1007 | 1008 | function updateScopeStatus() { 1009 | if (!currentUrl || !scopeStatusContainer) return; 1010 | 1011 | try { 1012 | const urlObj = new URL(currentUrl); 1013 | const domain = urlObj.hostname; 1014 | 1015 | // Only consider enabled programs when checking scope 1016 | const inScopePrograms = programs 1017 | .filter(program => program.enabled !== false) // Only enabled programs 1018 | .filter(program => isUrlInScope(currentUrl, program.scope, program.enabled)); 1019 | 1020 | let statusHTML = `

Current Tab Status

`; 1021 | 1022 | // Add current URL display 1023 | statusHTML += ` 1024 |
1025 |

${domain}

1026 |
1027 | `; 1028 | 1029 | if (inScopePrograms.length > 0) { 1030 | // In-scope status 1031 | statusHTML += ` 1032 |
1033 | ${ICONS.scope} In scope for ${inScopePrograms.length} program${inScopePrograms.length > 1 ? 's' : ''} 1034 |
1035 |
1036 | `; 1037 | 1038 | // List the programs it's in scope for using getProgramNames helper 1039 | const programNames = getProgramNames(inScopePrograms); 1040 | inScopePrograms.forEach(program => { 1041 | statusHTML += ` 1042 |
1043 | ${program.name || 'Unknown program'} 1044 |
1045 | `; 1046 | }); 1047 | 1048 | statusHTML += `
`; 1049 | } else { 1050 | // Out-of-scope status 1051 | statusHTML += ` 1052 |
1053 | ${ICONS.scope} Not in scope for any program 1054 |
1055 | `; 1056 | } 1057 | 1058 | scopeStatusContainer.innerHTML = statusHTML; 1059 | } catch (e) { 1060 | console.error('Error updating scope status:', e); 1061 | scopeStatusContainer.innerHTML = ` 1062 |

Current Tab Status

1063 |
1064 | ${ICONS.warning} Unable to determine scope status 1065 |
1066 | `; 1067 | } 1068 | } 1069 | 1070 | function isUrlInScope(url, scope, programEnabled = true) { 1071 | // If program is disabled, always return false 1072 | if (programEnabled === false) { 1073 | return false; 1074 | } 1075 | 1076 | // If no URL or scope, return false 1077 | if (!url || !scope) { 1078 | return false; 1079 | } 1080 | 1081 | try { 1082 | const urlObj = new URL(url); 1083 | const scopePatterns = scope.split('\n').map(pattern => pattern.trim()).filter(Boolean); 1084 | 1085 | // If no valid patterns, return false 1086 | if (scopePatterns.length === 0) { 1087 | return false; 1088 | } 1089 | 1090 | return scopePatterns.some(pattern => { 1091 | // Handle wildcard patterns 1092 | if (pattern.includes('*')) { 1093 | const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$'); 1094 | return regex.test(urlObj.hostname); 1095 | } 1096 | // Handle exact matches 1097 | return urlObj.hostname === pattern; 1098 | }); 1099 | } catch (e) { 1100 | console.error('Error checking URL scope:', e); 1101 | return false; 1102 | } 1103 | } 1104 | 1105 | function isValidUrl(url) { 1106 | try { 1107 | new URL(url); 1108 | return true; 1109 | } catch (e) { 1110 | return false; 1111 | } 1112 | } 1113 | 1114 | // Function to update dashboard statistics 1115 | function updateDashboardStats() { 1116 | chrome.storage.local.get(['proxies', 'programs', 'activeProxy', 'currentScopeStatus'], (data) => { 1117 | const proxies = data.proxies || []; 1118 | const programs = data.programs || []; 1119 | const activeProxyId = data.activeProxy; 1120 | const currentScopeStatus = data.currentScopeStatus || { inScope: false, programs: [] }; 1121 | 1122 | // Update proxy stats 1123 | const totalProxies = proxies.length; 1124 | const activeProxies = activeProxyId ? 1 : 0; 1125 | const activeRate = totalProxies > 0 ? (activeProxies / totalProxies) * 100 : 0; 1126 | 1127 | totalProxiesElement.textContent = totalProxies; 1128 | activeProxiesElement.textContent = activeProxies; 1129 | proxyChartBar.style.width = `${activeRate}%`; 1130 | 1131 | // Update program stats 1132 | const totalPrograms = programs.length; 1133 | const inScopePrograms = currentScopeStatus.programs.length; 1134 | 1135 | totalProgramsElement.textContent = totalPrograms; 1136 | inScopeCountElement.textContent = inScopePrograms; 1137 | 1138 | // Update scope status indicator 1139 | updateScopeStatusIndicator(currentScopeStatus.inScope, currentScopeStatus.programs); 1140 | }); 1141 | } 1142 | 1143 | // Helper function to safely get program names 1144 | function getProgramNames(programs) { 1145 | if (!programs || !Array.isArray(programs) || programs.length === 0) { 1146 | return ''; 1147 | } 1148 | 1149 | return programs 1150 | .map(p => (p && typeof p === 'object' && p.name) ? p.name : (typeof p === 'string' ? p : 'Unknown')) 1151 | .join(', '); 1152 | } 1153 | 1154 | // Function to update the scope status indicator on the dashboard 1155 | function updateScopeStatusIndicator(inScope, programs = []) { 1156 | let html = ''; 1157 | 1158 | if (inScope && programs && programs.length > 0) { 1159 | const programNames = getProgramNames(programs); 1160 | html = ` 1161 |
1162 | In Scope: ${programNames || 'Unknown program'} 1163 |
1164 | `; 1165 | } else { 1166 | html = ` 1167 |
1168 | Out of Scope 1169 |
1170 | `; 1171 | } 1172 | 1173 | if (scopeStatusElement) { 1174 | scopeStatusElement.innerHTML = html; 1175 | } 1176 | } 1177 | 1178 | // Function to toggle the active proxy (used by quick toggle) 1179 | function toggleActiveProxy() { 1180 | // If there's an active proxy, deactivate it 1181 | if (activeProxyId) { 1182 | toggleProxy(activeProxyId, false); 1183 | } else { 1184 | // If no active proxy, check if there's a proxy to activate 1185 | if (proxies.length > 0) { 1186 | // Use the most recently used proxy or the first one 1187 | const proxyToActivate = proxies[0].id; 1188 | toggleProxy(proxyToActivate, true); 1189 | } else { 1190 | showStatus('No proxies available. Please add a proxy first.', 'warning'); 1191 | } 1192 | } 1193 | 1194 | // Update the quick toggle button state 1195 | updateQuickToggleState(); 1196 | } 1197 | 1198 | // Function to switch tabs - improved for dashboard 1199 | function switchTab(tabId) { 1200 | // Update tab buttons 1201 | document.querySelectorAll('.tab').forEach(tab => { 1202 | if (tab.dataset.tab === tabId) { 1203 | tab.classList.add('active'); 1204 | } else { 1205 | tab.classList.remove('active'); 1206 | } 1207 | }); 1208 | 1209 | // Update tab content 1210 | document.querySelectorAll('.tab-content').forEach(content => { 1211 | if (content.id === `${tabId}-content`) { 1212 | content.classList.add('active'); 1213 | } else { 1214 | content.classList.remove('active'); 1215 | } 1216 | }); 1217 | 1218 | // Perform tab-specific actions 1219 | if (tabId === 'dashboard') { 1220 | updateDashboardStats(); 1221 | } else if (tabId === 'proxies') { 1222 | loadProxies(); 1223 | } else if (tabId === 'programs') { 1224 | loadPrograms(); 1225 | updateScopeStatus(); 1226 | } 1227 | } 1228 | 1229 | // Add this new function to update the active proxy info 1230 | function updateActiveProxyInfo() { 1231 | const activeProxyInfoElement = document.getElementById('active-proxy-info'); 1232 | if (!activeProxyInfoElement) return; 1233 | 1234 | chrome.storage.local.get(['proxies', 'activeProxy'], (data) => { 1235 | const proxies = data.proxies || []; 1236 | const activeProxyId = data.activeProxy; 1237 | 1238 | // If no active proxy, show empty state 1239 | if (!activeProxyId || proxies.length === 0) { 1240 | activeProxyInfoElement.innerHTML = ` 1241 |
1242 | 1243 |

No active proxy

1244 |
1245 | `; 1246 | return; 1247 | } 1248 | 1249 | // Find the active proxy 1250 | const activeProxy = proxies.find(proxy => proxy.id === activeProxyId); 1251 | if (!activeProxy) { 1252 | activeProxyInfoElement.innerHTML = ` 1253 |
1254 | 1255 |

Selected proxy not found

1256 |
1257 | `; 1258 | return; 1259 | } 1260 | 1261 | // Create HTML for active proxy details with improved table-like structure 1262 | const connectionString = `${activeProxy.protocol}://${activeProxy.ip}:${activeProxy.port}`; 1263 | 1264 | activeProxyInfoElement.innerHTML = ` 1265 |
1266 |
1267 |
Status
1268 |
1269 | 1270 | Active 1271 |
1272 |
1273 |
1274 |
Name
1275 |
${activeProxy.name}
1276 |
1277 |
1278 |
Connection
1279 |
${connectionString}
1280 |
1281 | ${activeProxy.username ? ` 1282 |
1283 |
Auth
1284 |
${activeProxy.username}
1285 |
` : ''} 1286 |
1287 | `; 1288 | }); 1289 | } 1290 | 1291 | // Function to update the quick toggle button state 1292 | function updateQuickToggleState() { 1293 | const quickToggleBtn = document.getElementById('quick-proxy-toggle'); 1294 | if (!quickToggleBtn) return; 1295 | 1296 | // Clear existing classes 1297 | quickToggleBtn.classList.remove('active', 'inactive'); 1298 | 1299 | if (activeProxyId) { 1300 | quickToggleBtn.classList.add('active'); 1301 | quickToggleBtn.title = 'Disable Proxy'; 1302 | } else { 1303 | quickToggleBtn.classList.add('inactive'); 1304 | quickToggleBtn.title = 'Enable Proxy'; 1305 | } 1306 | } 1307 | 1308 | // Initialize the popup 1309 | function init() { 1310 | // Load data 1311 | loadProxies(); 1312 | loadPrograms(); 1313 | 1314 | // Set up tabs 1315 | tabs.forEach(tab => { 1316 | tab.addEventListener('click', () => { 1317 | const tabName = tab.dataset.tab; 1318 | switchTab(tabName); 1319 | }); 1320 | }); 1321 | 1322 | // Update UI 1323 | updateDashboardStats(); 1324 | renderProxyList(); 1325 | renderProgramList(); 1326 | updateActiveProxyInfo(); 1327 | checkCurrentUrl(); 1328 | updateQuickToggleState(); 1329 | 1330 | // Set up form event listeners 1331 | if (proxyForm) { 1332 | proxyForm.addEventListener('submit', saveProxy); 1333 | } 1334 | } 1335 | 1336 | // Call init when DOM is ready 1337 | init(); 1338 | 1339 | // Function to ensure a URL has a scheme 1340 | function ensureUrlScheme(url) { 1341 | if (!url.startsWith('http://') && !url.startsWith('https://')) { 1342 | return 'https://' + url; 1343 | } 1344 | return url; 1345 | } 1346 | }); -------------------------------------------------------------------------------- /screenshots/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aacle/ProxSec/6b513bd67455d7818d9c79aa96dbb1dec3eecd0b/screenshots/image1.png -------------------------------------------------------------------------------- /screenshots/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aacle/ProxSec/6b513bd67455d7818d9c79aa96dbb1dec3eecd0b/screenshots/image2.png -------------------------------------------------------------------------------- /screenshots/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aacle/ProxSec/6b513bd67455d7818d9c79aa96dbb1dec3eecd0b/screenshots/image3.png -------------------------------------------------------------------------------- /screenshots/image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aacle/ProxSec/6b513bd67455d7818d9c79aa96dbb1dec3eecd0b/screenshots/image4.png -------------------------------------------------------------------------------- /scripts/generate-icons.js: -------------------------------------------------------------------------------- 1 | // This script generates PNG icons from the SVG source file 2 | // Install dependencies: npm install sharp fs-extra 3 | 4 | const sharp = require('sharp'); 5 | const fs = require('fs-extra'); 6 | const path = require('path'); 7 | 8 | const sizes = [16, 48, 128]; 9 | const srcFile = path.join(__dirname, '../icons/icon.svg'); 10 | const outputDir = path.join(__dirname, '../icons'); 11 | 12 | async function generateIcons() { 13 | console.log('Generating icons from:', srcFile); 14 | 15 | // Ensure the output directory exists 16 | await fs.ensureDir(outputDir); 17 | 18 | // Read the SVG file 19 | const svgBuffer = await fs.readFile(srcFile); 20 | 21 | // Generate PNG files for each size 22 | for (const size of sizes) { 23 | const outputFile = path.join(outputDir, `icon${size}.png`); 24 | 25 | console.log(`Creating ${size}x${size} icon at: ${outputFile}`); 26 | 27 | await sharp(svgBuffer) 28 | .resize(size, size) 29 | .png() 30 | .toFile(outputFile); 31 | } 32 | 33 | console.log('Icon generation complete!'); 34 | } 35 | 36 | generateIcons().catch(err => { 37 | console.error('Error generating icons:', err); 38 | process.exit(1); 39 | }); -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* ProxSec Styles - Minimalist Edition */ 2 | :root { 3 | /* Modern minimalist color palette */ 4 | --primary-color: #7952B3; /* Purple */ 5 | --primary-hover: #6742A3; 6 | --primary-light: #F3E8FF; 7 | --error-color: #DC2626; 8 | --success-color: #059669; 9 | --info-color: #2563EB; 10 | --warning-color: #D97706; 11 | --text-color: #111827; 12 | --secondary-text: #6B7280; 13 | --border-color: #E5E7EB; 14 | --background-color: #F9FAFB; 15 | --card-background: #FFFFFF; 16 | 17 | /* Status colors */ 18 | --active-status: #059669; 19 | --inactive-status: #9CA3AF; 20 | --error-status: #DC2626; 21 | --in-scope: #059669; 22 | --out-of-scope: #DC2626; 23 | 24 | /* Shadows */ 25 | --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 26 | --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 27 | 28 | /* Transitions */ 29 | --transition-fast: 150ms ease; 30 | --transition-normal: 250ms ease; 31 | 32 | /* Border radius */ 33 | --border-radius-sm: 4px; 34 | --border-radius-md: 6px; 35 | --border-radius-lg: 8px; 36 | } 37 | 38 | * { 39 | box-sizing: border-box; 40 | margin: 0; 41 | padding: 0; 42 | } 43 | 44 | html, body { 45 | height: 100%; 46 | margin: 0; 47 | padding: 0; 48 | } 49 | 50 | body { 51 | font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 52 | background-color: var(--background-color); 53 | color: var(--text-color); 54 | width: 380px; 55 | min-height: 500px; 56 | font-size: 14px; 57 | line-height: 1.5; 58 | overflow-x: hidden; 59 | } 60 | 61 | .container { 62 | display: flex; 63 | flex-direction: column; 64 | height: 100%; 65 | min-height: 500px; 66 | max-height: 600px; 67 | width: 380px; 68 | overflow: hidden; 69 | background-color: var(--background-color); 70 | box-shadow: var(--shadow-md); 71 | } 72 | 73 | /* Header */ 74 | header { 75 | background: linear-gradient(100deg, #7952B3 0%, #9370DB 100%); 76 | color: white; 77 | padding: 12px 16px; 78 | display: flex; 79 | flex-direction: column; 80 | gap: 12px; 81 | position: relative; 82 | z-index: 10; 83 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); 84 | } 85 | 86 | .header-top { 87 | display: flex; 88 | justify-content: space-between; 89 | align-items: center; 90 | padding: 5px 0 8px 0; 91 | } 92 | 93 | .logo { 94 | display: flex; 95 | align-items: center; 96 | gap: 12px; 97 | flex-direction: row; 98 | } 99 | 100 | .logo-text { 101 | display: flex; 102 | flex-direction: column; 103 | } 104 | 105 | .logo img { 106 | width: 36px; 107 | height: 36px; 108 | object-fit: contain; 109 | } 110 | 111 | .logo-image { 112 | width: 32px; 113 | height: 32px; 114 | object-fit: contain; 115 | filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.2)); 116 | border-radius: 6px; 117 | } 118 | 119 | header h1 { 120 | font-size: 20px; 121 | font-weight: 600; 122 | margin: 0; 123 | line-height: 1.2; 124 | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); 125 | } 126 | 127 | .subtitle { 128 | font-size: 11px; 129 | color: rgba(255, 255, 255, 0.85); 130 | margin: 0; 131 | font-weight: 400; 132 | letter-spacing: 0.2px; 133 | } 134 | 135 | /* Header Actions */ 136 | .header-actions { 137 | display: flex; 138 | gap: 8px; 139 | } 140 | 141 | .header-actions .btn-icon { 142 | background-color: rgba(255, 255, 255, 0.12); 143 | color: white; 144 | border: none; 145 | border-radius: var(--border-radius-md); 146 | width: 32px; 147 | height: 32px; 148 | display: flex; 149 | align-items: center; 150 | justify-content: center; 151 | cursor: pointer; 152 | transition: all var(--transition-fast); 153 | padding: 0; 154 | } 155 | 156 | .header-actions .btn-icon:hover { 157 | background-color: rgba(255, 255, 255, 0.2); 158 | } 159 | 160 | .header-actions .btn-icon:active { 161 | transform: translateY(1px); 162 | } 163 | 164 | .header-actions .btn-icon svg { 165 | width: 16px; 166 | height: 16px; 167 | } 168 | 169 | /* Tabs */ 170 | .tabs { 171 | display: flex; 172 | gap: 2px; 173 | margin-top: 4px; 174 | } 175 | 176 | .tab { 177 | background: transparent; 178 | color: rgba(255, 255, 255, 0.75); 179 | border: none; 180 | padding: 6px 12px; 181 | font-size: 13px; 182 | font-weight: 500; 183 | cursor: pointer; 184 | position: relative; 185 | transition: color var(--transition-normal); 186 | border-radius: var(--border-radius-sm); 187 | flex: 1; 188 | text-align: center; 189 | } 190 | 191 | .tab:hover { 192 | color: rgba(255, 255, 255, 0.9); 193 | background-color: rgba(255, 255, 255, 0.08); 194 | } 195 | 196 | .tab.active { 197 | color: white; 198 | font-weight: 600; 199 | background-color: rgba(255, 255, 255, 0.12); 200 | } 201 | 202 | .tab.active::after { 203 | content: ''; 204 | position: absolute; 205 | bottom: -2px; 206 | left: 25%; 207 | width: 50%; 208 | height: 2px; 209 | background-color: white; 210 | border-radius: 1px; 211 | } 212 | 213 | /* Main content */ 214 | main { 215 | flex: 1; 216 | overflow-y: auto; 217 | padding: 16px; 218 | position: relative; 219 | background-color: var(--background-color); 220 | scrollbar-width: thin; 221 | } 222 | 223 | main::-webkit-scrollbar { 224 | width: 4px; 225 | } 226 | 227 | main::-webkit-scrollbar-thumb { 228 | background-color: rgba(0, 0, 0, 0.2); 229 | border-radius: 2px; 230 | } 231 | 232 | /* Status bar */ 233 | .status-bar { 234 | background-color: var(--background-color); 235 | color: var(--secondary-text); 236 | padding: 6px 12px; 237 | font-size: 12px; 238 | font-weight: 500; 239 | border-top: 1px solid var(--border-color); 240 | border-bottom: 1px solid var(--border-color); 241 | display: flex; 242 | align-items: center; 243 | gap: 6px; 244 | transition: opacity 0.3s ease; 245 | } 246 | 247 | .status-bar svg { 248 | color: var(--secondary-text); 249 | } 250 | 251 | .status-success { 252 | background-color: var(--success-color); 253 | color: white; 254 | } 255 | 256 | .status-error { 257 | background-color: var(--error-color); 258 | color: white; 259 | } 260 | 261 | .status-warning { 262 | background-color: var(--warning-color); 263 | color: white; 264 | } 265 | 266 | .status-info { 267 | background-color: var(--info-color); 268 | color: white; 269 | } 270 | 271 | /* Tab Content */ 272 | .tab-content { 273 | display: none; 274 | flex: 1; 275 | overflow-y: auto; 276 | } 277 | 278 | .tab-content.active { 279 | display: flex; 280 | flex-direction: column; 281 | gap: 16px; 282 | } 283 | 284 | /* Dashboard Cards */ 285 | .dashboard-grid { 286 | display: grid; 287 | grid-template-columns: 1fr; 288 | gap: 14px; 289 | } 290 | 291 | .dashboard-card { 292 | background-color: var(--card-background); 293 | border-radius: var(--border-radius-md); 294 | box-shadow: var(--shadow-sm); 295 | border: 1px solid var(--border-color); 296 | overflow: hidden; 297 | transition: box-shadow var(--transition-normal); 298 | } 299 | 300 | .dashboard-card:hover { 301 | box-shadow: var(--shadow-md); 302 | } 303 | 304 | .dashboard-card.full-width { 305 | grid-column: 1 / -1; 306 | } 307 | 308 | .card-header { 309 | padding: 12px 14px; 310 | border-bottom: 1px solid var(--border-color); 311 | background-color: var(--primary-light); 312 | } 313 | 314 | .card-header h3 { 315 | font-size: 14px; 316 | font-weight: 600; 317 | color: var(--primary-color); 318 | margin: 0; 319 | } 320 | 321 | .card-body { 322 | padding: 14px; 323 | } 324 | 325 | /* Stats Container */ 326 | .stats-container { 327 | display: flex; 328 | justify-content: space-around; 329 | margin-bottom: 14px; 330 | } 331 | 332 | .stat-item { 333 | text-align: center; 334 | } 335 | 336 | .stat-value { 337 | font-size: 24px; 338 | font-weight: 700; 339 | color: var(--primary-color); 340 | margin-bottom: 2px; 341 | } 342 | 343 | .stat-label { 344 | font-size: 12px; 345 | color: var(--secondary-text); 346 | font-weight: 500; 347 | } 348 | 349 | /* Buttons */ 350 | .btn { 351 | display: inline-flex; 352 | align-items: center; 353 | justify-content: center; 354 | padding: 8px 14px; 355 | font-size: 13px; 356 | font-weight: 500; 357 | border-radius: var(--border-radius-md); 358 | border: none; 359 | cursor: pointer; 360 | transition: all var(--transition-fast); 361 | gap: 6px; 362 | } 363 | 364 | .btn-primary { 365 | background-color: var(--primary-color); 366 | color: white; 367 | } 368 | 369 | .btn-primary:hover { 370 | background-color: var(--primary-hover); 371 | } 372 | 373 | .btn-secondary { 374 | background-color: #f3f4f6; 375 | color: var(--text-color); 376 | border: 1px solid #e5e7eb; 377 | } 378 | 379 | .btn-secondary:hover { 380 | background-color: #e5e7eb; 381 | } 382 | 383 | .btn:active { 384 | transform: translateY(1px); 385 | } 386 | 387 | /* Empty state */ 388 | .empty-state { 389 | display: flex; 390 | flex-direction: column; 391 | align-items: center; 392 | justify-content: center; 393 | padding: 20px; 394 | text-align: center; 395 | color: var(--secondary-text); 396 | gap: 10px; 397 | } 398 | 399 | .empty-state svg { 400 | color: var(--secondary-text); 401 | opacity: 0.5; 402 | } 403 | 404 | .empty-state p { 405 | font-size: 13px; 406 | margin: 0; 407 | } 408 | 409 | /* Status indicators */ 410 | .scope-status-indicator { 411 | display: flex; 412 | align-items: center; 413 | justify-content: center; 414 | margin: 10px 0; 415 | } 416 | 417 | .status-icon { 418 | display: flex; 419 | align-items: center; 420 | justify-content: center; 421 | padding: 6px 12px; 422 | border-radius: 16px; 423 | font-size: 12px; 424 | font-weight: 500; 425 | gap: 6px; 426 | } 427 | 428 | .status-icon.in-scope { 429 | background-color: rgba(5, 150, 105, 0.1); 430 | color: var(--in-scope); 431 | border: 1px solid rgba(5, 150, 105, 0.2); 432 | } 433 | 434 | .status-icon.out-of-scope { 435 | background-color: rgba(220, 38, 38, 0.1); 436 | color: var(--out-of-scope); 437 | border: 1px solid rgba(220, 38, 38, 0.2); 438 | } 439 | 440 | /* Dashboard Action */ 441 | .dashboard-action { 442 | margin-top: 12px; 443 | text-align: center; 444 | } 445 | 446 | /* Section Headers */ 447 | .section-header { 448 | display: flex; 449 | justify-content: space-between; 450 | align-items: center; 451 | margin-bottom: 14px; 452 | } 453 | 454 | .section-header h2 { 455 | font-size: 16px; 456 | font-weight: 600; 457 | color: var(--text-color); 458 | margin: 0; 459 | } 460 | 461 | /* List Container */ 462 | .list-container { 463 | display: flex; 464 | flex-direction: column; 465 | gap: 10px; 466 | } 467 | 468 | /* Proxy & Program Items */ 469 | .proxy-item, .program-item { 470 | background-color: var(--card-background); 471 | border-radius: var(--border-radius-md); 472 | padding: 12px; 473 | box-shadow: var(--shadow-sm); 474 | border: 1px solid var(--border-color); 475 | transition: all var(--transition-fast); 476 | } 477 | 478 | .proxy-item:hover, .program-item:hover { 479 | box-shadow: var(--shadow-md); 480 | } 481 | 482 | .proxy-name, .program-name { 483 | font-weight: 600; 484 | font-size: 14px; 485 | color: var(--text-color); 486 | margin: 0; 487 | display: flex; 488 | align-items: center; 489 | justify-content: space-between; 490 | gap: 8px; 491 | } 492 | 493 | .proxy-details, .program-details { 494 | font-family: monospace; 495 | color: var(--secondary-text); 496 | font-size: 12px; 497 | margin: 6px 0 10px 0; 498 | word-break: break-all; 499 | } 500 | 501 | .proxy-actions, .program-actions { 502 | display: flex; 503 | justify-content: space-between; 504 | align-items: center; 505 | margin-top: 8px; 506 | } 507 | 508 | .action-buttons { 509 | display: flex; 510 | gap: 6px; 511 | } 512 | 513 | /* Form Container */ 514 | .form-container { 515 | background-color: var(--card-background); 516 | border-radius: var(--border-radius-md); 517 | padding: 14px; 518 | box-shadow: var(--shadow-md); 519 | border: 1px solid var(--border-color); 520 | margin-top: 16px; 521 | } 522 | 523 | .form-header { 524 | margin-bottom: 14px; 525 | border-bottom: 1px solid var(--border-color); 526 | padding-bottom: 10px; 527 | } 528 | 529 | .form-header h3 { 530 | font-size: 15px; 531 | font-weight: 600; 532 | color: var(--text-color); 533 | margin: 0; 534 | } 535 | 536 | .form-group { 537 | margin-bottom: 14px; 538 | } 539 | 540 | .form-group label { 541 | display: block; 542 | margin-bottom: 6px; 543 | font-weight: 500; 544 | color: var(--text-color); 545 | font-size: 12px; 546 | } 547 | 548 | .form-group input, 549 | .form-group select, 550 | .form-group textarea { 551 | width: 100%; 552 | padding: 8px 10px; 553 | border: 1px solid var(--border-color); 554 | border-radius: var(--border-radius-sm); 555 | font-size: 13px; 556 | background-color: white; 557 | transition: border-color var(--transition-fast); 558 | color: var(--text-color); 559 | } 560 | 561 | .form-group input:focus, 562 | .form-group select:focus, 563 | .form-group textarea:focus { 564 | outline: none; 565 | border-color: var(--primary-color); 566 | box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1); 567 | } 568 | 569 | .form-row { 570 | display: flex; 571 | gap: 12px; 572 | margin-bottom: 14px; 573 | } 574 | 575 | .form-row .form-group { 576 | flex: 1; 577 | margin-bottom: 0; 578 | } 579 | 580 | .form-actions { 581 | display: flex; 582 | justify-content: flex-end; 583 | gap: 10px; 584 | margin-top: 16px; 585 | padding-top: 14px; 586 | border-top: 1px solid var(--border-color); 587 | } 588 | 589 | .form-help { 590 | font-size: 11px; 591 | color: var(--secondary-text); 592 | margin-top: 4px; 593 | } 594 | 595 | /* Status Container */ 596 | .status-container { 597 | background-color: var(--card-background); 598 | border-radius: var(--border-radius-md); 599 | box-shadow: var(--shadow-sm); 600 | border: 1px solid var(--border-color); 601 | padding: 14px; 602 | margin-top: 16px; 603 | } 604 | 605 | /* About Tab Styling */ 606 | #about-content h2 { 607 | font-size: 18px; 608 | margin-bottom: 8px; 609 | color: var(--text-color); 610 | font-weight: 600; 611 | } 612 | 613 | .description { 614 | margin-bottom: 16px; 615 | color: var(--secondary-text); 616 | line-height: 1.6; 617 | font-size: 13px; 618 | } 619 | 620 | .info-box { 621 | background-color: var(--card-background); 622 | border-radius: var(--border-radius-md); 623 | padding: 14px; 624 | margin-bottom: 14px; 625 | border: 1px solid var(--border-color); 626 | box-shadow: var(--shadow-sm); 627 | } 628 | 629 | .info-box h3 { 630 | font-size: 14px; 631 | margin-bottom: 8px; 632 | color: var(--text-color); 633 | font-weight: 600; 634 | } 635 | 636 | .info-box p { 637 | color: var(--secondary-text); 638 | font-size: 12px; 639 | line-height: 1.5; 640 | margin-bottom: 8px; 641 | } 642 | 643 | .info-box ul { 644 | margin: 8px 0; 645 | padding-left: 16px; 646 | } 647 | 648 | .info-box li { 649 | margin-bottom: 4px; 650 | color: var (--secondary-text); 651 | font-size: 12px; 652 | } 653 | 654 | /* Trust Badges */ 655 | .trust-badges { 656 | display: flex; 657 | gap: 8px; 658 | margin: 14px 0; 659 | } 660 | 661 | .badge { 662 | display: flex; 663 | align-items: center; 664 | gap: 6px; 665 | background-color: var(--background-color); 666 | border: 1px solid var(--border-color); 667 | border-radius: var(--border-radius-sm); 668 | padding: 5px 8px; 669 | font-size: 11px; 670 | font-weight: 500; 671 | color: var(--text-color); 672 | } 673 | 674 | .badge svg { 675 | width: 14px; 676 | height: 14px; 677 | color: var(--primary-color); 678 | } 679 | 680 | /* Footer */ 681 | footer { 682 | padding: 8px 12px; 683 | background-color: var(--background-color); 684 | border-top: 1px solid var(--border-color); 685 | color: var(--secondary-text); 686 | display: flex; 687 | justify-content: flex-end; 688 | font-size: 11px; 689 | } 690 | 691 | /* Icon Button */ 692 | .btn-icon { 693 | background-color: transparent; 694 | border: 1px solid var(--border-color); 695 | border-radius: var(--border-radius-sm); 696 | width: 28px; 697 | height: 28px; 698 | display: flex; 699 | align-items: center; 700 | justify-content: center; 701 | cursor: pointer; 702 | transition: all var(--transition-fast); 703 | color: var(--text-color); 704 | padding: 0; 705 | } 706 | 707 | .btn-icon:hover { 708 | background-color: rgba(0, 0, 0, 0.05); 709 | } 710 | 711 | /* Toggle Switch */ 712 | .toggle-container { 713 | display: flex; 714 | align-items: center; 715 | margin-right: 10px; 716 | } 717 | 718 | .toggle-label { 719 | margin-left: 8px; 720 | font-size: 12px; 721 | } 722 | 723 | .switch { 724 | position: relative; 725 | display: inline-block; 726 | width: 36px; 727 | height: 20px; 728 | } 729 | 730 | .switch input { 731 | opacity: 0; 732 | width: 0; 733 | height: 0; 734 | } 735 | 736 | .slider { 737 | position: absolute; 738 | cursor: pointer; 739 | top: 0; 740 | left: 0; 741 | right: 0; 742 | bottom: 0; 743 | background-color: #ccc; 744 | transition: .3s; 745 | } 746 | 747 | .slider:before { 748 | position: absolute; 749 | content: ""; 750 | height: 16px; 751 | width: 16px; 752 | left: 2px; 753 | bottom: 2px; 754 | background-color: white; 755 | transition: .3s; 756 | } 757 | 758 | input:checked + .slider { 759 | background-color: var(--primary-color); 760 | } 761 | 762 | input:focus + .slider { 763 | box-shadow: 0 0 1px var(--primary-color); 764 | } 765 | 766 | input:checked + .slider:before { 767 | transform: translateX(16px); 768 | } 769 | 770 | .slider.round { 771 | border-radius: 20px; 772 | } 773 | 774 | .slider.round:before { 775 | border-radius: 50%; 776 | } 777 | 778 | /* Status badges */ 779 | .proxy-status, .scope-status { 780 | display: inline-flex; 781 | align-items: center; 782 | gap: 4px; 783 | padding: 2px 8px; 784 | border-radius: 12px; 785 | font-size: 11px; 786 | font-weight: 500; 787 | color: white; 788 | } 789 | 790 | .status-active { 791 | background-color: var(--active-status); 792 | } 793 | 794 | .status-error { 795 | background-color: var(--error-status); 796 | } 797 | 798 | .status-inactive { 799 | background-color: var(--inactive-status); 800 | } 801 | 802 | .status-disabled { 803 | background-color: #6B7280; /* Darker gray to indicate disabled state */ 804 | } 805 | 806 | .in-scope { 807 | background-color: var(--in-scope); 808 | } 809 | 810 | .out-of-scope { 811 | background-color: var(--out-of-scope); 812 | } 813 | 814 | /* Animations */ 815 | .fade-in { 816 | animation: fadeIn 0.3s forwards; 817 | } 818 | 819 | .fade-out { 820 | animation: fadeOut 0.3s forwards; 821 | } 822 | 823 | @keyframes fadeIn { 824 | from { opacity: 0; } 825 | to { opacity: 1; } 826 | } 827 | 828 | @keyframes fadeOut { 829 | from { opacity: 1; } 830 | to { opacity: 0; } 831 | } 832 | 833 | .hidden { 834 | display: none; 835 | } 836 | 837 | /* Program mini item */ 838 | .program-mini-item { 839 | padding: 4px 8px; 840 | background-color: var(--primary-light); 841 | color: var(--primary-color); 842 | border-radius: 4px; 843 | font-size: 12px; 844 | font-weight: 500; 845 | display: flex; 846 | align-items: center; 847 | margin-bottom: 4px; 848 | } 849 | 850 | .program-mini-item::before { 851 | content: ""; 852 | display: inline-block; 853 | width: 4px; 854 | height: 4px; 855 | border-radius: 50%; 856 | background-color: var(--primary-color); 857 | margin-right: 6px; 858 | } 859 | 860 | /* Current domain */ 861 | .current-domain { 862 | font-family: monospace; 863 | font-size: 13px; 864 | font-weight: 500; 865 | color: var(--text-color); 866 | background-color: var(--background-color); 867 | padding: 6px 8px; 868 | border-radius: 4px; 869 | margin-bottom: 10px; 870 | border: 1px solid var(--border-color); 871 | word-break: break-all; 872 | } 873 | 874 | /* Add these styles for better active proxy display */ 875 | .active-proxy-details { 876 | background-color: var(--background-color); 877 | border-radius: var(--border-radius-md); 878 | overflow: hidden; 879 | border: 1px solid var(--border-color); 880 | } 881 | 882 | .proxy-info-row { 883 | display: flex; 884 | border-bottom: 1px solid var(--border-color); 885 | } 886 | 887 | .proxy-info-row:last-child { 888 | border-bottom: none; 889 | } 890 | 891 | .proxy-info-label { 892 | width: 90px; 893 | padding: 8px 10px; 894 | font-size: 12px; 895 | font-weight: 500; 896 | color: var(--secondary-text); 897 | background-color: rgba(0, 0, 0, 0.02); 898 | border-right: 1px solid var(--border-color); 899 | } 900 | 901 | .proxy-info-value, .proxy-status-badge { 902 | flex: 1; 903 | padding: 8px 10px; 904 | font-size: 12px; 905 | font-weight: 500; 906 | color: var(--text-color); 907 | font-family: monospace; 908 | } 909 | 910 | .proxy-status-badge { 911 | display: flex; 912 | align-items: center; 913 | gap: 6px; 914 | font-family: inherit; 915 | color: var(--active-status); 916 | } 917 | 918 | .proxy-status-badge svg { 919 | width: 14px; 920 | height: 14px; 921 | stroke: var(--active-status); 922 | } 923 | 924 | .social-link { 925 | background-color: rgba(255, 255, 255, 0.15); 926 | border: none; 927 | width: 32px; 928 | height: 32px; 929 | color: white; 930 | border-radius: 50%; 931 | transition: all 0.2s ease; 932 | } 933 | 934 | .social-link:hover { 935 | background-color: rgba(255, 255, 255, 0.25); 936 | transform: translateY(-2px); 937 | } 938 | 939 | /* Quick Proxy Toggle Button Styles */ 940 | #quick-proxy-toggle { 941 | position: relative; 942 | overflow: hidden; 943 | } 944 | 945 | #quick-proxy-toggle.active { 946 | background-color: var(--success-color); 947 | box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3); 948 | transform: scale(1.05); 949 | } 950 | 951 | #quick-proxy-toggle.inactive { 952 | background-color: rgba(255, 255, 255, 0.15); 953 | } 954 | 955 | #quick-proxy-toggle svg { 956 | transition: all 0.2s ease; 957 | } 958 | 959 | #quick-proxy-toggle.active svg { 960 | fill: rgba(255, 255, 255, 0.6); 961 | filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.5)); 962 | } 963 | 964 | #quick-proxy-toggle::after { 965 | content: ''; 966 | position: absolute; 967 | top: -2px; 968 | right: -2px; 969 | width: 8px; 970 | height: 8px; 971 | border-radius: 50%; 972 | opacity: 0; 973 | transition: opacity 0.2s ease; 974 | } 975 | 976 | #quick-proxy-toggle.active::after { 977 | background-color: #ffffff; 978 | opacity: 1; 979 | box-shadow: 0 0 4px rgba(255, 255, 255, 0.8); 980 | } 981 | 982 | #quick-proxy-toggle.inactive::after { 983 | background-color: var(--inactive-status); 984 | opacity: 0.7; 985 | } --------------------------------------------------------------------------------