mirror of
https://github.com/momenbasel/keyFinder.git
synced 2026-04-23 04:56:21 +02:00
v2.0.0: Complete rewrite - Manifest V3, enterprise-grade secret detection
- Migrated to Chrome Manifest V3 with service worker architecture - 80+ secret detection patterns covering AWS, GCP, Azure, GitHub, GitLab, Stripe, Slack, Discord, OpenAI, and 30+ other providers - 10 scanning surfaces: inline scripts, external scripts, meta tags, hidden inputs, data attributes, HTML comments, URL params, web storage, cookies, and network response interception - Shannon entropy analysis for detecting undocumented secret formats - MAIN world interceptor for XHR/fetch response scanning and window globals - Professional dark-theme UI with filtering, search, and CSV/JSON export - Zero dependencies - removed jQuery, Bootstrap, font-awesome, popper - Proper XSS-safe DOM rendering throughout - Badge counter on extension icon showing finding count - All frames scanning including iframes
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
*.crx
|
||||
*.pem
|
||||
*.zip
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.claude/
|
||||
@@ -1,50 +1,141 @@
|
||||
<p align="center">
|
||||
<img width="460" height="300" alt="logo of keyfinder" src="https://raw.githubusercontent.com/momenbasel/keyFinder/master/css/icon.png">
|
||||
</p>
|
||||
<hr></hr>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Chrome-red.svg"/>
|
||||
<img src="https://img.shields.io/github/license/momenbasel/keyFinder"/>
|
||||
<img src="https://img.shields.io/github/downloads/momenbasel/keyFinder/total.svg"/>
|
||||
<img src="https://img.shields.io/badge/demo-youtube-blue.svg"/>
|
||||
<img width="128" height="128" alt="KeyFinder logo" src="https://raw.githubusercontent.com/momenbasel/keyFinder/master/icons/icon128.png">
|
||||
</p>
|
||||
|
||||
# What is keyFinder?
|
||||
keyFinder is chrome extension that searches the DOM for any embedded script link, as script tag may contain keys for specific API(such as Google maps API) and you can add keywords to search for it at any website you visit.
|
||||
<h1 align="center">KeyFinder</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Passive API key and secret discovery for Chrome</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/manifest-v3-blue"/>
|
||||
<img src="https://img.shields.io/badge/Chrome-Extension-green"/>
|
||||
<img src="https://img.shields.io/github/license/momenbasel/keyFinder"/>
|
||||
<img src="https://img.shields.io/github/v/release/momenbasel/keyFinder"/>
|
||||
<img src="https://img.shields.io/github/downloads/momenbasel/keyFinder/total.svg"/>
|
||||
</p>
|
||||
|
||||
## Features:
|
||||
* it searches the DOM for "src" of scripts and see if it contains certain words such as "keys" and save them.
|
||||
* it works at background
|
||||
* it is expandable(as you can add words to it)
|
||||
<hr>
|
||||
|
||||
KeyFinder is a Chrome extension that passively scans every page you visit for leaked API keys, tokens, secrets, and credentials. It runs silently in the background with zero configuration required.
|
||||
|
||||
## What It Detects
|
||||
|
||||
KeyFinder ships with **80+ detection patterns** covering secrets from:
|
||||
|
||||
| Category | Providers |
|
||||
|----------|-----------|
|
||||
| **Cloud** | AWS (Access Keys, Secret Keys, Session Tokens, Cognito), Google Cloud (API Keys, OAuth, Service Accounts), Azure (Storage Keys, SAS Tokens, Connection Strings) |
|
||||
| **Source Control** | GitHub (PATs, OAuth, Fine-grained tokens), GitLab (PATs, Pipeline, Runner tokens), Bitbucket |
|
||||
| **Payments** | Stripe (Secret, Publishable, Restricted, Webhook), PayPal Braintree, Square |
|
||||
| **Communication** | Slack (Bot, User, App tokens, Webhooks), Discord (Bot tokens, Webhooks), Telegram, Twilio, SendGrid |
|
||||
| **AI / ML** | OpenAI, Anthropic, HuggingFace, Replicate |
|
||||
| **Databases** | MongoDB, PostgreSQL, MySQL, Redis connection strings |
|
||||
| **SaaS** | Shopify, Sentry, New Relic, PlanetScale, Linear, Notion, Datadog, Algolia, Mapbox |
|
||||
| **Infrastructure** | HashiCorp Vault, Terraform, Docker Hub, NPM, Cloudflare, DigitalOcean, Doppler, Pulumi, Grafana |
|
||||
| **Crypto** | RSA, EC, OpenSSH, PGP, DSA private keys |
|
||||
| **Generic** | JWTs, Bearer tokens, Basic Auth, API key assignments, credential URLs, high-entropy strings |
|
||||
|
||||
## How It Works
|
||||
|
||||
KeyFinder scans **10 different attack surfaces** on every page:
|
||||
|
||||
1. **Script `src` URLs** - Checks all script source URLs for keywords and tokens in query parameters
|
||||
2. **Inline scripts** - Scans `<script>` tag contents for secret patterns
|
||||
3. **External scripts** - Fetches and scans same-origin JavaScript files
|
||||
4. **Meta tags** - Checks `<meta>` tags for leaked API keys and tokens
|
||||
5. **Hidden form fields** - Inspects `<input type="hidden">` values
|
||||
6. **Data attributes** - Scans `data-*` attributes for sensitive values
|
||||
7. **HTML comments** - Parses comment nodes for accidentally committed secrets
|
||||
8. **URL parameters** - Analyzes links and hrefs for tokens in query strings
|
||||
9. **Web storage** - Scans localStorage and sessionStorage
|
||||
10. **Network responses** - Intercepts XHR and Fetch responses for leaked secrets
|
||||
|
||||
Additionally, **Shannon entropy analysis** is applied to detect random high-entropy strings that may be undocumented secret formats.
|
||||
|
||||
## Features
|
||||
|
||||
- **Zero dependencies** - Pure vanilla JavaScript, no jQuery, no external libraries
|
||||
- **Manifest V3** - Built for modern Chrome with service worker architecture
|
||||
- **Passive scanning** - Runs automatically on every page load
|
||||
- **Custom keywords** - Add your own search terms to scan for
|
||||
- **Dashboard** - Professional results page with filtering, sorting, and search
|
||||
- **Export** - Download findings as JSON or CSV
|
||||
- **Badge counter** - Shows finding count on the extension icon
|
||||
- **Low footprint** - Minimal CPU and memory usage
|
||||
- **All frames** - Scans iframes and embedded content
|
||||
|
||||
## Installation
|
||||
|
||||
1. git clone https://github.com/momenbasel/KeyFinder.git
|
||||
2. open chrome and go to chrome://extensions
|
||||
3. Enable "Developer mode"
|
||||
4. drag and drop the keyFinder folder
|
||||
<img src="https://github.com/momenbasel/keyFinder/blob/master/installGif.gif?raw=true"/>
|
||||
|
||||
<br/>
|
||||
### From Release (Recommended)
|
||||
|
||||
## Demo:
|
||||
1. Go to [Releases](https://github.com/momenbasel/keyFinder/releases) and download the latest `.crx` file
|
||||
2. Open Chrome and navigate to `chrome://extensions`
|
||||
3. Enable **Developer mode** (top right toggle)
|
||||
4. Drag and drop the `.crx` file onto the page
|
||||
|
||||
### From Source
|
||||
|
||||

|
||||
```bash
|
||||
git clone https://github.com/momenbasel/keyFinder.git
|
||||
```
|
||||
|
||||
1. Open Chrome and go to `chrome://extensions`
|
||||
2. Enable **Developer mode**
|
||||
3. Click **Load unpacked** and select the `keyFinder` folder
|
||||
|
||||
<img src="https://github.com/momenbasel/keyFinder/blob/master/installGif.gif?raw=true" alt="Installation demo"/>
|
||||
|
||||
## Demo
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
1. **Install** the extension
|
||||
2. **Browse** the web normally - KeyFinder scans every page in the background
|
||||
3. Click the **extension icon** to see stats and manage keywords
|
||||
4. Click **View Findings** to open the full results dashboard
|
||||
5. **Filter** by severity, provider, or type
|
||||
6. **Export** findings as JSON or CSV for reporting
|
||||
|
||||
## Adding Custom Keywords
|
||||
|
||||
Click the extension icon, type a keyword in the input field, and click **Add**. The keyword will be used to scan script URLs, inline code, and key-value assignments on every page you visit.
|
||||
|
||||
Default keywords: `key`, `api_key`, `apikey`, `api-key`, `secret`, `token`, `access_token`, `auth`, `credential`, `password`, `client_id`, `client_secret`
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
keyFinder/
|
||||
manifest.json # MV3 manifest
|
||||
popup.html # Extension popup UI
|
||||
results.html # Findings dashboard
|
||||
js/
|
||||
background.js # Service worker - storage and message handling
|
||||
patterns.js # 80+ secret detection regex patterns
|
||||
content.js # Page scanner - DOM, scripts, network interception
|
||||
popup.js # Popup logic
|
||||
results.js # Dashboard logic with filtering and export
|
||||
css/
|
||||
popup.css # Popup styles
|
||||
results.css # Dashboard styles
|
||||
icons/
|
||||
icon16.png
|
||||
icon48.png
|
||||
icon128.png
|
||||
```
|
||||
|
||||
## Disclaimer
|
||||
This tool is for educational purposes only. You are responsible for your own actions. If you break any laws while using this chrome extension, it's your fault.
|
||||
|
||||
This tool is intended for **security research and authorized testing only**. Use it to identify leaked secrets on your own applications or during authorized penetration tests. You are responsible for your own actions.
|
||||
|
||||
## License
|
||||
|
||||
Contact:
|
||||
MIT
|
||||
|
||||
[@momenbassel](https://twitter.com/@momenbassel)
|
||||
## Author
|
||||
|
||||
[@momenbassel](https://x.com/momenbassel) - [LinkedIn](https://www.linkedin.com/in/momenbasel/)
|
||||
|
||||
Vendored
-7
File diff suppressed because one or more lines are too long
Vendored
-4
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB |
+240
@@ -0,0 +1,240 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 360px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background: #0f0f0f;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 16px 16px 12px;
|
||||
border-bottom: 1px solid #1e1e1e;
|
||||
background: linear-gradient(135deg, #0f0f0f 0%, #1a1a2e 100%);
|
||||
}
|
||||
|
||||
.header-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.header-brand h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
background: #1a1a1a;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.header-tagline {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #1e1e1e;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background: #141414;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #1e1e1e;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.keyword-form {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.keyword-form input {
|
||||
flex: 1;
|
||||
padding: 7px 10px;
|
||||
background: #141414;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.keyword-form input:focus {
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
.keyword-form input::placeholder {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.keyword-form button {
|
||||
padding: 7px 14px;
|
||||
background: #4a9eff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.keyword-form button:hover {
|
||||
background: #3a8eef;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
padding: 6px 10px;
|
||||
background: #2a1515;
|
||||
border: 1px solid #4a2020;
|
||||
border-radius: 6px;
|
||||
color: #ff6b6b;
|
||||
font-size: 11px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.keyword-list {
|
||||
list-style: none;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.keyword-list::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.keyword-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.keyword-list::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.keyword-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.keyword-item:hover {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.keyword-label {
|
||||
font-family: "SF Mono", "Fira Code", "Consolas", monospace;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.keyword-remove {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #555;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
.keyword-remove:hover {
|
||||
background: #2a1515;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
color: #444;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #1e1e1e;
|
||||
}
|
||||
|
||||
.results-btn {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
color: #4a9eff;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border: 1px solid #1e2d4a;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.results-btn:hover {
|
||||
background: linear-gradient(135deg, #1e2040 0%, #1a2848 100%);
|
||||
border-color: #2a4070;
|
||||
}
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background: #0a0a0a;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 16px 24px;
|
||||
background: #0f0f0f;
|
||||
border-bottom: 1px solid #1e1e1e;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.header-left h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 11px;
|
||||
color: #555;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-group select,
|
||||
.filter-group input {
|
||||
padding: 6px 10px;
|
||||
background: #141414;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
color: #ccc;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.filter-group select:focus,
|
||||
.filter-group input:focus {
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
.filter-group input {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1a1a1a;
|
||||
color: #ccc;
|
||||
border: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #222;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #2a1515;
|
||||
color: #ff6b6b;
|
||||
border: 1px solid #4a2020;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #3a1a1a;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
padding: 12px 24px;
|
||||
background: #0f0f0f;
|
||||
border-bottom: 1px solid #1e1e1e;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background: #111;
|
||||
border-radius: 6px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.stat-num {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stat-lbl {
|
||||
font-size: 10px;
|
||||
color: #555;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-critical .stat-num { color: #ff4444; }
|
||||
.stat-high .stat-num { color: #ff8c42; }
|
||||
.stat-medium .stat-num { color: #ffd166; }
|
||||
.stat-low .stat-num { color: #4ecdc4; }
|
||||
|
||||
.main {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.findings-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.findings-table thead th {
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid #1e1e1e;
|
||||
white-space: nowrap;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
.findings-table tbody tr {
|
||||
border-bottom: 1px solid #141414;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.findings-table tbody tr:hover {
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.findings-table td {
|
||||
padding: 8px 10px;
|
||||
vertical-align: middle;
|
||||
max-width: 280px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.badge-critical {
|
||||
background: #3a0a0a;
|
||||
color: #ff4444;
|
||||
border: 1px solid #5a1515;
|
||||
}
|
||||
|
||||
.badge-high {
|
||||
background: #3a1f0a;
|
||||
color: #ff8c42;
|
||||
border: 1px solid #5a3015;
|
||||
}
|
||||
|
||||
.badge-medium {
|
||||
background: #3a3a0a;
|
||||
color: #ffd166;
|
||||
border: 1px solid #5a5a15;
|
||||
}
|
||||
|
||||
.badge-low {
|
||||
background: #0a3a30;
|
||||
color: #4ecdc4;
|
||||
border: 1px solid #155a4a;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
font-family: "SF Mono", "Fira Code", monospace;
|
||||
}
|
||||
|
||||
.match-value {
|
||||
font-family: "SF Mono", "Fira Code", "Consolas", monospace;
|
||||
font-size: 11px;
|
||||
background: #141414;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #1e1e1e;
|
||||
color: #4ecdc4;
|
||||
max-width: 260px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.td-provider {
|
||||
font-weight: 600;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.td-pattern {
|
||||
color: #888;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.td-domain {
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.td-time {
|
||||
color: #555;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #4a9eff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 3px 8px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 4px;
|
||||
color: #888;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: #222;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.btn-icon-danger:hover {
|
||||
background: #2a1515;
|
||||
color: #ff6b6b;
|
||||
border-color: #4a2020;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 14px;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
border-radius: 4px;
|
||||
}
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
/*
|
||||
* Globals
|
||||
*/
|
||||
|
||||
/* Links */
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Custom default button */
|
||||
.btn-secondary,
|
||||
.btn-secondary:hover,
|
||||
.btn-secondary:focus {
|
||||
color: #333;
|
||||
text-shadow: none; /* Prevent inheritance from `body` */
|
||||
background-color: #fff;
|
||||
border: .05rem solid #fff;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Base structure
|
||||
*/
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
body {
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
color: #fff;
|
||||
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
|
||||
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
max-width: 42em;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Header
|
||||
*/
|
||||
.masthead {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.masthead-brand {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nav-masthead .nav-link {
|
||||
padding: .25rem 0;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, .5);
|
||||
background-color: transparent;
|
||||
border-bottom: .25rem solid transparent;
|
||||
}
|
||||
|
||||
.nav-masthead .nav-link:hover,
|
||||
.nav-masthead .nav-link:focus {
|
||||
border-bottom-color: rgba(255, 255, 255, .25);
|
||||
}
|
||||
|
||||
.nav-masthead .nav-link + .nav-link {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.nav-masthead .active {
|
||||
color: #fff;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
|
||||
@media (min-width: 48em) {
|
||||
.masthead-brand {
|
||||
float: left;
|
||||
}
|
||||
.nav-masthead {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Cover
|
||||
*/
|
||||
.cover {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
.cover .btn-lg {
|
||||
padding: .75rem 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Footer
|
||||
*/
|
||||
.mastfoot {
|
||||
color: rgba(255, 255, 255, .5);
|
||||
}
|
||||
Submodule https---github.com-momenbasel-keyFinder.git deleted from cdb33c2c23
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 228 B |
Binary file not shown.
|
After Width: | Height: | Size: 612 B |
+99
-10
@@ -1,14 +1,103 @@
|
||||
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
||||
if (request.method == "getStatus")
|
||||
sendResponse({status: localStorage});
|
||||
else
|
||||
sendResponse({});
|
||||
const KEYWORDS_KEY = "kf_keywords";
|
||||
const FINDINGS_KEY = "kf_findings";
|
||||
|
||||
const DEFAULT_KEYWORDS = [
|
||||
"key", "api_key", "apikey", "api-key", "secret", "token",
|
||||
"access_token", "auth", "credential", "password",
|
||||
"client_id", "client_secret"
|
||||
];
|
||||
|
||||
chrome.runtime.onInstalled.addListener(async (details) => {
|
||||
if (details.reason === "install") {
|
||||
await chrome.storage.local.set({
|
||||
[KEYWORDS_KEY]: DEFAULT_KEYWORDS,
|
||||
[FINDINGS_KEY]: []
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.type === "finding") {
|
||||
saveFinding(request.data).then(() => sendResponse({ ok: true }));
|
||||
return true;
|
||||
}
|
||||
if (request.type === "getKeywords") {
|
||||
getKeywords().then((keywords) => sendResponse({ keywords }));
|
||||
return true;
|
||||
}
|
||||
if (request.type === "getFindings") {
|
||||
getFindings().then((findings) => sendResponse({ findings }));
|
||||
return true;
|
||||
}
|
||||
if (request.type === "addKeyword") {
|
||||
addKeyword(request.keyword).then((result) => sendResponse(result));
|
||||
return true;
|
||||
}
|
||||
if (request.type === "removeKeyword") {
|
||||
removeKeyword(request.keyword).then(() => sendResponse({ ok: true }));
|
||||
return true;
|
||||
}
|
||||
if (request.type === "removeFinding") {
|
||||
removeFinding(request.url).then(() => sendResponse({ ok: true }));
|
||||
return true;
|
||||
}
|
||||
if (request.type === "clearFindings") {
|
||||
clearFindings().then(() => sendResponse({ ok: true }));
|
||||
return true;
|
||||
}
|
||||
if (request.type === "exportFindings") {
|
||||
getFindings().then((findings) => sendResponse({ findings }));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
async function getKeywords() {
|
||||
const result = await chrome.storage.local.get(KEYWORDS_KEY);
|
||||
return result[KEYWORDS_KEY] || DEFAULT_KEYWORDS;
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
||||
if (request.getter)
|
||||
console.log(request.getter);
|
||||
localStorage.setItem(request.getter,request.getter);
|
||||
});
|
||||
async function addKeyword(keyword) {
|
||||
const keywords = await getKeywords();
|
||||
const normalized = keyword.trim().toLowerCase();
|
||||
if (!normalized) return { ok: false, error: "Keyword cannot be empty." };
|
||||
if (keywords.includes(normalized)) return { ok: false, error: "Keyword already exists." };
|
||||
keywords.push(normalized);
|
||||
await chrome.storage.local.set({ [KEYWORDS_KEY]: keywords });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function removeKeyword(keyword) {
|
||||
const keywords = await getKeywords();
|
||||
await chrome.storage.local.set({ [KEYWORDS_KEY]: keywords.filter((k) => k !== keyword) });
|
||||
}
|
||||
|
||||
async function getFindings() {
|
||||
const result = await chrome.storage.local.get(FINDINGS_KEY);
|
||||
return result[FINDINGS_KEY] || [];
|
||||
}
|
||||
|
||||
async function saveFinding(finding) {
|
||||
const findings = await getFindings();
|
||||
const isDuplicate = findings.some(
|
||||
(f) => f.url === finding.url && f.match === finding.match
|
||||
);
|
||||
if (isDuplicate) return;
|
||||
findings.push(finding);
|
||||
await chrome.storage.local.set({ [FINDINGS_KEY]: findings });
|
||||
|
||||
const badgeCount = findings.length;
|
||||
chrome.action.setBadgeText({ text: badgeCount > 0 ? String(badgeCount) : "" });
|
||||
chrome.action.setBadgeBackgroundColor({ color: "#e74c3c" });
|
||||
}
|
||||
|
||||
async function removeFinding(url) {
|
||||
const findings = await getFindings();
|
||||
const updated = findings.filter((f) => f.url !== url);
|
||||
await chrome.storage.local.set({ [FINDINGS_KEY]: updated });
|
||||
chrome.action.setBadgeText({ text: updated.length > 0 ? String(updated.length) : "" });
|
||||
}
|
||||
|
||||
async function clearFindings() {
|
||||
await chrome.storage.local.set({ [FINDINGS_KEY]: [] });
|
||||
chrome.action.setBadgeText({ text: "" });
|
||||
}
|
||||
|
||||
Vendored
-7
File diff suppressed because one or more lines are too long
+314
-40
@@ -1,59 +1,333 @@
|
||||
(async function () {
|
||||
"use strict";
|
||||
|
||||
console.log("keyFinder🔑 is working!");
|
||||
const pageUrl = location.href;
|
||||
const pageDomain = location.hostname;
|
||||
const seen = new Set();
|
||||
|
||||
let keywords = [];
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({ type: "getKeywords" });
|
||||
keywords = (response.keywords || []).map((k) => k.toLowerCase());
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
function shannonEntropy(str) {
|
||||
const len = str.length;
|
||||
if (len === 0) return 0;
|
||||
const freq = {};
|
||||
for (const ch of str) freq[ch] = (freq[ch] || 0) + 1;
|
||||
let entropy = 0;
|
||||
for (const ch in freq) {
|
||||
const p = freq[ch] / len;
|
||||
entropy -= p * Math.log2(p);
|
||||
}
|
||||
return entropy;
|
||||
}
|
||||
|
||||
//custom searches
|
||||
function isHighEntropy(str) {
|
||||
if (str.length < 12) return false;
|
||||
return shannonEntropy(str) > 3.5;
|
||||
}
|
||||
|
||||
let js = document.getElementsByTagName('script');
|
||||
function isFalsePositive(match) {
|
||||
if (!match || match.length < 8) return true;
|
||||
const lower = match.toLowerCase();
|
||||
const fp = [
|
||||
"true", "false", "null", "undefined", "function", "return",
|
||||
"window", "document", "object", "string", "number", "boolean",
|
||||
"prototype", "constructor", "adsbygoogle", "googletag",
|
||||
"use strict", "text/javascript", "application/json",
|
||||
"content-type", "text/html", "text/css", "image/png",
|
||||
"image/jpeg", "charset=utf-8", "viewport", "width=device",
|
||||
"http-equiv", "stylesheet", "text/plain",
|
||||
];
|
||||
for (const f of fp) {
|
||||
if (lower === f) return true;
|
||||
}
|
||||
if (/^(0+|1+|a+|f+|x+)$/i.test(match)) return true;
|
||||
if (/^[a-z]+$/i.test(match) && match.length < 20) return true;
|
||||
if (/^(https?:\/\/)?[a-z0-9.-]+\.(js|css|html|png|jpg|gif|svg|woff|ttf|eot|ico)$/i.test(match)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function report(data) {
|
||||
const key = `${data.type}:${data.match}:${data.url || ""}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
try {
|
||||
chrome.runtime.sendMessage({
|
||||
type: "finding",
|
||||
data: { ...data, domain: pageDomain, pageUrl, timestamp: Date.now() },
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function scanText(text, sourceUrl, sourceType) {
|
||||
if (!text || text.length < 10) return;
|
||||
|
||||
chrome.runtime.sendMessage({method: "getStatus"}, function(response) {
|
||||
|
||||
var savedSearch = response.status;
|
||||
|
||||
for (var key in savedSearch) {
|
||||
|
||||
if (savedSearch.hasOwnProperty(key)) {
|
||||
|
||||
for(src in js){
|
||||
|
||||
var url = js[src].src;
|
||||
let regex = new RegExp(key,'i');
|
||||
|
||||
try {
|
||||
|
||||
if(url.search(regex) !== -1) {
|
||||
chrome.runtime.sendMessage({getter: url}, function(response) {
|
||||
});
|
||||
console.log(`KeyFinder: a potential API key found: ${url} it matched your search ${key}`);
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
for (const pattern of SECRET_PATTERNS) {
|
||||
pattern.re.lastIndex = 0;
|
||||
let m;
|
||||
while ((m = pattern.re.exec(text)) !== null) {
|
||||
const matched = m[1] || m[0];
|
||||
if (isFalsePositive(matched)) continue;
|
||||
report({
|
||||
url: sourceUrl,
|
||||
match: matched.substring(0, 200),
|
||||
type: sourceType,
|
||||
patternName: pattern.name,
|
||||
severity: pattern.severity,
|
||||
confidence: pattern.confidence,
|
||||
provider: pattern.provider,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
for (const kw of keywords) {
|
||||
const escaped = kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const kwRegex = new RegExp(
|
||||
`(?:${escaped})\\s*[:=]\\s*['"\`]([^'"\`\\n]{8,200})['"\`]`,
|
||||
"gi"
|
||||
);
|
||||
let m;
|
||||
while ((m = kwRegex.exec(text)) !== null) {
|
||||
const val = m[1];
|
||||
if (isFalsePositive(val)) continue;
|
||||
report({
|
||||
url: sourceUrl,
|
||||
match: val.substring(0, 200),
|
||||
type: sourceType,
|
||||
patternName: `Keyword: ${kw}`,
|
||||
severity: "medium",
|
||||
confidence: isHighEntropy(val) ? "high" : "medium",
|
||||
provider: "Keyword Match",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanScriptSrcUrls() {
|
||||
const scripts = document.querySelectorAll("script[src]");
|
||||
for (const script of scripts) {
|
||||
const src = script.src;
|
||||
if (!src) continue;
|
||||
for (const kw of keywords) {
|
||||
if (src.toLowerCase().includes(kw)) {
|
||||
report({
|
||||
url: src, match: src, type: "script-src",
|
||||
patternName: `Script URL contains: ${kw}`,
|
||||
severity: "medium", confidence: "medium", provider: "URL Scan",
|
||||
});
|
||||
}
|
||||
}
|
||||
try {
|
||||
const url = new URL(src);
|
||||
for (const [param, value] of url.searchParams) {
|
||||
if (value.length >= 16 && isHighEntropy(value)) {
|
||||
report({
|
||||
url: src, match: `${param}=${value.substring(0, 100)}`,
|
||||
type: "url-param", patternName: "High-Entropy URL Parameter",
|
||||
severity: "medium", confidence: "medium", provider: "URL Scan",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function scanInlineScripts() {
|
||||
const scripts = document.querySelectorAll("script:not([src])");
|
||||
for (const script of scripts) {
|
||||
scanText(script.textContent, pageUrl, "inline-script");
|
||||
}
|
||||
}
|
||||
|
||||
async function scanExternalScripts() {
|
||||
const scripts = document.querySelectorAll("script[src]");
|
||||
const fetched = new Set();
|
||||
for (const script of scripts) {
|
||||
try {
|
||||
const src = script.src;
|
||||
if (fetched.has(src)) continue;
|
||||
if (new URL(src).origin !== location.origin) continue;
|
||||
fetched.add(src);
|
||||
const resp = await fetch(src, { credentials: "omit" });
|
||||
if (!resp.ok) continue;
|
||||
const text = await resp.text();
|
||||
scanText(text, src, "external-script");
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// for GoogleMaps API key
|
||||
for (src in js) {
|
||||
let url = js[src].src;
|
||||
try {
|
||||
if(url.search(/key/i) !== -1) {
|
||||
console.log(`KeyFinder: a potential API key found: ${url}`)
|
||||
function scanMetaTags() {
|
||||
const metas = document.querySelectorAll("meta");
|
||||
for (const meta of metas) {
|
||||
const content = meta.getAttribute("content");
|
||||
if (!content || content.length < 12) continue;
|
||||
const name = (meta.getAttribute("name") || meta.getAttribute("property") || "").toLowerCase();
|
||||
const sensitive = ["api-key", "api_key", "apikey", "token", "secret", "access-token", "csrf-token", "csrf_token"];
|
||||
if (sensitive.some((s) => name.includes(s))) {
|
||||
report({
|
||||
url: pageUrl, match: `meta[${name}]=${content.substring(0, 100)}`,
|
||||
type: "meta-tag", patternName: "Sensitive Meta Tag",
|
||||
severity: "high", confidence: "high", provider: "DOM Scan",
|
||||
});
|
||||
}
|
||||
scanText(`${name}=${content}`, pageUrl, "meta-tag");
|
||||
}
|
||||
}
|
||||
|
||||
//saving the results
|
||||
chrome.runtime.sendMessage({getter: url}, function(response) {
|
||||
function scanHiddenInputs() {
|
||||
const inputs = document.querySelectorAll('input[type="hidden"]');
|
||||
for (const input of inputs) {
|
||||
const name = (input.name || input.id || "").toLowerCase();
|
||||
const value = input.value;
|
||||
if (!value || value.length < 8) continue;
|
||||
const sensitive = ["token", "csrf", "api_key", "apikey", "secret", "auth", "session", "nonce", "key", "access_token"];
|
||||
if (sensitive.some((s) => name.includes(s)) || isHighEntropy(value)) {
|
||||
report({
|
||||
url: pageUrl, match: `${name}=${value.substring(0, 100)}`,
|
||||
type: "hidden-input", patternName: "Hidden Form Field",
|
||||
severity: isHighEntropy(value) ? "high" : "medium",
|
||||
confidence: sensitive.some((s) => name.includes(s)) ? "high" : "medium",
|
||||
provider: "DOM Scan",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanDataAttributes() {
|
||||
const all = document.querySelectorAll("*");
|
||||
for (const el of all) {
|
||||
for (const attr of el.attributes) {
|
||||
if (!/^data-.*(?:key|token|secret|auth|api|credential|password)/i.test(attr.name)) continue;
|
||||
if (!attr.value || attr.value.length < 8) continue;
|
||||
report({
|
||||
url: pageUrl, match: `${attr.name}="${attr.value.substring(0, 100)}"`,
|
||||
type: "data-attribute", patternName: "Sensitive Data Attribute",
|
||||
severity: "medium", confidence: isHighEntropy(attr.value) ? "high" : "medium",
|
||||
provider: "DOM Scan",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanHtmlComments() {
|
||||
const walker = document.createTreeWalker(document.documentElement, NodeFilter.SHOW_COMMENT, null);
|
||||
while (walker.nextNode()) {
|
||||
const text = walker.currentNode.textContent;
|
||||
if (text && text.length >= 20) {
|
||||
scanText(text, pageUrl, "html-comment");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanLinkHrefs() {
|
||||
const links = document.querySelectorAll("a[href], link[href]");
|
||||
for (const link of links) {
|
||||
try {
|
||||
const href = link.href;
|
||||
if (!href) continue;
|
||||
const url = new URL(href);
|
||||
for (const [param, value] of url.searchParams) {
|
||||
const p = param.toLowerCase();
|
||||
const sensitive = ["key", "api_key", "apikey", "token", "secret", "access_token", "auth", "password", "session_id"];
|
||||
if (sensitive.some((s) => p.includes(s)) && value.length >= 8) {
|
||||
report({
|
||||
url: href, match: `${param}=${value.substring(0, 100)}`,
|
||||
type: "url-param", patternName: "Sensitive URL Parameter",
|
||||
severity: "high", confidence: "high", provider: "URL Scan",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function scanWebStorage() {
|
||||
const stores = [
|
||||
{ store: localStorage, label: "localStorage" },
|
||||
{ store: sessionStorage, label: "sessionStorage" },
|
||||
];
|
||||
for (const { store, label } of stores) {
|
||||
try {
|
||||
for (let i = 0; i < store.length; i++) {
|
||||
const key = store.key(i);
|
||||
const value = store.getItem(key);
|
||||
if (!value || value.length < 12) continue;
|
||||
const kl = key.toLowerCase();
|
||||
const sensitive = ["token", "key", "secret", "auth", "session", "credential", "password", "jwt", "bearer"];
|
||||
if (sensitive.some((s) => kl.includes(s)) || isHighEntropy(value.substring(0, 100))) {
|
||||
report({
|
||||
url: pageUrl, match: `${label}.${key}=${value.substring(0, 120)}`,
|
||||
type: "web-storage", patternName: `${label} Secret`,
|
||||
severity: "high",
|
||||
confidence: sensitive.some((s) => kl.includes(s)) ? "high" : "medium",
|
||||
provider: "Storage Scan",
|
||||
});
|
||||
}
|
||||
scanText(`${key}=${value}`, pageUrl, "web-storage");
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function scanCookies() {
|
||||
try {
|
||||
const cookies = document.cookie.split(";");
|
||||
for (const cookie of cookies) {
|
||||
const [name, ...rest] = cookie.split("=");
|
||||
if (!name) continue;
|
||||
const value = rest.join("=").trim();
|
||||
const n = name.trim().toLowerCase();
|
||||
const sensitive = ["token", "session", "auth", "jwt", "bearer", "api_key", "apikey", "secret", "credential"];
|
||||
if (value && value.length >= 16 && sensitive.some((s) => n.includes(s))) {
|
||||
report({
|
||||
url: pageUrl, match: `cookie:${name.trim()}=${value.substring(0, 80)}`,
|
||||
type: "cookie", patternName: "Sensitive Cookie",
|
||||
severity: "medium", confidence: "medium", provider: "Cookie Scan",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
window.addEventListener("__kf_finding__", (e) => {
|
||||
const data = e.detail;
|
||||
if (!data) return;
|
||||
|
||||
if (data.rawText) {
|
||||
scanText(data.rawText, data.sourceUrl || pageUrl, data.type);
|
||||
if (!data.match) return;
|
||||
}
|
||||
|
||||
if (data.match) {
|
||||
report({
|
||||
url: data.sourceUrl || pageUrl,
|
||||
match: data.match,
|
||||
type: data.type,
|
||||
patternName: data.patternName || data.type,
|
||||
severity: data.severity || "medium",
|
||||
confidence: data.confidence || "medium",
|
||||
provider: data.provider || "Runtime Scan",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
//console.log(err)
|
||||
});
|
||||
|
||||
scanScriptSrcUrls();
|
||||
scanInlineScripts();
|
||||
scanMetaTags();
|
||||
scanHiddenInputs();
|
||||
scanDataAttributes();
|
||||
scanHtmlComments();
|
||||
scanLinkHrefs();
|
||||
scanWebStorage();
|
||||
scanCookies();
|
||||
await scanExternalScripts();
|
||||
|
||||
if (seen.size > 0) {
|
||||
console.log(`[KeyFinder] ${seen.size} potential secret(s) found on ${pageDomain}`);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const EVENT_NAME = "__kf_finding__";
|
||||
|
||||
function emit(data) {
|
||||
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: data }));
|
||||
}
|
||||
|
||||
function shannonEntropy(str) {
|
||||
const len = str.length;
|
||||
if (len === 0) return 0;
|
||||
const freq = {};
|
||||
for (const ch of str) freq[ch] = (freq[ch] || 0) + 1;
|
||||
let entropy = 0;
|
||||
for (const ch in freq) {
|
||||
const p = freq[ch] / len;
|
||||
entropy -= p * Math.log2(p);
|
||||
}
|
||||
return entropy;
|
||||
}
|
||||
|
||||
function isHighEntropy(str) {
|
||||
return str.length >= 12 && shannonEntropy(str) > 3.5;
|
||||
}
|
||||
|
||||
const globalNames = [
|
||||
"API_KEY", "api_key", "apiKey", "apikey",
|
||||
"SECRET", "secret", "secretKey", "secret_key",
|
||||
"TOKEN", "token", "accessToken", "access_token",
|
||||
"AUTH_TOKEN", "authToken", "auth_token",
|
||||
"STRIPE_KEY", "stripeKey", "stripe_key",
|
||||
"FIREBASE_CONFIG", "firebaseConfig",
|
||||
"AWS_ACCESS_KEY", "awsAccessKey",
|
||||
"__NEXT_DATA__", "__NUXT__", "__APP_CONFIG__",
|
||||
"__ENV__", "__CONFIG__", "ENV", "CONFIG",
|
||||
];
|
||||
|
||||
for (const name of globalNames) {
|
||||
try {
|
||||
const val = window[name];
|
||||
if (val === undefined || val === null) continue;
|
||||
const str = typeof val === "object" ? JSON.stringify(val) : String(val);
|
||||
if (str.length < 8 || str === "[object Object]") continue;
|
||||
emit({
|
||||
match: `window.${name}=${str.substring(0, 200)}`,
|
||||
type: "window-global",
|
||||
patternName: "Exposed Global Variable",
|
||||
severity: "high",
|
||||
confidence: typeof val !== "object" && isHighEntropy(str.substring(0, 60)) ? "high" : "medium",
|
||||
provider: "JS Global Scan",
|
||||
isObject: typeof val === "object",
|
||||
rawText: typeof val === "object" ? str.substring(0, 5000) : null,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const origXhrOpen = XMLHttpRequest.prototype.open;
|
||||
const origXhrSend = XMLHttpRequest.prototype.send;
|
||||
|
||||
XMLHttpRequest.prototype.open = function (method, url) {
|
||||
this._kfUrl = url;
|
||||
return origXhrOpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function () {
|
||||
this.addEventListener("load", function () {
|
||||
try {
|
||||
const ct = this.getResponseHeader("content-type") || "";
|
||||
if (ct.includes("json") || ct.includes("javascript") || ct.includes("text")) {
|
||||
const body = this.responseText;
|
||||
if (body && body.length > 10 && body.length < 500000) {
|
||||
emit({
|
||||
type: "xhr-response",
|
||||
sourceUrl: String(this._kfUrl || ""),
|
||||
rawText: body,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
return origXhrSend.apply(this, arguments);
|
||||
};
|
||||
|
||||
const origFetch = window.fetch;
|
||||
window.fetch = async function () {
|
||||
const response = await origFetch.apply(this, arguments);
|
||||
try {
|
||||
const url = typeof arguments[0] === "string" ? arguments[0] : arguments[0]?.url || "";
|
||||
const cloned = response.clone();
|
||||
const ct = cloned.headers.get("content-type") || "";
|
||||
if (ct.includes("json") || ct.includes("javascript") || ct.includes("text")) {
|
||||
cloned.text().then((body) => {
|
||||
if (body && body.length > 10 && body.length < 500000) {
|
||||
emit({
|
||||
type: "fetch-response",
|
||||
sourceUrl: String(url || ""),
|
||||
rawText: body,
|
||||
});
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
} catch {}
|
||||
return response;
|
||||
};
|
||||
})();
|
||||
Vendored
-2
File diff suppressed because one or more lines are too long
+134
@@ -0,0 +1,134 @@
|
||||
const SECRET_PATTERNS = [
|
||||
|
||||
{ name: "AWS Access Key ID", re: /\b(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}\b/g, severity: "critical", confidence: "high", provider: "AWS" },
|
||||
{ name: "AWS Secret Access Key", re: /(?:aws_secret_access_key|aws_secret|secret_access_key|AWS_SECRET)\s*[:=]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi, severity: "critical", confidence: "high", provider: "AWS" },
|
||||
{ name: "AWS MWS Auth Token", re: /amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, severity: "critical", confidence: "high", provider: "AWS" },
|
||||
{ name: "AWS Cognito Pool ID", re: /(?:us|eu|ap|sa|ca|me|af)-(?:east|west|south|north|central|southeast|northeast)-[0-9]:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, severity: "medium", confidence: "high", provider: "AWS" },
|
||||
{ name: "AWS AppSync GraphQL Key", re: /da2-[a-z0-9]{26}/g, severity: "high", confidence: "high", provider: "AWS" },
|
||||
{ name: "AWS Session Token", re: /(?:aws_session_token|AWS_SESSION_TOKEN)\s*[:=]\s*['"]?([A-Za-z0-9/+=]{100,})['"]?/gi, severity: "critical", confidence: "high", provider: "AWS" },
|
||||
|
||||
{ name: "Google API Key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g, severity: "high", confidence: "high", provider: "Google" },
|
||||
{ name: "Google OAuth Access Token", re: /\bya29\.[0-9A-Za-z_-]+/g, severity: "critical", confidence: "high", provider: "Google" },
|
||||
{ name: "Google OAuth Client ID", re: /[0-9]+-[a-z0-9_]{32}\.apps\.googleusercontent\.com/g, severity: "medium", confidence: "high", provider: "Google" },
|
||||
{ name: "Google OAuth Client Secret", re: /(?:client_secret|google_secret)\s*[:=]\s*['"]?(GOCSPX-[A-Za-z0-9_-]{28})['"]?/gi, severity: "critical", confidence: "high", provider: "Google" },
|
||||
{ name: "Google Cloud Service Account", re: /"type"\s*:\s*"service_account"/g, severity: "critical", confidence: "high", provider: "Google" },
|
||||
{ name: "Firebase Database URL", re: /https:\/\/[a-z0-9-]+\.firebaseio\.com/g, severity: "medium", confidence: "high", provider: "Firebase" },
|
||||
{ name: "Firebase Cloud Messaging Key", re: /\bAAAA[A-Za-z0-9_-]{7}:[A-Za-z0-9_-]{140}/g, severity: "high", confidence: "high", provider: "Firebase" },
|
||||
|
||||
{ name: "Azure Storage Account Key", re: /(?:AccountKey|azure_storage_key|AZURE_STORAGE_KEY)\s*[:=]\s*['"]?([A-Za-z0-9/+=]{88})['"]?/gi, severity: "critical", confidence: "high", provider: "Azure" },
|
||||
{ name: "Azure Connection String", re: /DefaultEndpointsProtocol=https?;AccountName=[^;]+;AccountKey=[A-Za-z0-9/+=]{88}/g, severity: "critical", confidence: "high", provider: "Azure" },
|
||||
{ name: "Azure SAS Token", re: /[?&]sig=[A-Za-z0-9%/+=]+&/g, severity: "high", confidence: "medium", provider: "Azure" },
|
||||
|
||||
{ name: "GitHub Personal Access Token", re: /\bghp_[A-Za-z0-9_]{36,}\b/g, severity: "critical", confidence: "high", provider: "GitHub" },
|
||||
{ name: "GitHub OAuth Access Token", re: /\bgho_[A-Za-z0-9_]{36,}\b/g, severity: "critical", confidence: "high", provider: "GitHub" },
|
||||
{ name: "GitHub User-to-Server Token", re: /\bghu_[A-Za-z0-9_]{36,}\b/g, severity: "critical", confidence: "high", provider: "GitHub" },
|
||||
{ name: "GitHub Server-to-Server Token", re: /\bghs_[A-Za-z0-9_]{36,}\b/g, severity: "critical", confidence: "high", provider: "GitHub" },
|
||||
{ name: "GitHub Refresh Token", re: /\bghr_[A-Za-z0-9_]{36,}\b/g, severity: "critical", confidence: "high", provider: "GitHub" },
|
||||
{ name: "GitHub Fine-grained PAT", re: /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, severity: "critical", confidence: "high", provider: "GitHub" },
|
||||
|
||||
{ name: "GitLab Personal Access Token", re: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, severity: "critical", confidence: "high", provider: "GitLab" },
|
||||
{ name: "GitLab Pipeline Token", re: /\bglptt-[A-Za-z0-9_-]{20,}\b/g, severity: "high", confidence: "high", provider: "GitLab" },
|
||||
{ name: "GitLab Runner Token", re: /\bGR1348941[A-Za-z0-9_-]{20}\b/g, severity: "high", confidence: "high", provider: "GitLab" },
|
||||
|
||||
{ name: "Stripe Secret Key", re: /\bsk_(live|test)_[0-9a-zA-Z]{24,}\b/g, severity: "critical", confidence: "high", provider: "Stripe" },
|
||||
{ name: "Stripe Publishable Key", re: /\bpk_(live|test)_[0-9a-zA-Z]{24,}\b/g, severity: "low", confidence: "high", provider: "Stripe" },
|
||||
{ name: "Stripe Restricted Key", re: /\brk_(live|test)_[0-9a-zA-Z]{24,}\b/g, severity: "critical", confidence: "high", provider: "Stripe" },
|
||||
{ name: "Stripe Webhook Secret", re: /\bwhsec_[A-Za-z0-9]{32,}\b/g, severity: "high", confidence: "high", provider: "Stripe" },
|
||||
|
||||
{ name: "PayPal Braintree Access Token", re: /access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}/g, severity: "critical", confidence: "high", provider: "PayPal" },
|
||||
|
||||
{ name: "Square Access Token", re: /\bsq0atp-[0-9A-Za-z_-]{22}\b/g, severity: "critical", confidence: "high", provider: "Square" },
|
||||
{ name: "Square OAuth Secret", re: /\bsq0csp-[0-9A-Za-z_-]{43}\b/g, severity: "critical", confidence: "high", provider: "Square" },
|
||||
|
||||
{ name: "Slack Bot Token", re: /\bxoxb-[0-9]{10,}-[0-9]{10,}-[A-Za-z0-9]{24,}\b/g, severity: "critical", confidence: "high", provider: "Slack" },
|
||||
{ name: "Slack User Token", re: /\bxoxp-[0-9]{10,}-[0-9]{10,}-[A-Za-z0-9]{24,}\b/g, severity: "critical", confidence: "high", provider: "Slack" },
|
||||
{ name: "Slack App Token", re: /\bxapp-[0-9]+-[A-Za-z0-9]+-[0-9]+-[A-Za-z0-9]+/g, severity: "high", confidence: "high", provider: "Slack" },
|
||||
{ name: "Slack Webhook URL", re: /hooks\.slack\.com\/services\/T[A-Z0-9]{8,}\/B[A-Z0-9]{8,}\/[A-Za-z0-9]{24}/g, severity: "high", confidence: "high", provider: "Slack" },
|
||||
|
||||
{ name: "Discord Bot Token", re: /[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27,}/g, severity: "critical", confidence: "high", provider: "Discord" },
|
||||
{ name: "Discord Webhook URL", re: /discord(?:app)?\.com\/api\/webhooks\/[0-9]+\/[A-Za-z0-9_-]+/g, severity: "high", confidence: "high", provider: "Discord" },
|
||||
|
||||
{ name: "Telegram Bot Token", re: /\b[0-9]{8,10}:[A-Za-z0-9_-]{35}\b/g, severity: "critical", confidence: "high", provider: "Telegram" },
|
||||
|
||||
{ name: "Twilio Account SID", re: /\bAC[0-9a-fA-F]{32}\b/g, severity: "medium", confidence: "high", provider: "Twilio" },
|
||||
{ name: "Twilio API Key", re: /\bSK[0-9a-fA-F]{32}\b/g, severity: "high", confidence: "high", provider: "Twilio" },
|
||||
|
||||
{ name: "SendGrid API Key", re: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/g, severity: "critical", confidence: "high", provider: "SendGrid" },
|
||||
|
||||
{ name: "Mailchimp API Key", re: /\b[0-9a-f]{32}-us[0-9]{1,2}\b/g, severity: "high", confidence: "high", provider: "Mailchimp" },
|
||||
|
||||
{ name: "Mailgun API Key", re: /\bkey-[0-9a-zA-Z]{32}\b/g, severity: "high", confidence: "high", provider: "Mailgun" },
|
||||
|
||||
{ name: "NPM Access Token", re: /\bnpm_[A-Za-z0-9]{36}\b/g, severity: "critical", confidence: "high", provider: "NPM" },
|
||||
|
||||
{ name: "Docker Hub Token", re: /\bdckr_pat_[A-Za-z0-9_-]{27}\b/g, severity: "high", confidence: "high", provider: "Docker" },
|
||||
|
||||
{ name: "Vault Token", re: /\b(?:hvs|hvb|hvr)\.[A-Za-z0-9_-]{24,}\b/g, severity: "critical", confidence: "high", provider: "HashiCorp" },
|
||||
{ name: "Terraform Cloud Token", re: /\b[A-Za-z0-9]{14}\.atlasv1\.[A-Za-z0-9_-]{60,}\b/g, severity: "critical", confidence: "high", provider: "HashiCorp" },
|
||||
|
||||
{ name: "MongoDB Connection String", re: /mongodb(?:\+srv)?:\/\/[^\s'"<>]+/g, severity: "critical", confidence: "high", provider: "MongoDB" },
|
||||
{ name: "PostgreSQL Connection String", re: /postgres(?:ql)?:\/\/[^\s'"<>]+/g, severity: "critical", confidence: "high", provider: "PostgreSQL" },
|
||||
{ name: "MySQL Connection String", re: /mysql:\/\/[^\s'"<>]+/g, severity: "critical", confidence: "high", provider: "MySQL" },
|
||||
{ name: "Redis Connection String", re: /redis(?:s)?:\/\/[^\s'"<>]+/g, severity: "critical", confidence: "high", provider: "Redis" },
|
||||
|
||||
{ name: "Shopify Access Token", re: /\bshpat_[a-fA-F0-9]{32}\b/g, severity: "critical", confidence: "high", provider: "Shopify" },
|
||||
{ name: "Shopify Custom App Token", re: /\bshpca_[a-fA-F0-9]{32}\b/g, severity: "critical", confidence: "high", provider: "Shopify" },
|
||||
{ name: "Shopify Private App Token", re: /\bshppa_[a-fA-F0-9]{32}\b/g, severity: "critical", confidence: "high", provider: "Shopify" },
|
||||
{ name: "Shopify Shared Secret", re: /\bshpss_[a-fA-F0-9]{32}\b/g, severity: "critical", confidence: "high", provider: "Shopify" },
|
||||
|
||||
{ name: "Sentry DSN", re: /https:\/\/[0-9a-f]{32}@(?:o[0-9]+\.)?(?:sentry\.io|[a-z0-9.-]+)\/[0-9]+/g, severity: "medium", confidence: "high", provider: "Sentry" },
|
||||
{ name: "Sentry Auth Token", re: /\bsntrys_[A-Za-z0-9_]{64,}\b/g, severity: "high", confidence: "high", provider: "Sentry" },
|
||||
|
||||
{ name: "New Relic API Key", re: /\bNRAK-[A-Z0-9]{27}\b/g, severity: "high", confidence: "high", provider: "New Relic" },
|
||||
{ name: "New Relic Browser Key", re: /\bNRJS-[a-f0-9]{19}\b/g, severity: "medium", confidence: "high", provider: "New Relic" },
|
||||
|
||||
{ name: "PlanetScale Token", re: /\bpscale_tkn_[A-Za-z0-9_-]{43}\b/g, severity: "critical", confidence: "high", provider: "PlanetScale" },
|
||||
{ name: "PlanetScale Password", re: /\bpscale_pw_[A-Za-z0-9_-]{43}\b/g, severity: "critical", confidence: "high", provider: "PlanetScale" },
|
||||
|
||||
{ name: "Linear API Key", re: /\blin_api_[A-Za-z0-9]{40}\b/g, severity: "high", confidence: "high", provider: "Linear" },
|
||||
|
||||
{ name: "Notion Integration Token", re: /\bntn_[A-Za-z0-9]{40,}\b/g, severity: "high", confidence: "high", provider: "Notion" },
|
||||
{ name: "Notion Secret", re: /\bsecret_[A-Za-z0-9]{43}\b/g, severity: "high", confidence: "medium", provider: "Notion" },
|
||||
|
||||
{ name: "OpenAI API Key", re: /\bsk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}\b/g, severity: "critical", confidence: "high", provider: "OpenAI" },
|
||||
{ name: "OpenAI API Key (Project)", re: /\bsk-proj-[A-Za-z0-9_-]{40,}\b/g, severity: "critical", confidence: "high", provider: "OpenAI" },
|
||||
{ name: "Anthropic API Key", re: /\bsk-ant-[A-Za-z0-9_-]{90,}\b/g, severity: "critical", confidence: "high", provider: "Anthropic" },
|
||||
{ name: "HuggingFace Token", re: /\bhf_[A-Za-z0-9]{34}\b/g, severity: "high", confidence: "high", provider: "HuggingFace" },
|
||||
{ name: "Replicate API Token", re: /\br8_[A-Za-z0-9]{36}\b/g, severity: "high", confidence: "high", provider: "Replicate" },
|
||||
|
||||
{ name: "Twitter Bearer Token", re: /\bAAAAAAAAAAAAAAAAAAAAA[A-Za-z0-9%]+/g, severity: "critical", confidence: "high", provider: "Twitter" },
|
||||
{ name: "Facebook Access Token", re: /\bEAAC[a-zA-Z0-9]+/g, severity: "critical", confidence: "high", provider: "Facebook" },
|
||||
{ name: "Instagram Access Token", re: /\bIGQV[A-Za-z0-9_-]+/g, severity: "high", confidence: "high", provider: "Instagram" },
|
||||
|
||||
{ name: "Cloudflare API Token", re: /(?:cloudflare_api_token|CF_API_TOKEN|CLOUDFLARE_API_TOKEN)\s*[:=]\s*['"]?([A-Za-z0-9_-]{40})['"]?/gi, severity: "high", confidence: "medium", provider: "Cloudflare" },
|
||||
{ name: "DigitalOcean Token", re: /\bdop_v1_[a-f0-9]{64}\b/g, severity: "critical", confidence: "high", provider: "DigitalOcean" },
|
||||
{ name: "DigitalOcean Spaces Key", re: /\bDO00[A-Z0-9]{36}\b/g, severity: "high", confidence: "high", provider: "DigitalOcean" },
|
||||
{ name: "Doppler Token", re: /\bdp\.(?:ct|st|sa|scim)\.[A-Za-z0-9_-]{40,}\b/g, severity: "critical", confidence: "high", provider: "Doppler" },
|
||||
{ name: "Pulumi Access Token", re: /\bpul-[a-f0-9]{40}\b/g, severity: "high", confidence: "high", provider: "Pulumi" },
|
||||
{ name: "Grafana API Key", re: /\bglc_[A-Za-z0-9_+/]{32,}\b/g, severity: "high", confidence: "high", provider: "Grafana" },
|
||||
{ name: "Grafana Service Account Token", re: /\bglsa_[A-Za-z0-9_]{32,}_[0-9a-f]{8}\b/g, severity: "high", confidence: "high", provider: "Grafana" },
|
||||
|
||||
{ name: "Mapbox Public Token", re: /\bpk\.[A-Za-z0-9_-]{60,}\.[A-Za-z0-9_-]{20,}\b/g, severity: "medium", confidence: "high", provider: "Mapbox" },
|
||||
{ name: "Mapbox Secret Token", re: /\bsk\.[A-Za-z0-9_-]{60,}\.[A-Za-z0-9_-]{20,}\b/g, severity: "high", confidence: "high", provider: "Mapbox" },
|
||||
|
||||
{ name: "Datadog API Key", re: /(?:datadog_api_key|DD_API_KEY|DATADOG_API_KEY)\s*[:=]\s*['"]?([0-9a-f]{32})['"]?/gi, severity: "high", confidence: "high", provider: "Datadog" },
|
||||
|
||||
{ name: "Algolia API Key", re: /(?:algolia_api_key|ALGOLIA_API_KEY|algolia_admin_key)\s*[:=]\s*['"]?([A-Za-z0-9]{32})['"]?/gi, severity: "high", confidence: "medium", provider: "Algolia" },
|
||||
|
||||
{ name: "Vercel Access Token", re: /(?:vercel_token|VERCEL_TOKEN)\s*[:=]\s*['"]?([A-Za-z0-9]{24})['"]?/gi, severity: "high", confidence: "medium", provider: "Vercel" },
|
||||
|
||||
{ name: "JSON Web Token", re: /\beyJhbGci[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, severity: "high", confidence: "high", provider: "JWT" },
|
||||
|
||||
{ name: "RSA Private Key", re: /-----BEGIN RSA PRIVATE KEY-----/g, severity: "critical", confidence: "high", provider: "Crypto" },
|
||||
{ name: "EC Private Key", re: /-----BEGIN EC PRIVATE KEY-----/g, severity: "critical", confidence: "high", provider: "Crypto" },
|
||||
{ name: "OpenSSH Private Key", re: /-----BEGIN OPENSSH PRIVATE KEY-----/g, severity: "critical", confidence: "high", provider: "Crypto" },
|
||||
{ name: "PGP Private Key Block", re: /-----BEGIN PGP PRIVATE KEY BLOCK-----/g, severity: "critical", confidence: "high", provider: "Crypto" },
|
||||
|
||||
{ name: "Authorization Bearer Token", re: /(?:Authorization|Bearer)\s*[:=]\s*['"]?Bearer\s+([A-Za-z0-9_.\-/+=]{20,})['"]?/gi, severity: "high", confidence: "medium", provider: "Generic" },
|
||||
{ name: "Basic Auth Credentials", re: /(?:Authorization)\s*[:=]\s*['"]?Basic\s+([A-Za-z0-9+/=]{10,})['"]?/gi, severity: "high", confidence: "medium", provider: "Generic" },
|
||||
{ name: "Generic API Key", re: /(?:api[_-]?key|apiKey|API_KEY)\s*[:=]\s*['"`]([A-Za-z0-9_\-/.]{16,120})['"`]/gi, severity: "medium", confidence: "medium", provider: "Generic" },
|
||||
{ name: "Generic Secret", re: /(?:secret[_-]?key|secretKey|SECRET_KEY|app[_-]?secret|APP_SECRET)\s*[:=]\s*['"`]([A-Za-z0-9_\-/.]{16,120})['"`]/gi, severity: "high", confidence: "medium", provider: "Generic" },
|
||||
{ name: "Generic Token", re: /(?:access[_-]?token|auth[_-]?token|AUTH_TOKEN|ACCESS_TOKEN)\s*[:=]\s*['"`]([A-Za-z0-9_\-/.]{16,120})['"`]/gi, severity: "high", confidence: "medium", provider: "Generic" },
|
||||
{ name: "Generic Password", re: /(?:password|passwd|PASSWD|PASSWORD)\s*[:=]\s*['"`]([^\s'"`]{8,120})['"`]/gi, severity: "high", confidence: "low", provider: "Generic" },
|
||||
{ name: "Credential URL", re: /(?:https?|ftp):\/\/[^\s:@'"]+:[^\s:@'"]+@[^\s'"]+/g, severity: "high", confidence: "medium", provider: "Generic" },
|
||||
];
|
||||
Vendored
-5
File diff suppressed because one or more lines are too long
+74
-40
@@ -1,48 +1,82 @@
|
||||
var save = document.getElementById('save');
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
|
||||
|
||||
save.onclick = function() {
|
||||
|
||||
var keyword = document.getElementById("keyword").value;
|
||||
|
||||
|
||||
if(keyword == "key") {
|
||||
alert("already exist!");
|
||||
|
||||
}
|
||||
if(keyword === ""){
|
||||
alert("cannot be empty");
|
||||
}
|
||||
|
||||
if(keyword == localStorage.getItem(keyword)){
|
||||
alert("you can't add the same word twice!")
|
||||
}
|
||||
else
|
||||
{
|
||||
keyword.trim();
|
||||
//setting key value to keyword for easy lopping
|
||||
localStorage.setItem(keyword,keyword);
|
||||
|
||||
}
|
||||
async function init() {
|
||||
await renderKeywords();
|
||||
await renderStats();
|
||||
document.getElementById("keywordForm").addEventListener("submit", handleAddKeyword);
|
||||
}
|
||||
|
||||
async function renderKeywords() {
|
||||
const response = await chrome.runtime.sendMessage({ type: "getKeywords" });
|
||||
const keywords = response.keywords || [];
|
||||
const list = document.getElementById("keywordList");
|
||||
list.innerHTML = "";
|
||||
|
||||
var i;
|
||||
for(i=0; i < localStorage.length; i++) {
|
||||
//regex to filter keywords from localStorage and showing URLS
|
||||
if(!/(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i.test(localStorage.key(i))){
|
||||
if(localStorage.key(i) !== 'undefined')
|
||||
{
|
||||
$('body').append(`<li>${localStorage.key(i)}<button value=${localStorage.key(i)} class="btn"><i class="fa fa-trash"></i></button></li>`)
|
||||
}
|
||||
document.getElementById("keywordCount").textContent = keywords.length;
|
||||
|
||||
if (keywords.length === 0) {
|
||||
list.innerHTML = '<li class="empty-state">No keywords configured</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const kw of keywords) {
|
||||
const li = document.createElement("li");
|
||||
li.className = "keyword-item";
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.className = "keyword-label";
|
||||
label.textContent = kw;
|
||||
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.className = "keyword-remove";
|
||||
removeBtn.textContent = "\u00D7";
|
||||
removeBtn.title = `Remove "${kw}"`;
|
||||
removeBtn.addEventListener("click", () => handleRemoveKeyword(kw));
|
||||
|
||||
li.appendChild(label);
|
||||
li.appendChild(removeBtn);
|
||||
list.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
async function renderStats() {
|
||||
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
|
||||
const findings = response.findings || [];
|
||||
document.getElementById("findingCount").textContent = findings.length;
|
||||
}
|
||||
|
||||
$('.btn').click(function() {
|
||||
var selectedItem = $(this).val();
|
||||
localStorage.removeItem(selectedItem);
|
||||
location.reload();
|
||||
alert(`${selectedItem} removed`);
|
||||
})
|
||||
async function handleAddKeyword(e) {
|
||||
e.preventDefault();
|
||||
const input = document.getElementById("keywordInput");
|
||||
const errorMsg = document.getElementById("errorMsg");
|
||||
const keyword = input.value.trim();
|
||||
|
||||
errorMsg.hidden = true;
|
||||
|
||||
if (!keyword) {
|
||||
showError("Keyword cannot be empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await chrome.runtime.sendMessage({ type: "addKeyword", keyword });
|
||||
|
||||
if (!result.ok) {
|
||||
showError(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
input.value = "";
|
||||
await renderKeywords();
|
||||
}
|
||||
|
||||
async function handleRemoveKeyword(keyword) {
|
||||
await chrome.runtime.sendMessage({ type: "removeKeyword", keyword });
|
||||
await renderKeywords();
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
const errorMsg = document.getElementById("errorMsg");
|
||||
errorMsg.textContent = msg;
|
||||
errorMsg.hidden = false;
|
||||
setTimeout(() => { errorMsg.hidden = true; }, 3000);
|
||||
}
|
||||
|
||||
+233
-71
@@ -1,87 +1,249 @@
|
||||
//Searching localStorage for URL
|
||||
var i;
|
||||
for(i=0; i < localStorage.length; i++) {
|
||||
//regex to filter keywords from localStorage and showing URLS
|
||||
if(/(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i.test(localStorage.key(i))){
|
||||
$('.table').append(`
|
||||
<tr>
|
||||
<td scope="row"></td>
|
||||
<td>${extractHostname(localStorage.key(i))}</td>
|
||||
<td> <a href='${localStorage.key(i)}'>${localStorage.key(i)}</a> <button value=${localStorage.key(i)} class="btn"><i class="fa fa-trash"></i></button> </td>
|
||||
<td>${searchKeywordOnURL(localStorage.key(i))}</td>
|
||||
<tr>
|
||||
<style>
|
||||
let allFindings = [];
|
||||
|
||||
`)
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
async function init() {
|
||||
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
|
||||
allFindings = response.findings || [];
|
||||
|
||||
populateFilters();
|
||||
renderStats();
|
||||
renderFindings();
|
||||
|
||||
document.getElementById("severityFilter").addEventListener("change", renderFindings);
|
||||
document.getElementById("typeFilter").addEventListener("change", renderFindings);
|
||||
document.getElementById("providerFilter").addEventListener("change", renderFindings);
|
||||
document.getElementById("searchBox").addEventListener("input", renderFindings);
|
||||
document.getElementById("exportJsonBtn").addEventListener("click", exportJson);
|
||||
document.getElementById("exportCsvBtn").addEventListener("click", exportCsv);
|
||||
document.getElementById("clearBtn").addEventListener("click", clearAll);
|
||||
}
|
||||
|
||||
function getFiltered() {
|
||||
const severity = document.getElementById("severityFilter").value;
|
||||
const type = document.getElementById("typeFilter").value;
|
||||
const provider = document.getElementById("providerFilter").value;
|
||||
const search = document.getElementById("searchBox").value.toLowerCase();
|
||||
|
||||
return allFindings.filter((f) => {
|
||||
if (severity !== "all" && f.severity !== severity) return false;
|
||||
if (type !== "all" && f.type !== type) return false;
|
||||
if (provider !== "all" && f.provider !== provider) return false;
|
||||
if (search && !JSON.stringify(f).toLowerCase().includes(search)) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function populateFilters() {
|
||||
const types = [...new Set(allFindings.map((f) => f.type))].sort();
|
||||
const providers = [...new Set(allFindings.map((f) => f.provider))].sort();
|
||||
|
||||
const typeSelect = document.getElementById("typeFilter");
|
||||
for (const t of types) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = t;
|
||||
opt.textContent = t;
|
||||
typeSelect.appendChild(opt);
|
||||
}
|
||||
|
||||
const providerSelect = document.getElementById("providerFilter");
|
||||
for (const p of providers) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = p;
|
||||
opt.textContent = p;
|
||||
providerSelect.appendChild(opt);
|
||||
}
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
const bar = document.getElementById("statsBar");
|
||||
const critical = allFindings.filter((f) => f.severity === "critical").length;
|
||||
const high = allFindings.filter((f) => f.severity === "high").length;
|
||||
const medium = allFindings.filter((f) => f.severity === "medium").length;
|
||||
const low = allFindings.filter((f) => f.severity === "low").length;
|
||||
const domains = new Set(allFindings.map((f) => f.domain)).size;
|
||||
|
||||
|
||||
function searchKeywordOnURL(key) {
|
||||
let i;
|
||||
var keyword;
|
||||
for(i=0; i < localStorage.length; i++) {
|
||||
//regex to filter keywords from localStorage and showing URLS
|
||||
if(!/(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i.test(localStorage.key(i))){
|
||||
if(key.indexOf(localStorage.key(i)) !== -1) {
|
||||
return localStorage.key(i);
|
||||
}
|
||||
}
|
||||
bar.innerHTML = "";
|
||||
const stats = [
|
||||
{ label: "Total", value: allFindings.length, cls: "stat-total" },
|
||||
{ label: "Critical", value: critical, cls: "stat-critical" },
|
||||
{ label: "High", value: high, cls: "stat-high" },
|
||||
{ label: "Medium", value: medium, cls: "stat-medium" },
|
||||
{ label: "Low", value: low, cls: "stat-low" },
|
||||
{ label: "Domains", value: domains, cls: "stat-domains" },
|
||||
];
|
||||
for (const s of stats) {
|
||||
const el = document.createElement("div");
|
||||
el.className = `stat-item ${s.cls}`;
|
||||
const num = document.createElement("span");
|
||||
num.className = "stat-num";
|
||||
num.textContent = s.value;
|
||||
const lbl = document.createElement("span");
|
||||
lbl.className = "stat-lbl";
|
||||
lbl.textContent = s.label;
|
||||
el.appendChild(num);
|
||||
el.appendChild(lbl);
|
||||
bar.appendChild(el);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function renderFindings() {
|
||||
const filtered = getFiltered();
|
||||
const tbody = document.getElementById("findingsBody");
|
||||
const empty = document.getElementById("emptyState");
|
||||
|
||||
tbody.innerHTML = "";
|
||||
|
||||
//numbering results && self-inovking the function
|
||||
(function(cl){
|
||||
var table = document.querySelector('table.' + cl)
|
||||
var trs = table.querySelectorAll('tr')
|
||||
var counter = 1
|
||||
if (filtered.length === 0) {
|
||||
empty.hidden = false;
|
||||
return;
|
||||
}
|
||||
empty.hidden = true;
|
||||
|
||||
Array.prototype.forEach.call(trs, function(x,i){
|
||||
var firstChild = x.children[0]
|
||||
if (firstChild.tagName === 'TD') {
|
||||
var cell = document.createElement('td')
|
||||
cell.textContent = counter ++
|
||||
x.insertBefore(cell,firstChild)
|
||||
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
||||
filtered.sort((a, b) => (severityOrder[a.severity] || 5) - (severityOrder[b.severity] || 5));
|
||||
|
||||
filtered.forEach((f, i) => {
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
const tdNum = document.createElement("td");
|
||||
tdNum.textContent = i + 1;
|
||||
|
||||
const tdSev = document.createElement("td");
|
||||
const badge = document.createElement("span");
|
||||
badge.className = `badge badge-${f.severity || "medium"}`;
|
||||
badge.textContent = (f.severity || "medium").toUpperCase();
|
||||
tdSev.appendChild(badge);
|
||||
|
||||
const tdProvider = document.createElement("td");
|
||||
tdProvider.textContent = f.provider || "-";
|
||||
tdProvider.className = "td-provider";
|
||||
|
||||
const tdPattern = document.createElement("td");
|
||||
tdPattern.textContent = f.patternName || "-";
|
||||
tdPattern.className = "td-pattern";
|
||||
|
||||
const tdMatch = document.createElement("td");
|
||||
const matchCode = document.createElement("code");
|
||||
matchCode.textContent = f.match || "-";
|
||||
matchCode.className = "match-value";
|
||||
matchCode.title = f.match || "";
|
||||
tdMatch.appendChild(matchCode);
|
||||
|
||||
const tdType = document.createElement("td");
|
||||
const typeBadge = document.createElement("span");
|
||||
typeBadge.className = "type-badge";
|
||||
typeBadge.textContent = f.type || "-";
|
||||
tdType.appendChild(typeBadge);
|
||||
|
||||
const tdDomain = document.createElement("td");
|
||||
tdDomain.textContent = f.domain || "-";
|
||||
tdDomain.className = "td-domain";
|
||||
|
||||
const tdSource = document.createElement("td");
|
||||
if (f.url && f.url.startsWith("http")) {
|
||||
const a = document.createElement("a");
|
||||
a.href = f.url;
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener";
|
||||
a.textContent = truncateUrl(f.url, 40);
|
||||
a.title = f.url;
|
||||
tdSource.appendChild(a);
|
||||
} else {
|
||||
firstChild.setAttribute('colspan',2)
|
||||
}
|
||||
})
|
||||
})("table");
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//delete
|
||||
$('.btn').click(function() {
|
||||
var selectedItem = $(this).val();
|
||||
localStorage.removeItem(selectedItem);
|
||||
location.reload();
|
||||
alert(`${selectedItem} removed`);
|
||||
})
|
||||
|
||||
//extract domain name from URL
|
||||
function extractHostname(url) {
|
||||
var hostname;
|
||||
//find & remove protocol (http, ftp, etc.) and get hostname
|
||||
|
||||
if (url.indexOf("//") > -1) {
|
||||
hostname = url.split('/')[2];
|
||||
}
|
||||
else {
|
||||
hostname = url.split('/')[0];
|
||||
tdSource.textContent = f.url ? truncateUrl(f.url, 40) : "-";
|
||||
}
|
||||
|
||||
//find & remove port number
|
||||
hostname = hostname.split(':')[0];
|
||||
//find & remove "?"
|
||||
hostname = hostname.split('?')[0];
|
||||
const tdTime = document.createElement("td");
|
||||
tdTime.textContent = f.timestamp ? formatTime(f.timestamp) : "-";
|
||||
tdTime.className = "td-time";
|
||||
|
||||
return hostname;
|
||||
const tdActions = document.createElement("td");
|
||||
const copyBtn = document.createElement("button");
|
||||
copyBtn.className = "btn-icon";
|
||||
copyBtn.textContent = "Copy";
|
||||
copyBtn.title = "Copy match value";
|
||||
copyBtn.addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(f.match || "");
|
||||
copyBtn.textContent = "Done";
|
||||
setTimeout(() => (copyBtn.textContent = "Copy"), 1500);
|
||||
});
|
||||
|
||||
const delBtn = document.createElement("button");
|
||||
delBtn.className = "btn-icon btn-icon-danger";
|
||||
delBtn.textContent = "Del";
|
||||
delBtn.title = "Remove finding";
|
||||
delBtn.addEventListener("click", async () => {
|
||||
await chrome.runtime.sendMessage({ type: "removeFinding", url: f.url });
|
||||
allFindings = allFindings.filter((x) => x !== f);
|
||||
renderStats();
|
||||
renderFindings();
|
||||
});
|
||||
|
||||
tdActions.appendChild(copyBtn);
|
||||
tdActions.appendChild(delBtn);
|
||||
|
||||
tr.appendChild(tdNum);
|
||||
tr.appendChild(tdSev);
|
||||
tr.appendChild(tdProvider);
|
||||
tr.appendChild(tdPattern);
|
||||
tr.appendChild(tdMatch);
|
||||
tr.appendChild(tdType);
|
||||
tr.appendChild(tdDomain);
|
||||
tr.appendChild(tdSource);
|
||||
tr.appendChild(tdTime);
|
||||
tr.appendChild(tdActions);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function truncateUrl(url, max) {
|
||||
if (url.length <= max) return url;
|
||||
return url.substring(0, max - 3) + "...";
|
||||
}
|
||||
|
||||
function formatTime(ts) {
|
||||
const d = new Date(ts);
|
||||
return d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
function exportJson() {
|
||||
const filtered = getFiltered();
|
||||
const blob = new Blob([JSON.stringify(filtered, null, 2)], { type: "application/json" });
|
||||
downloadBlob(blob, `keyfinder-findings-${Date.now()}.json`);
|
||||
}
|
||||
|
||||
function exportCsv() {
|
||||
const filtered = getFiltered();
|
||||
const headers = ["Severity", "Provider", "Pattern", "Match", "Type", "Domain", "URL", "Page URL", "Timestamp"];
|
||||
const rows = filtered.map((f) => [
|
||||
f.severity || "",
|
||||
f.provider || "",
|
||||
f.patternName || "",
|
||||
`"${(f.match || "").replace(/"/g, '""')}"`,
|
||||
f.type || "",
|
||||
f.domain || "",
|
||||
f.url || "",
|
||||
f.pageUrl || "",
|
||||
f.timestamp ? new Date(f.timestamp).toISOString() : "",
|
||||
]);
|
||||
const csv = [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
|
||||
const blob = new Blob([csv], { type: "text/csv" });
|
||||
downloadBlob(blob, `keyfinder-findings-${Date.now()}.csv`);
|
||||
}
|
||||
|
||||
function downloadBlob(blob, filename) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function clearAll() {
|
||||
if (!confirm("Remove all findings?")) return;
|
||||
await chrome.runtime.sendMessage({ type: "clearFindings" });
|
||||
allFindings = [];
|
||||
renderStats();
|
||||
renderFindings();
|
||||
}
|
||||
|
||||
+29
-14
@@ -1,23 +1,38 @@
|
||||
{
|
||||
"name": "KeyFinder",
|
||||
"description": "a browser extension that searches the DOM for leaked keys🔑.",
|
||||
"version":"1.0.0",
|
||||
"manifest_version": 2,
|
||||
"browser_action":{
|
||||
"default_icon":"./css/icon.png",
|
||||
"default_popup":"popup.html"
|
||||
"description": "Passively discovers API keys, tokens, and secrets leaked in page scripts, DOM, network responses, and browser storage.",
|
||||
"version": "2.0.0",
|
||||
"manifest_version": 3,
|
||||
"action": {
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches":[
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": ["./js/results.js","./js/jquery.js","./js/content.js"]
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["js/patterns.js", "js/content.js"],
|
||||
"run_at": "document_idle",
|
||||
"all_frames": true
|
||||
},
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["js/interceptor.js"],
|
||||
"run_at": "document_start",
|
||||
"world": "MAIN",
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"scripts":["./js/background.js"]
|
||||
}
|
||||
,
|
||||
"permissions": ["activeTab","storage"]
|
||||
"service_worker": "js/background.js"
|
||||
},
|
||||
"permissions": ["activeTab", "storage"]
|
||||
}
|
||||
|
||||
+50
-48
@@ -1,52 +1,54 @@
|
||||
<!--
|
||||
▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄ ▄ ▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄
|
||||
▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░▌ ▐░▌▐░░░░░░░░░░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌
|
||||
▐░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌▐░█▀▀▀▀▀▀▀▀▀ ▀▀▀▀█░█▀▀▀▀ ▐░▌░▌ ▐░▌▐░█▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌
|
||||
▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌
|
||||
▐░▌░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌
|
||||
▐░░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌
|
||||
▐░▌░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀█░█▀▀
|
||||
▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌
|
||||
▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▄▄▄▄█░█▄▄▄▄ ▐░▌ ▐░▐░▌▐░█▄▄▄▄▄▄▄█░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌
|
||||
▐░▌ ▐░▌▐░░░░░░░░░░░▌ ▐░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░░▌▐░░░░░░░░░░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░▌
|
||||
▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀
|
||||
|
||||
|
||||
by @momenbassel
|
||||
!-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="X-UA-Compitble" content="ie=edge">
|
||||
<title>KeyFinder</title>
|
||||
<link rel="stylesheet" herf="https://fonts.googleapis.com/css?family=Open+Sans" type="text/css">
|
||||
<link rel="stylesheet" href="./css/font-awesome.min.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="modal-header">
|
||||
<h1 class="logo">
|
||||
<img src="./css/icon.png" alt="keyFinder Logo" class="logo-icon"> Key Finder <span class="version">1.00</span>
|
||||
</h1>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>KeyFinder</title>
|
||||
<link rel="stylesheet" href="css/popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-brand">
|
||||
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
|
||||
<h1>KeyFinder</h1>
|
||||
<span class="version">v2.0</span>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<p> Grab keys from the DOM scripts<p>
|
||||
</div>
|
||||
<form>
|
||||
<label for="keyword">Search:</label>
|
||||
<input type="text" id="keyword" aria-describedby="keyword" style="width:50px" placeholder="API">
|
||||
<button id="save" type="submit">add</button>
|
||||
<br>
|
||||
<small id="keywordDetails">add a keyword to search the DOM scripts for.</small>
|
||||
<script src="./js/jquery.js"></script>
|
||||
<script src="./js/popup.js"></script>
|
||||
<script src="./js/content.js"> </script>
|
||||
</div>
|
||||
</form>
|
||||
<ul>
|
||||
</ul>
|
||||
<p class="header-tagline">Passive API key & secret discovery</p>
|
||||
</header>
|
||||
|
||||
<a href="/results.html" target="_blank">Result/s</a>
|
||||
</body>
|
||||
<section class="stats" id="stats">
|
||||
<div class="stat-card">
|
||||
<span class="stat-number" id="findingCount">-</span>
|
||||
<span class="stat-label">Findings</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number" id="keywordCount">-</span>
|
||||
<span class="stat-label">Keywords</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2 class="section-title">Keywords</h2>
|
||||
<form id="keywordForm" class="keyword-form">
|
||||
<input
|
||||
type="text"
|
||||
id="keywordInput"
|
||||
placeholder="Add a keyword (e.g. api_key)"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
<button type="submit" id="addBtn">Add</button>
|
||||
</form>
|
||||
<div id="errorMsg" class="error-msg" hidden></div>
|
||||
<ul id="keywordList" class="keyword-list"></ul>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<a href="results.html" target="_blank" id="resultsLink" class="results-btn">
|
||||
View Findings
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
<script src="js/popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+61
-60
@@ -1,67 +1,68 @@
|
||||
<!--
|
||||
▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄ ▄ ▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄
|
||||
▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░▌ ▐░▌▐░░░░░░░░░░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌
|
||||
▐░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌▐░█▀▀▀▀▀▀▀▀▀ ▀▀▀▀█░█▀▀▀▀ ▐░▌░▌ ▐░▌▐░█▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌
|
||||
▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌
|
||||
▐░▌░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌
|
||||
▐░░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌
|
||||
▐░▌░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀█░█▀▀
|
||||
▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌
|
||||
▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▄▄▄▄█░█▄▄▄▄ ▐░▌ ▐░▐░▌▐░█▄▄▄▄▄▄▄█░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌
|
||||
▐░▌ ▐░▌▐░░░░░░░░░░░▌ ▐░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░░▌▐░░░░░░░░░░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░▌
|
||||
▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀
|
||||
|
||||
|
||||
by @momenbassel
|
||||
!-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="X-UA-Compitble" content="ie=edge">
|
||||
<title>KeyFinder Results</title>
|
||||
<!-- requiring/importing bootstrap -->
|
||||
<link rel="stylesheet" href="./css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="./css/style.css">
|
||||
<link rel="stylesheet" href="./css/font-awesome.min.css">
|
||||
<script src="./js/jquery.js"></script>
|
||||
<script src="./js/popper.min.js"></script>
|
||||
<script src="./js/bootstrap.min.js"></script>
|
||||
<script src="./js/results.js"></script>
|
||||
</head>
|
||||
<body class="text-center">
|
||||
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
|
||||
|
||||
<h3 class="masthead-brand">KeyFinder🔑</h3>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>KeyFinder - Findings</title>
|
||||
<link rel="stylesheet" href="css/results.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
|
||||
<h1>KeyFinder <span class="version">v2.0</span></h1>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="filter-group">
|
||||
<select id="severityFilter">
|
||||
<option value="all">All Severities</option>
|
||||
<option value="critical">Critical</option>
|
||||
<option value="high">High</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="low">Low</option>
|
||||
</select>
|
||||
<select id="typeFilter">
|
||||
<option value="all">All Types</option>
|
||||
</select>
|
||||
<select id="providerFilter">
|
||||
<option value="all">All Providers</option>
|
||||
</select>
|
||||
<input type="text" id="searchBox" placeholder="Search findings...">
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button id="exportJsonBtn" class="btn btn-secondary">Export JSON</button>
|
||||
<button id="exportCsvBtn" class="btn btn-secondary">Export CSV</button>
|
||||
<button id="clearBtn" class="btn btn-danger">Clear All</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main role="main" class="inner cover">
|
||||
<h1 class="cover-heading">Result/s:</h1>
|
||||
<p class="lead">Keyfinder🔑 is a tool that let you find keys while surfing the web!</p>
|
||||
<p class="lead">
|
||||
</p>
|
||||
<div class="stats-bar" id="statsBar"></div>
|
||||
|
||||
<main class="main">
|
||||
<table class="findings-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Severity</th>
|
||||
<th>Provider</th>
|
||||
<th>Pattern</th>
|
||||
<th>Match</th>
|
||||
<th>Type</th>
|
||||
<th>Domain</th>
|
||||
<th>Source</th>
|
||||
<th>Time</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="findingsBody"></tbody>
|
||||
</table>
|
||||
<div id="emptyState" class="empty-state" hidden>
|
||||
<div class="empty-icon">🔍</div>
|
||||
<p>No findings yet. Browse some pages and KeyFinder will passively scan for leaked secrets.</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<main role="main" class="inner cover">
|
||||
<table class="table table-dark" cellpadding="3" width="50%" height="50%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Domain</th>
|
||||
<th scope="col">URL</th>
|
||||
<th scope="col">Search Word</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</table>
|
||||
|
||||
<footer class="mastfoot mt-auto">
|
||||
<div class="inner">keyFinder🔑 by <a href="https://twitter.com/momenbassel" target="_blank">@momenbassel </a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="./js/results.js"></script>
|
||||
<script src="js/results.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user