├── icon.png ├── manifest.json ├── regax.txt ├── README.md ├── secrets.js ├── popup.html ├── styles.css ├── popup.js └── background.js /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dirtycoder0124/keysec-hunter---Chrome-extension/HEAD/icon.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "HTML_Search_Engine", 4 | "version": "3.1", 5 | "description": "Search saved keywords in HTML source + internal links, only on sites you enable.", 6 | "permissions": ["scripting", "tabs", "storage", "webNavigation", "notifications"], 7 | "host_permissions": [""], 8 | "action": { 9 | "default_popup": "popup.html", 10 | "default_icon": { 11 | "16": "icon.png", 12 | "32": "icon.png", 13 | "48": "icon.png", 14 | "128": "icon.png" 15 | } 16 | }, 17 | "icons": { 18 | "16": "icon.png", 19 | "32": "icon.png", 20 | "48": "icon.png", 21 | "128": "icon.png" 22 | }, 23 | "background": { 24 | "service_worker": "background.js" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /regax.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Slack Token": "xox[pboa]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32}", 3 | "Google API Key": "AIza[0-9A-Za-z\\-_]{35}", 4 | "RSA private key": "-----BEGIN RSA PRIVATE KEY-----", 5 | "SSH (DSA) private key": "-----BEGIN DSA PRIVATE KEY-----", 6 | "SSH (EC) private key": "-----BEGIN EC PRIVATE KEY-----", 7 | "PGP private key block": "-----BEGIN PGP PRIVATE KEY BLOCK-----", 8 | "Amazon MWS Auth Token": "amzn\\.mws\\.[0-9a-f\\-]{36}", 9 | "AWS AppSync GraphQL Key": "da2-[a-z0-9]{26}", 10 | "Facebook Access Token": "EAACEdEose0cBA[0-9A-Za-z]+", 11 | "GitHub Token": "[gG][iI][tT][hH][uU][bB].{0,20}['|\"][0-9a-zA-Z]{35,40}['|\"]", 12 | "Google Service Account": "\"type\":\\s*\"service_account\"", 13 | "Heroku API Key": "[hH][eE][rR][oO][kK][uU].{0,20}[0-9A-Fa-f\\-]{36}", 14 | "JWT": "eyJhbGciOiJ[a-zA-Z0-9_-]{5,}\\.[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+", 15 | "MailChimp API Key": "[0-9a-f]{32}-us[0-9]{1,2}", 16 | "Slack Webhook": "https://hooks\\.slack\\.com/services/[A-Za-z0-9_/]{24,}", 17 | "Stripe Key": "(sk|rk)_live_[0-9a-zA-Z]{24}", 18 | "Telegram Bot Token": "[0-9]+:AA[0-9A-Za-z\\-_]{33}", 19 | "Twilio API Key": "SK[0-9a-fA-F]{32}", 20 | "Generic API Key": "[aA][pP][iI]_?[kK][eE][yY].{0,20}['|\"][0-9a-zA-Z]{32,45}['|\"]", 21 | "Generic Secret": "[sS][eE][cC][rR][eE][tT].{0,20}['|\"][0-9a-zA-Z]{32,45}['|\"]" 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔑 KeySec Hunter 2 | 3 | **Keywords + Secrets = KeySec Hunter** 4 | 5 | KeySec Hunter is a **Chrome extension** that scans webpages and linked JavaScript files for **user-defined keywords** and **potential secrets** (API keys, tokens, passwords, credentials, etc.). 6 | It’s built for security researchers, developers, and bug bounty hunters who want to quickly detect sensitive data leaks during web analysis. 7 | 8 | ## 🚀 Features 9 | - **Custom Keyword Scanning** — define your own keywords to match anything you care about. 10 | - **Secret Detection Engine** — built-in regex patterns for API keys, tokens, and credentials. 11 | - **Smart Crawling** — scans internal links and JavaScript files. 12 | - **Three Notification Modes** 13 | - Chrome notifications 14 | - In-page alerts 15 | - Disabled mode for silent operation 16 | - **Simple UI Tabs** 17 | - Home — live scan results for keywords and secrets 18 | - All Parameter Links — lists every link with parameters found on the site 19 | - Settings — configure keywords, limits, and notifications easily 20 | 21 | ## ⚙️ How to Use 22 | 1. **Clone or download** this repository. 23 | 2. Open Chrome → go to chrome://extensions/ 24 | 3. Enable Developer mode 25 | 4. Click Load unpacked and select the KeySec Hunter folder 26 | 5. Visit any website and open the KeySec Hunter popup 27 | 6. Toggle scanning ON and watch results update live! 28 | 29 | ## 🏠 Home Tab 30 | Shows **Keywords found** and **Secrets detected** in real-time while scanning a page. 31 | image 32 | ## 🔗 All Parameter Links Tab 33 | Displays **all URLs that contain query parameters**, helping you discover interesting endpoints for further testing. 34 | image 35 | ## ⚙️ Settings Tab 36 | Customize your scan by adding **keywords** you want to search across the website, choose **max links**, and set your **notification mode**. 37 | image 38 | 39 | 40 | ## 📁 Project Structure 41 | keysec-hunter/ 42 | 43 | ├── manifest.json 44 | 45 | ├── background.js 46 | 47 | ├── popup.html 48 | 49 | ├── popup.js 50 | 51 | ├── styles.css 52 | 53 | ├── regax.txt # secret regex list (You can add your favourite regax in this file) 54 | 55 | └── icons/ 56 | -------------------------------------------------------------------------------- /secrets.js: -------------------------------------------------------------------------------- 1 | // secrets.js 2 | // Displays secrets only for the current active site's domain (fresh, domain-specific results) 3 | 4 | (async () => { 5 | try { 6 | const container = document.getElementById("secretResults"); 7 | if (!container) return; 8 | 9 | container.innerHTML = "Loading saved secrets..."; 10 | 11 | // Get current tab and domain 12 | const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 13 | if (!tab?.url) { 14 | container.textContent = "Unable to detect current site."; 15 | return; 16 | } 17 | 18 | const currentDomain = new URL(tab.url).hostname; 19 | const storageKey = `secretsFound_${currentDomain}`; 20 | 21 | // Fetch secrets for this domain 22 | chrome.storage.local.get([storageKey], (data) => { 23 | const matches = data[storageKey] || []; 24 | 25 | container.innerHTML = ""; // Clear "Loading..." 26 | if (matches.length === 0) { 27 | container.textContent = "No secrets found yet for this site."; 28 | return; 29 | } 30 | 31 | // Render each secret 32 | for (const item of matches) { 33 | const div = document.createElement("div"); 34 | div.className = "secret-item"; 35 | 36 | const url = item.pageUrl || ""; 37 | const fileUrl = item.fileUrl || ""; 38 | let displayName = ""; 39 | 40 | try { 41 | // Determine readable filename 42 | if (!fileUrl || fileUrl === "PAGE_HTML") { 43 | const pageName = url.split("/").filter(Boolean).pop() || "index.html"; 44 | displayName = makeLink(url, pageName); 45 | } 46 | else if (fileUrl.includes("#script-")) { 47 | const baseName = fileUrl.split("#")[0].split("/").pop() || "index.html"; 48 | const scriptNum = fileUrl.split("#script-").pop(); 49 | displayName = makeLink(url, `${baseName} (inline script ${scriptNum})`); 50 | } 51 | else { 52 | const cleanName = fileUrl.split("/").pop() || "index.html"; 53 | displayName = makeLink(fileUrl, cleanName); 54 | } 55 | } catch { 56 | displayName = `index.html`; 57 | } 58 | 59 | div.innerHTML = ` 60 |
61 | ${escapeHtml(item.name)} 62 |
63 |
66 | ${highlightSecret(item.match)} 67 |
68 |
${displayName}
69 | `; 70 | container.appendChild(div); 71 | } 72 | }); 73 | 74 | } catch (err) { 75 | console.error("Secrets tab error:", err); 76 | const container = document.getElementById("secretResults"); 77 | if (container) container.textContent = "Error loading secrets: " + String(err); 78 | } 79 | })(); 80 | 81 | // ---------- Helpers ---------- 82 | function escapeHtml(s) { 83 | return s 84 | ? String(s).replace(/[&<>"']/g, c => 85 | ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]) 86 | ) 87 | : ""; 88 | } 89 | 90 | function highlightSecret(secret) { 91 | return secret 92 | ? `${escapeHtml(secret)}` 93 | : ""; 94 | } 95 | 96 | function makeLink(href, text) { 97 | return ` 99 | ${escapeHtml(text)} 100 | `; 101 | } 102 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | KeySec Hunter 5 | 6 | 151 | 152 | 153 | 154 |
155 |
Home
156 |
All Parameter Links
157 |
Settings
158 |
159 | 160 | 161 |
162 |
163 | 167 | 168 |
169 | 170 |
171 | 172 |
173 |

Found Keyword Links

174 |
Loading...
175 |
176 | 177 | 178 |
179 |

Secrets Found

180 |
Loading...
181 |
182 |
183 |
184 | 185 | 186 |
187 |

KeySec Hunter - Settings

188 | 189 |
190 |

191 | 192 |
193 |

199 | 200 |
201 |

206 | 207 | 208 |

209 | 210 |
211 | 212 |
213 | 214 |
215 | 216 |
217 | 218 | 219 |
220 |

All Parameter Links

221 |
Loading...
222 |
223 | 224 | 229 | 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* ===== Global Styling ===== */ 2 | body { 3 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 4 | background-color: #f8f9fa; 5 | color: #333; 6 | padding: 15px; 7 | width: 700px; 8 | height: auto; 9 | overflow-x: hidden; 10 | box-sizing: border-box; 11 | } 12 | 13 | h2 { 14 | margin-top: 0; 15 | text-align: center; 16 | color: #1b1b1b; 17 | font-size: 1.4em; 18 | } 19 | 20 | input, select { 21 | width: 100%; 22 | margin-top: 5px; 23 | padding: 8px; 24 | border: 1px solid #ccc; 25 | border-radius: 5px; 26 | font-size: 0.95em; 27 | box-sizing: border-box; 28 | } 29 | 30 | input:focus, select:focus { 31 | outline: none; 32 | border-color: #1DA1F2; 33 | box-shadow: 0 0 5px rgba(29,161,242,0.5); 34 | } 35 | 36 | button { 37 | width: 100%; 38 | margin-top: 10px; 39 | padding: 8px; 40 | background-color: #1DA1F2; 41 | color: white; 42 | font-weight: bold; 43 | border: none; 44 | border-radius: 5px; 45 | cursor: pointer; 46 | font-size: 0.95em; 47 | transition: background 0.3s ease; 48 | } 49 | 50 | button:hover { 51 | background-color: #0d8ddb; 52 | } 53 | 54 | #status { 55 | margin-top: 10px; 56 | font-size: 0.9em; 57 | text-align: center; 58 | } 59 | 60 | #status.success { 61 | color: green; 62 | } 63 | 64 | #status.error { 65 | color: red; 66 | } 67 | 68 | /* ===== Tabs ===== */ 69 | .tabs { 70 | display: flex; 71 | border-bottom: 1px solid #ccc; 72 | margin-bottom: 10px; 73 | } 74 | 75 | .tab { 76 | padding: 8px 15px; 77 | cursor: pointer; 78 | border: 1px solid #ccc; 79 | border-bottom: none; 80 | background: #f1f1f1; 81 | margin-right: 5px; 82 | border-radius: 6px 6px 0 0; 83 | transition: all 0.2s ease; 84 | } 85 | 86 | .tab:hover { 87 | background: #e7f3ff; 88 | } 89 | 90 | .tab.active { 91 | background: #fff; 92 | font-weight: bold; 93 | border-bottom: 1px solid #fff; 94 | } 95 | 96 | .tab-content { 97 | display: none; 98 | } 99 | 100 | .tab-content.active { 101 | display: block; 102 | } 103 | 104 | /* ===== Home Grid Layout ===== */ 105 | .home-grid { 106 | display: grid; 107 | grid-template-columns: 1fr 1fr; 108 | gap: 15px; 109 | } 110 | 111 | .column-box { 112 | background: #fff; 113 | border: 1px solid #ddd; 114 | border-radius: 8px; 115 | padding: 12px; 116 | overflow-y: auto; 117 | max-height: 420px; 118 | box-shadow: 0 2px 5px rgba(0,0,0,0.08); 119 | } 120 | 121 | .column-box h3 { 122 | margin-top: 0; 123 | border-bottom: 1px solid #e0e0e0; 124 | padding-bottom: 6px; 125 | color: #222; 126 | font-size: 1.05em; 127 | } 128 | 129 | /* ===== Toggle Switch ===== */ 130 | .toggle-container { 131 | display: flex; 132 | align-items: center; 133 | margin-bottom: 10px; 134 | } 135 | 136 | .site-label { 137 | margin-left: 10px; 138 | font-size: 0.95em; 139 | font-weight: 500; 140 | color: #333; 141 | } 142 | 143 | .switch { 144 | position: relative; 145 | display: inline-block; 146 | width: 50px; 147 | height: 24px; 148 | margin-right: 10px; 149 | } 150 | 151 | .switch input { 152 | opacity: 0; 153 | width: 0; 154 | height: 0; 155 | } 156 | 157 | .slider { 158 | position: absolute; 159 | cursor: pointer; 160 | top: 0; 161 | left: 0; 162 | right: 0; 163 | bottom: 0; 164 | background-color: #ccc; 165 | transition: 0.4s; 166 | border-radius: 24px; 167 | } 168 | 169 | .slider:before { 170 | position: absolute; 171 | content: ""; 172 | height: 18px; 173 | width: 18px; 174 | left: 3px; 175 | bottom: 3px; 176 | background-color: white; 177 | transition: 0.4s; 178 | border-radius: 50%; 179 | } 180 | 181 | input:checked + .slider { 182 | background-color: #1DA1F2; 183 | } 184 | 185 | input:checked + .slider:before { 186 | transform: translateX(26px); 187 | } 188 | 189 | /* ===== Results (Found Keywords) ===== */ 190 | #results { 191 | margin-top: 10px; 192 | font-size: 14px; 193 | } 194 | 195 | .result-card { 196 | background: #ffffff; 197 | border: 1px solid #e1e4e8; 198 | border-radius: 8px; 199 | padding: 10px 12px; 200 | margin-bottom: 10px; 201 | box-shadow: 0 1px 3px rgba(0,0,0,0.08); 202 | font-size: 14px; 203 | transition: transform 0.15s ease, box-shadow 0.15s ease; 204 | word-wrap: break-word; 205 | } 206 | 207 | .result-card:hover { 208 | transform: translateY(-2px); 209 | box-shadow: 0 3px 6px rgba(0,0,0,0.12); 210 | } 211 | 212 | .result-card b { 213 | color: #d6336c; 214 | } 215 | 216 | .result-card a { 217 | color: #0077cc; 218 | text-decoration: none; 219 | font-weight: 500; 220 | word-break: break-word; 221 | } 222 | 223 | .result-card a:hover { 224 | text-decoration: underline; 225 | } 226 | 227 | .result-card code { 228 | display: block; 229 | background-color: #f4f6f7; 230 | padding: 4px 6px; 231 | border-radius: 4px; 232 | margin-top: 4px; 233 | font-size: 0.85em; 234 | color: #495057; 235 | } 236 | 237 | /* ===== Secrets Found Section ===== */ 238 | #secretResults, #homeSecrets { 239 | margin-top: 10px; 240 | font-size: 14px; 241 | max-height: 400px; 242 | overflow-y: auto; 243 | } 244 | 245 | .secret-item { 246 | background: #fff; 247 | border: 1px solid #e0e0e0; 248 | padding: 8px 10px; 249 | border-left: 4px solid #ff4d4d; 250 | border-radius: 6px; 251 | margin-bottom: 8px; 252 | font-family: monospace; 253 | word-wrap: break-word; 254 | box-shadow: 0 1px 3px rgba(0,0,0,0.08); 255 | } 256 | 257 | .secret-item b { 258 | color: #c0392b; 259 | } 260 | 261 | .secret-item code { 262 | background: #fdf0f0; 263 | border: 1px solid #f1cccc; 264 | border-radius: 6px; 265 | padding: 5px 8px; 266 | display: block; 267 | margin: 5px 0; 268 | color: #d32f2f; 269 | font-size: 13px; 270 | } 271 | 272 | /* ===== Links with Parameters ===== */ 273 | #linksWithParams { 274 | margin-top: 12px; 275 | max-height: 400px; 276 | overflow-y: auto; 277 | } 278 | 279 | #linksWithParams ul { 280 | list-style: none; 281 | padding: 0; 282 | margin: 0; 283 | } 284 | 285 | #linksWithParams li { 286 | background: #fff; 287 | border: 1px solid #e0e0e0; 288 | padding: 8px 10px; 289 | border-radius: 6px; 290 | margin-bottom: 8px; 291 | font-size: 13.5px; 292 | box-shadow: 0 1px 3px rgba(0,0,0,0.08); 293 | transition: transform 0.15s ease, box-shadow 0.15s ease; 294 | } 295 | 296 | #linksWithParams li:hover { 297 | transform: translateY(-2px); 298 | box-shadow: 0 3px 6px rgba(0,0,0,0.12); 299 | } 300 | 301 | #linksWithParams a { 302 | color: #1DA1F2; 303 | font-weight: 500; 304 | text-decoration: none; 305 | word-break: break-word; 306 | } 307 | 308 | #linksWithParams a:hover { 309 | text-decoration: underline; 310 | } 311 | 312 | /* ===== Misc ===== */ 313 | #twitterHandle { 314 | display: block; 315 | margin-top: 15px; 316 | text-align: center; 317 | font-size: 0.9em; 318 | color: #555; 319 | } 320 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", async () => { 2 | const tabs = document.querySelectorAll(".tab"); 3 | const tabContents = document.querySelectorAll(".tab-content"); 4 | const linksWithParamsDiv = document.getElementById("linksWithParams"); 5 | const resultsDiv = document.getElementById("results"); 6 | 7 | // --- Helper to escape HTML safely --- 8 | function escapeHTML(str) { 9 | return str 10 | ? str 11 | .replace(/&/g, "&") 12 | .replace(//g, ">") 14 | .replace(/"/g, """) 15 | .replace(/'/g, "'") 16 | : ""; 17 | } 18 | 19 | // --- Get current domain --- 20 | const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 21 | const currentDomain = tab?.url ? new URL(tab.url).hostname : ""; 22 | 23 | // --- TAB SWITCHING --- 24 | tabs.forEach(tab => { 25 | tab.addEventListener("click", () => { 26 | const target = tab.getAttribute("data-tab"); 27 | 28 | tabs.forEach(t => t.classList.remove("active")); 29 | tabContents.forEach(c => c.classList.remove("active")); 30 | 31 | tab.classList.add("active"); 32 | document.getElementById(target).classList.add("active"); 33 | 34 | if (target === "home") { 35 | loadFoundLinks(); 36 | loadHomeSecrets(); 37 | } 38 | if (target === "params") fetchLinksWithParams(); 39 | }); 40 | }); 41 | 42 | // --- Deduplicate helper --- 43 | function dedupeResults(arr) { 44 | const seen = new Set(); 45 | return arr.filter(item => { 46 | const key = `${item.url}|${item.keyword}|${item.lineNum}`; 47 | if (seen.has(key)) return false; 48 | seen.add(key); 49 | return true; 50 | }); 51 | } 52 | 53 | // --- Load Found Keyword Links (current site only) --- 54 | function loadFoundLinks() { 55 | resultsDiv.innerHTML = "Loading..."; 56 | const domainKey = `keywordsFound_${currentDomain}`; 57 | 58 | chrome.storage.local.get(["foundResults", domainKey], (data) => { 59 | const foundAll = [...(data.foundResults || []), ...(data[domainKey] || [])]; 60 | 61 | const found = foundAll.filter(item => { 62 | try { 63 | return new URL(item.url).hostname === currentDomain; 64 | } catch { 65 | return false; 66 | } 67 | }); 68 | 69 | const deduped = dedupeResults(found); 70 | 71 | if (deduped.length === 0) { 72 | resultsDiv.textContent = "No keyword matches found yet."; 73 | return; 74 | } 75 | 76 | resultsDiv.innerHTML = ""; 77 | deduped.forEach(item => { 78 | const div = document.createElement("div"); 79 | div.className = "keyword-item"; 80 | div.style.marginBottom = "10px"; 81 | div.style.background = "#fff"; 82 | div.style.border = "1px solid #ddd"; 83 | div.style.borderRadius = "8px"; 84 | div.style.padding = "8px"; 85 | div.style.boxShadow = "0 1px 3px rgba(0,0,0,0.1)"; 86 | div.innerHTML = ` 87 | ${escapeHTML(item.keyword)}
88 | ${escapeHTML(item.url)}
89 | Line: ${item.lineNum} 90 | `; 91 | resultsDiv.appendChild(div); 92 | }); 93 | }); 94 | } 95 | 96 | // --- Load Secrets in Home Column (current site only) --- 97 | function loadHomeSecrets() { 98 | const secretDiv = document.getElementById("homeSecrets"); 99 | secretDiv.innerHTML = "Loading..."; 100 | const domainKey = `secretsFound_${currentDomain}`; 101 | chrome.storage.local.get([domainKey], (data) => { 102 | const allMatches = data[domainKey] || []; 103 | const matches = allMatches.filter(item => { 104 | try { 105 | const url = item.pageUrl || item.fileUrl || ""; 106 | return new URL(url).hostname === currentDomain; 107 | } catch { 108 | return false; 109 | } 110 | }); 111 | 112 | // Deduplicate secrets also 113 | const deduped = matches.filter( 114 | (v, i, a) => 115 | a.findIndex( 116 | t => 117 | t.name === v.name && 118 | t.match === v.match && 119 | t.fileUrl === v.fileUrl 120 | ) === i 121 | ); 122 | 123 | if (deduped.length === 0) { 124 | secretDiv.textContent = "No secrets found yet."; 125 | return; 126 | } 127 | 128 | let html = ""; 129 | for (const item of deduped) { 130 | const fileName = (() => { 131 | try { 132 | const url = item.fileUrl || item.pageUrl || ""; 133 | return url.split("/").filter(Boolean).pop() || "home"; 134 | } catch { 135 | return "home"; 136 | } 137 | })(); 138 | 139 | let link = item.fileUrl || item.pageUrl || "#"; 140 | try { 141 | // If it's a relative path like /main.js or main.js 142 | const u = new URL(link, tab.url); 143 | link = u.href; 144 | } catch { 145 | // fallback in case something breaks 146 | link = tab.url; 147 | } 148 | 149 | 150 | 151 | html += ` 152 |
160 |
161 | ${escapeHTML(item.name)} 162 |
163 |
174 | ${escapeHTML(item.match)} 175 |
176 |
177 | 179 | ${escapeHTML(fileName)} 180 | 181 |
182 |
`; 183 | } 184 | 185 | secretDiv.innerHTML = html; 186 | }); 187 | } 188 | 189 | // --- Initial Load on Popup Open --- 190 | loadFoundLinks(); 191 | loadHomeSecrets(); 192 | 193 | // --- Refresh Button --- 194 | const refreshBtn = document.createElement("button"); 195 | refreshBtn.textContent = "🔄 Refresh"; 196 | refreshBtn.style.marginBottom = "10px"; 197 | refreshBtn.addEventListener("click", () => { 198 | loadFoundLinks(); 199 | loadHomeSecrets(); 200 | }); 201 | const homeHeader = document.querySelector("#home h2"); 202 | if (homeHeader) homeHeader.insertAdjacentElement("afterend", refreshBtn); 203 | 204 | // --- Settings + Toggles --- 205 | const keywordsInput = document.getElementById("keywords"); 206 | const notifyModeSelect = document.getElementById("notifyMode"); 207 | const maxLinksSelect = document.getElementById("maxLinks"); 208 | const saveBtn = document.getElementById("save"); 209 | const status = document.getElementById("status"); 210 | const showKeywordsBtn = document.getElementById("showKeywords"); 211 | const keywordsListDiv = document.getElementById("keywordsList"); 212 | const clearAllBtn = document.getElementById("clearAll"); 213 | const siteToggle = document.getElementById("siteToggle"); 214 | const siteLabel = document.getElementById("siteLabel"); 215 | 216 | if (tab.url && tab.url.startsWith("http")) { 217 | siteLabel.textContent = `Turn ON for: ${currentDomain}`; 218 | } 219 | 220 | chrome.storage.local.get(["activeSites"], (data) => { 221 | const activeSites = data.activeSites || {}; 222 | siteToggle.checked = !!activeSites[currentDomain]; 223 | }); 224 | 225 | // ✅ Toggle logic (fresh scan trigger) 226 | siteToggle.addEventListener("change", async () => { 227 | const { activeSites = {} } = await chrome.storage.local.get("activeSites"); 228 | 229 | if (siteToggle.checked) { 230 | activeSites[currentDomain] = true; 231 | await chrome.storage.local.set({ activeSites }); 232 | chrome.runtime.sendMessage({ cmd: "resetDomain", domain: currentDomain }); 233 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 234 | if (tabs && tabs[0]) chrome.tabs.reload(tabs[0].id); 235 | }); 236 | showStatus(`🔍 Fresh scan started for ${currentDomain}`); 237 | } else { 238 | delete activeSites[currentDomain]; 239 | await chrome.storage.local.set({ activeSites }); 240 | chrome.runtime.sendMessage({ cmd: "resetDomain", domain: currentDomain }); 241 | showStatus(`❌ Scanning disabled and data cleared.`); 242 | } 243 | }); 244 | 245 | function showStatus(msg) { 246 | status.textContent = msg; 247 | status.className = "success"; 248 | setTimeout(() => (status.textContent = ""), 4000); 249 | } 250 | 251 | // --- Settings save/load --- 252 | chrome.storage.local.get(["keywords", "notifyMode", "maxLinks"], (data) => { 253 | if (data.notifyMode) notifyModeSelect.value = data.notifyMode; 254 | if (data.maxLinks) maxLinksSelect.value = data.maxLinks; 255 | }); 256 | 257 | saveBtn.addEventListener("click", () => { 258 | const newKeywords = keywordsInput.value 259 | ? keywordsInput.value.split(",").map(k => k.trim()).filter(Boolean) 260 | : []; 261 | chrome.storage.local.get(["keywords"], (data) => { 262 | let keywords = data.keywords || []; 263 | newKeywords.forEach(k => { 264 | if (!keywords.includes(k)) keywords.push(k); 265 | }); 266 | chrome.storage.local.set({ 267 | keywords, 268 | notifyMode: notifyModeSelect.value, 269 | maxLinks: maxLinksSelect.value 270 | }, () => { 271 | status.textContent = "Settings saved!"; 272 | status.className = "success"; 273 | keywordsInput.value = ""; 274 | setTimeout(() => (status.textContent = ""), 2000); 275 | }); 276 | }); 277 | }); 278 | 279 | // --- Show/Remove Keywords --- 280 | function displayKeywords() { 281 | chrome.storage.local.get(["keywords"], (data) => { 282 | keywordsListDiv.innerHTML = ""; 283 | if (!data.keywords || data.keywords.length === 0) { 284 | keywordsListDiv.textContent = "No keywords saved."; 285 | return; 286 | } 287 | 288 | const list = document.createElement("ul"); 289 | list.style.paddingLeft = "0"; 290 | list.style.listStyle = "none"; 291 | 292 | data.keywords.forEach((kw) => { 293 | const li = document.createElement("li"); 294 | li.style.display = "flex"; 295 | li.style.justifyContent = "space-between"; 296 | li.style.alignItems = "center"; 297 | li.style.marginBottom = "5px"; 298 | li.style.borderBottom = "1px solid #ccc"; 299 | li.style.padding = "2px 0"; 300 | 301 | const span = document.createElement("span"); 302 | span.textContent = kw; 303 | 304 | const delBtn = document.createElement("span"); 305 | delBtn.textContent = "X"; 306 | delBtn.style.color = "red"; 307 | delBtn.style.cursor = "pointer"; 308 | delBtn.style.marginLeft = "10px"; 309 | delBtn.style.fontWeight = "bold"; 310 | delBtn.title = "Delete keyword"; 311 | 312 | delBtn.addEventListener("click", () => { 313 | const updatedKeywords = data.keywords.filter(k => k !== kw); 314 | chrome.storage.local.set({ keywords: updatedKeywords }, displayKeywords); 315 | }); 316 | 317 | li.appendChild(span); 318 | li.appendChild(delBtn); 319 | list.appendChild(li); 320 | }); 321 | 322 | keywordsListDiv.appendChild(list); 323 | }); 324 | } 325 | showKeywordsBtn.addEventListener("click", displayKeywords); 326 | 327 | // --- Clear All Found Links (current site only) --- 328 | clearAllBtn.addEventListener("click", () => { 329 | chrome.storage.local.get(["foundResults"], (data) => { 330 | const foundAll = data.foundResults || []; 331 | const remaining = foundAll.filter(item => { 332 | try { 333 | return new URL(item.url).hostname !== currentDomain; 334 | } catch { 335 | return true; 336 | } 337 | }); 338 | chrome.storage.local.set({ foundResults: remaining }, () => { 339 | resultsDiv.innerHTML = ""; 340 | status.textContent = "Cleared data for this site!"; 341 | setTimeout(() => (status.textContent = ""), 2000); 342 | }); 343 | }); 344 | }); 345 | 346 | // --- Fetch & Display Links with Params --- 347 | async function fetchLinksWithParams() { 348 | linksWithParamsDiv.innerHTML = "Loading..."; 349 | const [{ result: linksWithParams }] = await chrome.scripting.executeScript({ 350 | target: { tabId: tab.id }, 351 | func: () => { 352 | const baseDomain = window.location.hostname; 353 | const result = []; 354 | document.querySelectorAll("a[href]").forEach(a => { 355 | try { 356 | const urlObj = new URL(a.href, window.location.origin); 357 | if (urlObj.hostname === baseDomain && urlObj.search) 358 | result.push(urlObj.href); 359 | } catch {} 360 | }); 361 | return [...new Set(result)]; 362 | } 363 | }); 364 | 365 | if (!linksWithParams || linksWithParams.length === 0) { 366 | linksWithParamsDiv.textContent = "No internal links with parameters found."; 367 | return; 368 | } 369 | 370 | const list = document.createElement("ul"); 371 | linksWithParams.forEach(link => { 372 | const li = document.createElement("li"); 373 | li.innerHTML = `${link}`; 374 | list.appendChild(li); 375 | }); 376 | linksWithParamsDiv.innerHTML = ""; 377 | linksWithParamsDiv.appendChild(list); 378 | } 379 | 380 | // --- Live auto-refresh --- 381 | chrome.storage.onChanged.addListener((changes) => { 382 | if (changes.foundResults) loadFoundLinks(); 383 | }); 384 | }); 385 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | // background.js — full working version (keywords + secrets + live refresh) 2 | // UPDATED: fixes inflated secret notification counts 3 | 4 | const scannedMap = new Map(); 5 | 6 | // --- Load regex list for secret patterns --- 7 | async function loadRegexList() { 8 | try { 9 | const url = chrome.runtime.getURL("regax.txt"); 10 | const r = await fetch(url); 11 | const txt = await r.text(); 12 | try { 13 | const parsed = JSON.parse(txt); 14 | if (Array.isArray(parsed)) return parsed.map(e => [e.name, e.pattern]); 15 | else return Object.entries(parsed); 16 | } catch (e) { 17 | console.error("Failed to parse regax.txt as JSON", e); 18 | return []; 19 | } 20 | } catch (err) { 21 | console.error("Failed to fetch regax.txt", err); 22 | return []; 23 | } 24 | } 25 | 26 | // --- Deduplicate found results (merge helper) --- 27 | function dedupeAndMerge(prev = [], found = []) { 28 | const out = prev.slice(); 29 | for (const f of found) { 30 | const exists = out.some(p => 31 | p.name === f.name && 32 | p.match === f.match && 33 | p.pageUrl === f.pageUrl && 34 | p.fileUrl === f.fileUrl 35 | ); 36 | if (!exists) out.push(f); 37 | } 38 | return out; 39 | } 40 | 41 | // --- Track which domains/tabs have been scanned --- 42 | function markScanned(domain, tabId) { 43 | const set = scannedMap.get(domain) || new Set(); 44 | set.add(tabId); 45 | scannedMap.set(domain, set); 46 | } 47 | function isScanned(domain, tabId) { 48 | const set = scannedMap.get(domain); 49 | return set ? set.has(tabId) : false; 50 | } 51 | chrome.tabs.onRemoved.addListener(tabId => { 52 | for (const [domain, set] of scannedMap.entries()) { 53 | if (set.has(tabId)) { 54 | set.delete(tabId); 55 | if (set.size === 0) scannedMap.delete(domain); 56 | } 57 | } 58 | }); 59 | 60 | // --- Main scanning trigger --- 61 | chrome.webNavigation.onCompleted.addListener(async details => { 62 | try { 63 | if (details.frameId !== 0 || !details.url.startsWith("http")) return; 64 | 65 | const domain = new URL(details.url).hostname; 66 | const tabId = details.tabId; 67 | 68 | const data = await new Promise(res => chrome.storage.local.get( 69 | ["keywords", "notifyMode", "foundResults", "maxLinks", "activeSites"], 70 | res 71 | )); 72 | 73 | const keywords = data.keywords || []; 74 | const notifyMode = data.notifyMode || "notification"; 75 | const maxLinks = data.maxLinks || "10"; 76 | const activeSites = data.activeSites || {}; 77 | let foundResults = data.foundResults || []; 78 | 79 | if (!activeSites[domain]) { 80 | // if toggle is OFF → clear site data and skip 81 | await new Promise(r => chrome.storage.local.remove(`secretsFound_${domain}`, r)); 82 | return; 83 | } 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | // --- KEYWORD SCANNING --- 93 | try { 94 | if (keywords.length > 0) { // ✅ Always scan, even if notifications are disabled 95 | const [{ result: scanResult }] = await chrome.scripting.executeScript({ 96 | target: { tabId }, 97 | func: async (keywordsArg, maxLinksArg) => { 98 | const origin = location.origin; 99 | const visited = new Set(); 100 | const toVisit = [location.href]; 101 | const results = []; 102 | const SAFETY_CAP = 1000; 103 | const limit = maxLinksArg === "all" ? SAFETY_CAP : parseInt(maxLinksArg, 10) || 10; 104 | 105 | async function fetchText(url) { 106 | try { 107 | const r = await fetch(url, { credentials: "include" }); 108 | if (!r.ok) return ""; 109 | return await r.text(); 110 | } catch { 111 | return ""; 112 | } 113 | } 114 | 115 | function extractLinksAndScripts(html, baseUrl) { 116 | const parser = new DOMParser(); 117 | const doc = parser.parseFromString(html, "text/html"); 118 | const links = Array.from(doc.querySelectorAll("a[href]")) 119 | .map(a => { 120 | try { return new URL(a.href, baseUrl).href; } catch { return null; } 121 | }) 122 | .filter(Boolean) 123 | .filter(u => u.startsWith(origin)); 124 | const scripts = Array.from(doc.querySelectorAll("script[src]")) 125 | .map(s => { 126 | try { return new URL(s.src, baseUrl).href; } catch { return null; } 127 | }) 128 | .filter(Boolean) 129 | .filter(u => u.startsWith(origin)); 130 | return { links: [...new Set(links)], scripts: [...new Set(scripts)] }; 131 | } 132 | 133 | function recordMatches(text, url) { 134 | for (const kw of keywordsArg) { 135 | if (text.toLowerCase().includes(kw.toLowerCase())) { 136 | const lines = text.split("\n"); 137 | lines.forEach((line, i) => { 138 | if (line.toLowerCase().includes(kw.toLowerCase())) 139 | results.push({ keyword: kw, url, line: line.trim(), lineNum: i + 1 }); 140 | }); 141 | } 142 | } 143 | } 144 | 145 | while (toVisit.length && visited.size < limit && visited.size < SAFETY_CAP) { 146 | const pageUrl = toVisit.shift(); 147 | if (!pageUrl || visited.has(pageUrl)) continue; 148 | visited.add(pageUrl); 149 | 150 | const html = await fetchText(pageUrl); 151 | if (!html) continue; 152 | recordMatches(html, pageUrl); 153 | 154 | const { links, scripts } = extractLinksAndScripts(html, pageUrl); 155 | 156 | for (const js of scripts) { 157 | try { 158 | const jsText = await fetchText(js); 159 | if (jsText) recordMatches(jsText, js); 160 | } catch (e) { 161 | console.warn("⚠️ JS fetch failed:", js, e); 162 | } 163 | } 164 | 165 | for (const l of links) 166 | if (!visited.has(l) && !toVisit.includes(l)) toVisit.push(l); 167 | } 168 | 169 | return results; 170 | }, 171 | args: [keywords, maxLinks] 172 | }); 173 | 174 | const matches = Array.isArray(scanResult) ? scanResult : []; 175 | 176 | if (matches.length > 0) { 177 | foundResults = dedupeAndMerge(foundResults, matches); 178 | const domainKey = `keywordsFound_${domain}`; 179 | await chrome.storage.local.set({ 180 | foundResults, 181 | [domainKey]: matches 182 | }); 183 | 184 | chrome.runtime.sendMessage({ cmd: "refreshKeywords", domain }); 185 | 186 | // ✅ Only show notifications if enabled 187 | if (notifyMode !== "disabled") { 188 | let msg = matches.slice(0, 3).map(m => `${m.keyword} @ ${m.url}`).join("\n"); 189 | if (matches.length > 3) msg += `\n+${matches.length - 3} more...`; 190 | 191 | if (notifyMode === "notification") { 192 | chrome.notifications.create({ 193 | type: "basic", 194 | iconUrl: "icon.png", 195 | title: "HTML_search keywords found", 196 | message: msg, 197 | priority: 2, 198 | requireInteraction: true 199 | }); 200 | } else if (notifyMode === "alert") { 201 | chrome.scripting.executeScript({ 202 | target: { tabId }, 203 | func: msg => alert("HTML_search:\n" + msg), 204 | args: [msg] 205 | }); 206 | } 207 | } 208 | } 209 | } 210 | } catch (err) { 211 | console.error("Keyword scan error:", err); 212 | } 213 | 214 | // --- SECRET SCANNING (fixed counting) --- 215 | try { 216 | if (isScanned(domain, tabId)) return; 217 | 218 | // NOTE: DO NOT remove the stored secrets here — only clear when user toggles reset. 219 | const regexEntries = await loadRegexList(); 220 | if (!regexEntries.length) { 221 | markScanned(domain, tabId); 222 | return; 223 | } 224 | 225 | const [{ result: scanResult }] = await chrome.scripting.executeScript({ 226 | target: { tabId }, 227 | func: async (regexEntriesArg, maxLinksArg) => { 228 | async function fetchText(url) { 229 | try { 230 | const r = await fetch(url, { credentials: "include" }); 231 | if (!r.ok) return ""; 232 | return await r.text(); 233 | } catch { 234 | return ""; 235 | } 236 | } 237 | 238 | function extractLinksAndScripts(html, baseUrl) { 239 | const parser = new DOMParser(); 240 | const doc = parser.parseFromString(html, "text/html"); 241 | const links = Array.from(doc.querySelectorAll("a[href]")) 242 | .map(a => { 243 | try { return new URL(a.href, baseUrl).href; } catch { return null; } 244 | }) 245 | .filter(Boolean) 246 | .filter(u => u.startsWith(location.origin)); 247 | const scripts = Array.from(doc.querySelectorAll("script[src]")) 248 | .map(s => { 249 | try { return new URL(s.src, baseUrl).href; } catch { return null; } 250 | }) 251 | .filter(Boolean) 252 | .filter(u => u.startsWith(location.origin)); 253 | return { links: [...new Set(links)], scripts: [...new Set(scripts)] }; 254 | } 255 | 256 | const SAFETY_CAP = 1000; 257 | const visited = new Set(); 258 | const toVisit = [location.href]; 259 | const results = []; 260 | const limit = maxLinksArg === "all" ? SAFETY_CAP : parseInt(maxLinksArg, 10) || 10; 261 | 262 | while (toVisit.length && visited.size < limit && visited.size < SAFETY_CAP) { 263 | const pageUrl = toVisit.shift(); 264 | if (!pageUrl || visited.has(pageUrl)) continue; 265 | visited.add(pageUrl); 266 | 267 | const html = await fetchText(pageUrl); 268 | if (!html) continue; 269 | 270 | for (const [name, pattern] of regexEntriesArg) { 271 | let re; try { re = new RegExp(pattern, "gi"); } catch { continue; } 272 | let m; 273 | while ((m = re.exec(html)) !== null) { 274 | results.push({ name, match: m[0], pageUrl, fileUrl: pageUrl }); 275 | if (m.index === re.lastIndex) re.lastIndex++; 276 | } 277 | } 278 | 279 | const { links, scripts } = extractLinksAndScripts(html, pageUrl); 280 | 281 | for (const js of scripts) { 282 | try { 283 | const jsText = await fetchText(js); 284 | for (const [name, pattern] of regexEntriesArg) { 285 | let re; try { re = new RegExp(pattern, "gi"); } catch { continue; } 286 | let m; 287 | while ((m = re.exec(jsText)) !== null) { 288 | results.push({ name, match: m[0], pageUrl, fileUrl: js }); 289 | if (m.index === re.lastIndex) re.lastIndex++; 290 | } 291 | } 292 | } catch {} 293 | } 294 | 295 | for (const l of links) 296 | if (!visited.has(l) && !toVisit.includes(l)) toVisit.push(l); 297 | } 298 | 299 | return results; 300 | }, 301 | args: [regexEntries, maxLinks] 302 | }); 303 | 304 | // Normalize foundSecrets (array) 305 | let foundSecrets = Array.isArray(scanResult) ? scanResult : []; 306 | 307 | // Deduplicate the fresh scan results first (by name+match+fileUrl) 308 | foundSecrets = dedupeAndMerge([], foundSecrets); 309 | 310 | const domainKey = `secretsFound_${domain}`; 311 | const prevData = await new Promise(res => chrome.storage.local.get(domainKey, res)); 312 | const oldSecrets = prevData[domainKey] || []; 313 | 314 | // Build list of truly new unique secrets (not present in oldSecrets) 315 | const uniqueNewSecrets = foundSecrets.filter(f => { 316 | return !oldSecrets.some(o => 317 | o.name === f.name && o.match === f.match && o.fileUrl === f.fileUrl 318 | ); 319 | }); 320 | 321 | // Merge and persist (old + new unique ones) 322 | const merged = dedupeAndMerge(oldSecrets, foundSecrets); 323 | await chrome.storage.local.set({ [domainKey]: merged }); 324 | 325 | // Use count of truly new unique secrets for notifications 326 | const newCount = uniqueNewSecrets.length; 327 | 328 | if (newCount > 0) { 329 | const msg = `${newCount} new unique secret(s) found on ${domain}`; 330 | if (notifyMode === "notification") { 331 | chrome.notifications.create({ 332 | type: "basic", 333 | iconUrl: "icon.png", 334 | title: "🔑 Secrets Found", 335 | message: msg, 336 | priority: 2 337 | }); 338 | } else if (notifyMode === "alert") { 339 | chrome.scripting.executeScript({ 340 | target: { tabId }, 341 | func: msg => alert(msg), 342 | args: [msg] 343 | }); 344 | } 345 | } 346 | 347 | markScanned(domain, tabId); 348 | } 349 | catch (err) { 350 | console.error("Secret scan error:", err); 351 | markScanned(domain, tabId); 352 | } 353 | } catch (err) { 354 | console.error("background onCompleted error:", err); 355 | } 356 | }); 357 | 358 | // --- Reset domain scan --- 359 | function resetDomainScan(domain) { 360 | if (scannedMap.has(domain)) scannedMap.delete(domain); 361 | const domainKey = `secretsFound_${domain}`; 362 | const keyKw = `keywordsFound_${domain}`; 363 | chrome.storage.local.remove([domainKey, keyKw]); 364 | chrome.storage.local.get(["foundResults"], d => { 365 | const found = d.foundResults || []; 366 | const filtered = found.filter(f => { 367 | try { return new URL(f.url).hostname !== domain; } catch { return true; } 368 | }); 369 | chrome.storage.local.set({ foundResults: filtered }); 370 | }); 371 | console.log(`🔄 Reset complete for ${domain}`); 372 | } 373 | 374 | chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { 375 | if (msg?.cmd === "resetDomain" && msg.domain) { 376 | resetDomainScan(msg.domain); 377 | sendResponse({ ok: true }); 378 | return true; 379 | } 380 | }); 381 | --------------------------------------------------------------------------------