Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>GitHub KeyHunter - Automated API Key Scanner</title> | |
| <link rel="icon" type="image/x-icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔑</text></svg>"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/animejs/lib/anime.iife.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); | |
| body { font-family: 'Inter', sans-serif; } | |
| .gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } | |
| .glass-effect { backdrop-filter: blur(10px); background: rgba(255, 255, 255, 0.1); } | |
| .scan-animation { animation: pulse 2s infinite; } | |
| @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } | |
| .key-card { transition: all 0.3s ease; } | |
| .key-card:hover { transform: translateY(-5px); box-shadow: 0 20px 40px rgba(0,0,0,0.1); } | |
| .status-active { background: linear-gradient(90deg, #10b981, #059669); } | |
| .status-inactive { background: linear-gradient(90deg, #ef4444, #dc2626); } | |
| .status-testing { background: linear-gradient(90deg, #f59e0b, #d97706); } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-white min-h-screen"> | |
| <div id="vanta-bg" class="fixed inset-0 z-0"></div> | |
| <nav class="relative z-10 glass-effect border-b border-gray-700"> | |
| <div class="container mx-auto px-6 py-4"> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center space-x-3"> | |
| <i data-feather="key" class="w-8 h-8 text-purple-400"></i> | |
| <h1 class="text-2xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">GitHub KeyHunter</h1> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button id="startScanBtn" class="bg-purple-600 hover:bg-purple-700 px-6 py-2 rounded-lg font-medium transition-all duration-300 flex items-center space-x-2"> | |
| <i data-feather="play" class="w-4 h-4"></i> | |
| <span>Start Scan</span> | |
| </button> | |
| <button id="settingsBtn" class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg transition-all duration-300"> | |
| <i data-feather="settings" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </nav> | |
| <main class="relative z-10 container mx-auto px-6 py-8"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8"> | |
| <div class="glass-effect rounded-xl p-6 border border-gray-700"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h3 class="text-lg font-semibold">Total Scanned</h3> | |
| <i data-feather="search" class="w-6 h-6 text-blue-400"></i> | |
| </div> | |
| <div class="text-3xl font-bold text-blue-400" id="totalScanned">0</div> | |
| <div class="text-sm text-gray-400 mt-2">Repositories</div> | |
| </div> | |
| <div class="glass-effect rounded-xl p-6 border border-gray-700"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h3 class="text-lg font-semibold">Keys Found</h3> | |
| <i data-feather="key" class="w-6 h-6 text-yellow-400"></i> | |
| </div> | |
| <div class="text-3xl font-bold text-yellow-400" id="keysFound">0</div> | |
| <div class="text-sm text-gray-400 mt-2">Potential API Keys</div> | |
| </div> | |
| <div class="glass-effect rounded-xl p-6 border border-gray-700"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h3 class="text-lg font-semibold">Valid Keys</h3> | |
| <i data-feather="check-circle" class="w-6 h-6 text-green-400"></i> | |
| </div> | |
| <div class="text-3xl font-bold text-green-400" id="validKeys">0</div> | |
| <div class="text-sm text-gray-400 mt-2">Working Keys</div> | |
| </div> | |
| </div> | |
| <div class="glass-effect rounded-xl p-6 border border-gray-700 mb-8"> | |
| <div class="flex items-center justify-between mb-6"> | |
| <h2 class="text-xl font-bold">Scan Progress</h2> | |
| <div class="flex items-center space-x-4"> | |
| <span id="scanStatus" class="px-3 py-1 rounded-full text-sm font-medium bg-gray-700">Idle</span> | |
| <button id="pauseScanBtn" class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg transition-all duration-300 hidden"> | |
| <i data-feather="pause" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <div class="flex justify-between text-sm mb-2"> | |
| <span>Progress</span> | |
| <span id="progressPercent">0%</span> | |
| </div> | |
| <div class="w-full bg-gray-700 rounded-full h-3 overflow-hidden"> | |
| <div id="progressBar" class="bg-gradient-to-r from-purple-500 to-pink-500 h-full rounded-full transition-all duration-300" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm"> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-400">Current Repository:</span> | |
| <span id="currentRepo" class="font-mono truncate ml-2">-</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-400">Scan Rate:</span> | |
| <span id="scanRate" class="font-mono">0 repos/min</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="glass-effect rounded-xl p-6 border border-gray-700"> | |
| <div class="flex items-center justify-between mb-6"> | |
| <h2 class="text-xl font-bold">Discovered API Keys</h2> | |
| <div class="flex items-center space-x-3"> | |
| <select id="filterStatus" class="bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-sm focus:outline-none focus:border-purple-500"> | |
| <option value="all">All Keys</option> | |
| <option value="valid">Valid</option> | |
| <option value="testing">Testing</option> | |
| <option value="invalid">Invalid</option> | |
| </select> | |
| <button id="exportBtn" class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg text-sm transition-all duration-300"> | |
| <i data-feather="download" class="w-4 h-4 inline mr-2"></i>Export | |
| </button> | |
| </div> | |
| </div> | |
| <div id="keysContainer" class="space-y-3 max-h-96 overflow-y-auto"> | |
| <div class="text-center py-8 text-gray-400"> | |
| <i data-feather="inbox" class="w-12 h-12 mx-auto mb-3"></i> | |
| <p>No keys found yet. Start scanning to discover API keys!</p> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <div id="settingsModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center p-4"> | |
| <div class="glass-effect rounded-xl p-6 max-w-md w-full border border-gray-700"> | |
| <h3 class="text-xl font-bold mb-4">Scan Settings</h3> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium mb-2">GitHub Token (Optional)</label> | |
| <input type="password" id="githubToken" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 focus:outline-none focus:border-purple-500" placeholder="ghp_xxxxxxxxxxxx"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-2">Max Repositories per Scan</label> | |
| <input type="number" id="maxRepos" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 focus:outline-none focus:border-purple-500" value="100" min="10" max="1000"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-2">Delay between requests (ms)</label> | |
| <input type="number" id="requestDelay" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 focus:outline-none focus:border-purple-500" value="1000" min="100" max="5000"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-2">Search Query</label> | |
| <input type="text" id="searchQuery" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 focus:outline-none focus:border-purple-500" value="API_KEY OR api_key OR token OR secret" placeholder="Search terms"> | |
| </div> | |
| </div> | |
| <div class="flex justify-end space-x-3 mt-6"> | |
| <button id="cancelSettings" class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg transition-all duration-300">Cancel</button> | |
| <button id="saveSettings" class="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-lg transition-all duration-300">Save</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.net.min.js"></script> | |
| <script> | |
| VANTA.NET({ | |
| el: "#vanta-bg", | |
| mouseControls: true, | |
| touchControls: true, | |
| gyroControls: false, | |
| minHeight: 200.00, | |
| minWidth: 200.00, | |
| scale: 1.00, | |
| scaleMobile: 1.00, | |
| color: 0x667eea, | |
| backgroundColor: 0x111827, | |
| points: 8.00, | |
| maxDistance: 25.00, | |
| spacing: 18.00 | |
| }); | |
| feather.replace(); | |
| // Global variables | |
| let isScanning = false; | |
| let isPaused = false; | |
| let scannedRepos = 0; | |
| let foundKeys = []; | |
| let validKeys = []; | |
| let currentPage = 1; | |
| let scanInterval; | |
| // DOM elements | |
| const startScanBtn = document.getElementById('startScanBtn'); | |
| const pauseScanBtn = document.getElementById('pauseScanBtn'); | |
| const settingsBtn = document.getElementById('settingsBtn'); | |
| const settingsModal = document.getElementById('settingsModal'); | |
| const cancelSettings = document.getElementById('cancelSettings'); | |
| const saveSettings = document.getElementById('saveSettings'); | |
| const exportBtn = document.getElementById('exportBtn'); | |
| const filterStatus = document.getElementById('filterStatus'); | |
| const keysContainer = document.getElementById('keysContainer'); | |
| // Settings | |
| let settings = { | |
| githubToken: '', | |
| maxRepos: 100, | |
| requestDelay: 1000, | |
| searchQuery: 'API_KEY OR api_key OR token OR secret' | |
| }; | |
| // API Key patterns | |
| const apiPatterns = { | |
| 'aws': /AKIA[0-9A-Z]{16}/, | |
| 'github': /ghp_[0-9a-zA-Z]{36}/, | |
| 'google': /AIza[0-9A-Za-z\\-_]{35}/, | |
| 'slack': /xox[baprs]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32}/, | |
| 'stripe': /sk_live_[0-9a-zA-Z]{24}/, | |
| 'sendgrid': /SG\.[0-9A-Za-z\-_]{22}\.[0-9A-Za-z\-_]{43}/, | |
| 'mailgun': /key-[0-9a-zA-Z]{32}/, | |
| 'twilio': /SK[0-9a-fA-F]{32}/, | |
| 'generic': /(?:api[_-]?key|apikey|token|secret)[\s:=]+["']?([a-zA-Z0-9\-_]{20,})["']?/i | |
| }; | |
| // Event listeners | |
| startScanBtn.addEventListener('click', () => { | |
| if (isScanning) { | |
| stopScan(); | |
| } else { | |
| startScan(); | |
| } | |
| }); | |
| pauseScanBtn.addEventListener('click', togglePause); | |
| settingsBtn.addEventListener('click', () => settingsModal.classList.remove('hidden')); | |
| cancelSettings.addEventListener('click', () => settingsModal.classList.add('hidden')); | |
| saveSettings.addEventListener('click', saveSettingsHandler); | |
| exportBtn.addEventListener('click', exportKeys); | |
| filterStatus.addEventListener('change', filterKeys); | |
| async function startScan() { | |
| if (isScanning) return; | |
| isScanning = true; | |
| isPaused = false; | |
| scannedRepos = 0; | |
| foundKeys = []; | |
| validKeys = []; | |
| currentPage = 1; | |
| updateUI(); | |
| startScanBtn.innerHTML = '<i data-feather="stop" class="w-4 h-4"></i><span>Stop Scan</span>'; | |
| pauseScanBtn.classList.remove('hidden'); | |
| feather.replace(); | |
| await scanGitHub(); | |
| } | |
| async function scanGitHub() { | |
| const scanStatus = document.getElementById('scanStatus'); | |
| const currentRepo = document.getElementById('currentRepo'); | |
| const scanRate = document.getElementById('scanRate'); | |
| scanStatus.textContent = 'Scanning'; | |
| scanStatus.className = 'px-3 py-1 rounded-full text-sm font-medium status-testing'; | |
| const startTime = Date.now(); | |
| while (isScanning && scannedRepos < settings.maxRepos) { | |
| if (isPaused) { | |
| await new Promise(resolve => setTimeout(resolve, 100)); | |
| continue; | |
| } | |
| try { | |
| // Search for repositories | |
| const searchUrl = `https://api.github.com/search/repositories?q=${encodeURIComponent(settings.searchQuery)}&sort=updated&order=desc&per_page=30&page=${currentPage}`; | |
| const headers = settings.githubToken ? { 'Authorization': `token ${settings.githubToken}` } : {}; | |
| const response = await axios.get(searchUrl, { headers }); | |
| const repos = response.data.items; | |
| if (repos.length === 0) { | |
| currentPage = 1; // Reset to first page if no more results | |
| continue; | |
| } | |
| for (const repo of repos) { | |
| if (!isScanning || isPaused) break; | |
| currentRepo.textContent = repo.full_name; | |
| await scanRepository(repo, headers); | |
| scannedRepos++; | |
| updateUI(); | |
| // Calculate scan rate | |
| const elapsedMinutes = (Date.now() - startTime) / 60000; | |
| const rate = Math.round(scannedRepos / elapsedMinutes); | |
| scanRate.textContent = `${rate} repos/min`; | |
| await delay(settings.requestDelay); | |
| } | |
| currentPage++; | |
| } catch (error) { | |
| console.error('Error scanning GitHub:', error); | |
| if (error.response && error.response.status === 403) { | |
| scanStatus.textContent = 'Rate Limited'; | |
| scanStatus.className = 'px-3 py-1 rounded-full text-sm font-medium bg-red-600'; | |
| await delay(60000); // Wait 1 minute on rate limit | |
| } | |
| } | |
| } | |
| if (isScanning) { | |
| completeScan(); | |
| } | |
| } | |
| async function scanRepository(repo, headers) { | |
| try { | |
| // Get repository contents | |
| const contentsUrl = `https://api.github.com/repos/${repo.full_name}/git/trees/${repo.default_branch}?recursive=1`; | |
| const contentsResponse = await axios.get(contentsUrl, { headers }); | |
| const files = contentsResponse.data.tree.filter(file => file.type === 'blob'); | |
| // Scan files for API keys | |
| for (const file of files.slice(0, 10)) { // Limit to 10 files per repo | |
| if (!isScanning || isPaused) break; | |
| try { | |
| const fileUrl = `https://api.github.com/repos/${repo.full_name}/git/blobs/${file.sha}`; | |
| const fileResponse = await axios.get(fileUrl, { headers }); | |
| const content = atob(fileResponse.data.content); | |
| scanContentForKeys(content, repo.full_name, file.path); | |
| } catch (error) { | |
| console.error(`Error scanning file ${file.path}:`, error); | |
| } | |
| await delay(100); | |
| } | |
| } catch (error) { | |
| console.error(`Error accessing repository ${repo.full_name}:`, error); | |
| } | |
| } | |
| function scanContentForKeys(content, repoName, filePath) { | |
| for (const [provider, pattern] of Object.entries(apiPatterns)) { | |
| const matches = content.match(new RegExp(pattern, 'g')); | |
| if (matches) { | |
| matches.forEach(match => { | |
| const key = { | |
| id: Date.now() + Math.random(), | |
| key: match, | |
| provider: provider, | |
| repo: repoName, | |
| file: filePath, | |
| status: 'testing', | |
| discoveredAt: new Date().toISOString() | |
| }; | |
| foundKeys.push(key); | |
| testKey(key); | |
| updateKeysDisplay(); | |
| }); | |
| } | |
| } | |
| } | |
| async function testKey(key) { | |
| // Simulate key testing with different providers | |
| setTimeout(() => { | |
| // Randomly determine if key is valid (70% chance for demo) | |
| key.status = Math.random() > 0.3 ? 'valid' : 'invalid'; | |
| if (key.status === 'valid') { | |
| validKeys.push(key); | |
| } | |
| updateUI(); | |
| updateKeysDisplay(); | |
| }, Math.random() * 5000 + 2000); | |
| } | |
| function updateUI() { | |
| document.getElementById('totalScanned').textContent = scannedRepos; | |
| document.getElementById('keysFound').textContent = foundKeys.length; | |
| document.getElementById('validKeys').textContent = validKeys.length; | |
| const progress = Math.min((scannedRepos / settings.maxRepos) * 100, 100); | |
| document.getElementById('progressPercent').textContent = `${Math.round(progress)}%`; | |
| document.getElementById('progressBar').style.width = `${progress}%`; | |
| } | |
| function updateKeysDisplay() { | |
| const filter = filterStatus.value; | |
| const filteredKeys = foundKeys.filter(key => | |
| filter === 'all' || key.status === filter | |
| ); | |
| if (filteredKeys.length === 0) { | |
| keysContainer.innerHTML = ` | |
| <div class="text-center py-8 text-gray-400"> | |
| <i data-feather="inbox" class="w-12 h-12 mx-auto mb-3"></i> | |
| <p>No keys found${filter !== 'all' ? ` with status: ${filter}` : ''}</p> | |
| </div> | |
| `; | |
| feather.replace(); | |
| return; | |
| } | |
| keysContainer.innerHTML = filteredKeys.map(key => ` | |
| <div class="key-card bg-gray-800 rounded-lg p-4 border border-gray-700 hover:border-purple-500"> | |
| <div class="flex items-start justify-between mb-3"> | |
| <div class="flex items-center space-x-3"> | |
| <span class="px-2 py-1 rounded text-xs font-medium bg-purple-600">${key.provider}</span> | |
| <span class="px-2 py-1 rounded text-xs font-medium ${ | |
| key.status === 'valid' ? 'status-active' : | |
| key.status === 'invalid' ? 'status-inactive' : 'status-testing' | |
| }">${key.status}</span> | |
| </div> | |
| <button onclick="copyKey('${key.key.replace(/'/g, "\\'")}')" class="text-gray-400 hover:text-white"> | |
| <i data-feather="copy" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| <div class="font-mono text-sm bg-gray-900 rounded p-2 mb-2 break-all">${key.key}</div> | |
| <div class="text-xs text-gray-400"> | |
| <div class="flex items-center space-x-4"> | |
| <span><i data-feather="github" class="w-3 h-3 inline mr-1"></i>${key.repo}</span> | |
| <span><i data-feather="file" class="w-3 h-3 inline mr-1"></i>${key.file.split('/').pop()}</span> | |
| </div> | |
| <div class="mt-1"><i data-feather="clock" class="w-3 h-3 inline mr-1"></i>${new Date(key.discoveredAt).toLocaleString()}</div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| feather.replace(); | |
| } | |
| function filterKeys() { | |
| updateKeysDisplay(); | |
| } | |
| function copyKey(key) { | |
| navigator.clipboard.writeText(key).then(() => { | |
| Swal.fire({ | |
| icon: 'success', | |
| title: 'Key Copied!', | |
| text: 'API key copied to clipboard', | |
| timer: 1500, | |
| showConfirmButton: false | |
| }); | |
| }); | |
| } | |
| function exportKeys() { | |
| const validOnly = validKeys.map(key => ({ | |
| key: key.key, | |
| provider: key.provider, | |
| repo: key.repo, | |
| file: key.file, | |
| discoveredAt: key.discoveredAt | |
| })); | |
| const dataStr = JSON.stringify(validOnly, null, 2); | |
| const dataBlob = new Blob([dataStr], { type: 'application/json' }); | |
| const url = URL.createObjectURL(dataBlob); | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.download = `api-keys-${new Date().toISOString().split('T')[0]}.json`; | |
| link.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| function togglePause() { | |
| isPaused = !isPaused; | |
| pauseScanBtn.innerHTML = isPaused ? | |
| '<i data-feather="play" class="w-4 h-4"></i>' : | |
| '<i data-feather="pause" class="w-4 h-4"></i>'; | |
| feather.replace(); | |
| } | |
| isScanning = false; | |
| isPaused = false; | |
| const scanStatus = document.getElementById('scanStatus'); | |
| scanStatus.textContent = 'Completed'; | |
| scanStatus.className = 'px-3 py-1 rounded-full text-sm font-medium status-active'; | |
| startScanBtn.innerHTML = '<i data-feather="play" class="w-4 h-4"></i><span>Start Scan</span>'; | |
| pauseScanBtn.classList.add('hidden'); | |
| feather.replace(); | |
| Swal.fire({ | |
| icon: 'success', | |
| title: 'Scan Complete!', | |
| text: `Scanned ${scannedRepos} repositories and found ${foundKeys.length} potential API keys (${validKeys.length} valid)`, | |
| confirmButtonColor: '#667eea' | |
| }); | |
| } | |
| function stopScan() { | |
| isScanning = false; | |
| isPaused = false; | |
| const scanStatus = document.getElementById('scanStatus'); | |
| scanStatus.textContent = 'Stopped'; | |
| scanStatus.className = 'px-3 py-1 rounded-full text-sm font-medium bg-gray-600'; | |
| startScanBtn.innerHTML = '<i data-feather="play" class="w-4 h-4"></i><span>Start Scan</span>'; | |
| pauseScanBtn.classList.add('hidden'); | |
| feather.replace(); | |
| } | |
| function saveSettingsHandler() { | |
| settings.githubToken = document.getElementById('githubToken').value; | |
| settings.maxRepos = parseInt(document.getElementById('maxRepos').value); | |
| settings.requestDelay = parseInt(document.getElementById('requestDelay').value); | |
| settings.searchQuery = document.getElementById('searchQuery').value; | |
| settingsModal.classList.add('hidden'); | |
| Swal.fire({ | |
| icon: 'success', | |
| title: 'Settings Saved!', | |
| text: 'Your scan settings have been updated', | |
| timer: 1500, | |
| showConfirmButton: false | |
| }); | |
| } | |
| function delay(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| updateUI(); | |
| updateKeysDisplay(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |