2 Commits

Author SHA1 Message Date
moamen f9325db5ee docs: update landing page for Chrome + Firefox support 2026-04-14 15:44:13 +02:00
moamen 7b5aad4864 Add GitHub Pages landing page 2026-04-10 07:08:09 +02:00
30 changed files with 975 additions and 2586 deletions
-10
View File
@@ -1,10 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
groups:
actions-minor:
update-types: ["minor", "patch"]
-32
View File
@@ -1,32 +0,0 @@
name: CI
on:
push:
branches: [master]
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate manifest JSON
run: |
python3 -c "import json,sys; json.load(open('manifest.json'))"
python3 -c "import json,sys; json.load(open('manifest.firefox.json'))"
- name: Manifest versions must match
run: |
C=$(python3 -c "import json; print(json.load(open('manifest.json'))['version'])")
F=$(python3 -c "import json; print(json.load(open('manifest.firefox.json'))['version'])")
[ "$C" = "$F" ] || { echo "Chrome=$C Firefox=$F"; exit 1; }
- name: Build runs cleanly
run: bash scripts/build.sh
- name: Install web-ext
run: npm install -g web-ext
- name: web-ext lint (Firefox)
run: web-ext lint --source-dir dist/firefox --self-hosted
-71
View File
@@ -1,71 +0,0 @@
name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Tag to build (e.g. v2.1.0). Leave blank to build current ref."
required: false
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag || github.ref }}
- name: Read version from manifest
id: meta
run: |
VERSION=$(grep '"version"' manifest.json | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
- name: Verify tag matches manifest version
if: startsWith(github.ref, 'refs/tags/v')
run: |
TAG="${GITHUB_REF#refs/tags/}"
if [ "$TAG" != "${{ steps.meta.outputs.tag }}" ] && [ "$TAG" != "${{ steps.meta.outputs.tag }}-firefox" ]; then
echo "Tag $TAG does not match manifest version ${{ steps.meta.outputs.tag }}"
exit 1
fi
- name: Build Chrome + Firefox zips
run: bash scripts/build.sh
- name: Compute checksums
working-directory: dist
run: |
shasum -a 256 keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip > keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip.sha256
shasum -a 256 keyfinder-v${{ steps.meta.outputs.version }}-firefox.zip > keyfinder-v${{ steps.meta.outputs.version }}-firefox.zip.sha256
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: keyfinder-v${{ steps.meta.outputs.version }}
path: |
dist/keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip
dist/keyfinder-v${{ steps.meta.outputs.version }}-firefox.zip
dist/keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip.sha256
dist/keyfinder-v${{ steps.meta.outputs.version }}-firefox.zip.sha256
if-no-files-found: error
- name: Attach to GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
files: |
dist/keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip
dist/keyfinder-v${{ steps.meta.outputs.version }}-firefox.zip
dist/keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip.sha256
dist/keyfinder-v${{ steps.meta.outputs.version }}-firefox.zip.sha256
generate_release_notes: true
fail_on_unmatched_files: true
-11
View File
@@ -1,11 +0,0 @@
.DS_Store
*.crx
*.pem
*.zip
dist/
.idea/
.vscode/
*.swp
*.swo
*~
.claude/
-56
View File
@@ -1,56 +0,0 @@
# Changelog
All notable changes to KeyFinder are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [SemVer](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- `SECURITY.md` with threat model, disclosure policy, and known limitations of the MAIN <-> ISOLATED nonce bridge
- `.github/dependabot.yml` for weekly GitHub Actions version bumps
- `CHANGELOG.md`
### Changed
- CSV export sanitiser now also prefixes cells starting with LF (`\n`), not just `=`, `+`, `-`, `@`, tab, CR
- Popup and results page version label is now read from the manifest at runtime instead of being hardcoded
- Window-global scan in `js/interceptor.js` now runs at `document_start`, `DOMContentLoaded`, and `load`, with per-name dedupe. The previous implementation only scanned at `document_start` when page globals had not yet been assigned, making the entire pass dead code on most real pages
## [2.1.0] - 2026-04-14
### Added
- Per-session nonce validation between MAIN-world interceptor and ISOLATED content script to prevent forged finding injection
- CSV formula-injection sanitiser on findings export
- Serialised storage writes to eliminate cross-tab race conditions
- 5000-finding cap with FIFO eviction
- Per-tab alert badge with red-dot icon overlay when secrets are detected
- MutationObserver scans dynamically-injected DOM nodes for SPA coverage
- Explicit Content Security Policy in Chrome and Firefox manifests
- `js/interceptor-loader.js` for both browsers, replacing direct MAIN-world content script on Firefox so the nonce handoff actually works
- GitHub Actions release pipeline (`.github/workflows/release.yml`): on `v*` tag, build Chrome + Firefox zips, compute SHA256, attach to GitHub Release
- GitHub Actions CI pipeline (`.github/workflows/ci.yml`): manifest JSON validation, Chrome <-> Firefox version parity check, build verification, `web-ext lint` on the Firefox bundle
### Changed
- Keyword input validation: 50 character maximum, 50 keyword maximum
- Findings are now deleted by unique ID instead of URL substring match
- URL parameter scanner uses exact match instead of substring (was matching `author` as `auth`)
- Keyword scanner enforces word boundaries (was matching `key` inside `hotkey`, `monkey`)
- camelCase JS identifiers are now skipped in keyword value matches
- Sentry DSN downgraded from `high` to `low` severity (public by design)
### Fixed
- Stored finding race conditions across concurrent tabs
- False positives from GitHub localStorage caches (`ref-selector:*`, `jump_to:*`, `soft-nav:*`, `COPILOT_*`)
- False positives from common CSRF tokens (`authenticity_token`, `csrf_token`, `__RequestVerificationToken`)
- False positives from keyboard shortcut data attributes (`data-hotkey`, `data-hotkey-scope`)
## [2.0.0] - 2026-04-07
### Added
- Complete rewrite to Manifest V3
- Enterprise-grade secret detection with 80+ regex patterns covering AWS, GCP, Azure, GitHub, GitLab, Stripe, PayPal, Square, Slack, Discord, and more
- Firefox support (MV3, Firefox 128+)
- Privacy policy
- Replaced demo gifs with professional logo
### Removed
- Manifest V2 background page
- Legacy jQuery dependency
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 Mo'men Basel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-61
View File
@@ -1,61 +0,0 @@
# Privacy Policy - KeyFinder Chrome Extension
**Last Updated:** April 7, 2026
## Overview
KeyFinder is a browser extension that scans web pages for leaked API keys, tokens, and secrets. This privacy policy explains how the extension handles data.
## Data Collection
KeyFinder does **not** collect, transmit, or share any personal data or browsing data with external servers. The extension operates entirely on your local device.
## What Data is Stored Locally
The extension stores the following data locally on your device using Chrome's built-in storage API (`chrome.storage.local`):
- **Search keywords**: User-configured keywords used to identify potential secrets on web pages.
- **Scan findings**: When a potential secret is detected, the extension stores the pattern name, severity level, matched value, page URL, and domain where it was found.
This data never leaves your device.
## How Data is Processed
- All page scanning happens locally within your browser.
- The extension reads page content (scripts, meta tags, form fields, HTML comments, browser storage, and network responses) to match against known secret patterns.
- No page content, scan results, or browsing activity is sent to any external server, API, or third party.
## Data Sharing
KeyFinder does **not** share any data with third parties. Specifically:
- No data is sold to third parties.
- No data is used for advertising or marketing purposes.
- No data is transferred to third parties for reasons unrelated to the extension's core functionality.
- No analytics, telemetry, or tracking is implemented.
## Data Retention
All stored data remains on your local device until you choose to delete it. You can clear all findings at any time using the "Clear All" button in the findings dashboard. Uninstalling the extension removes all stored data.
## Permissions
- **activeTab**: Used to access the current page's content for scanning when the extension is active.
- **storage**: Used to save your keyword preferences and scan findings locally.
- **Host permissions**: The extension runs on all URLs because leaked secrets can appear on any website. No data from these pages is transmitted externally.
## Third-Party Services
KeyFinder does not integrate with, connect to, or send data to any third-party services.
## Changes to This Policy
Any changes to this privacy policy will be reflected in the extension's GitHub repository and the "Last Updated" date above.
## Contact
If you have questions about this privacy policy, contact the developer:
- GitHub: [github.com/momenbasel](https://github.com/momenbasel)
- X: [@momenbassel](https://x.com/momenbassel)
- LinkedIn: [linkedin.com/in/momenbasel](https://www.linkedin.com/in/momenbasel/)
-145
View File
@@ -1,145 +0,0 @@
<p align="center">
<img width="128" height="128" alt="KeyFinder logo" src="https://raw.githubusercontent.com/momenbasel/keyFinder/master/icons/icon128.png">
</p>
<h1 align="center">KeyFinder</h1>
<p align="center">
<strong>Passive API key and secret discovery for Chrome and Firefox</strong>
</p>
<p align="center">
<img src="https://img.shields.io/badge/manifest-v3-blue"/>
<a href="https://chromewebstore.google.com/detail/keyfinder/oogbljkilkfgdlkbmajlolpanilnnkim"><img src="https://img.shields.io/badge/Chrome-Extension-green"/></a>
<a href="https://addons.mozilla.org/en-US/firefox/addon/keyfinder-original/"><img src="https://img.shields.io/badge/Firefox-Add--on-orange"/></a>
<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>
<hr>
KeyFinder is a browser extension for Chrome and Firefox 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 and Firefox 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
### Chrome
- [Install from Chrome Web Store](https://chromewebstore.google.com/detail/keyfinder/oogbljkilkfgdlkbmajlolpanilnnkim)
### Firefox
- [Install from Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/keyfinder-original/)
### From Source
```bash
git clone https://github.com/momenbasel/keyFinder.git
```
**Chrome:**
1. Go to `chrome://extensions`
2. Enable **Developer mode**
3. Click **Load unpacked** and select the `keyFinder` folder
**Firefox:**
1. Go to `about:debugging` > "This Firefox"
2. Click **Load Temporary Add-on**
3. Select `manifest.firefox.json` from the `keyFinder` folder
## 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 # Chrome MV3 manifest
manifest.firefox.json # Firefox 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
interceptor.js # XHR/Fetch hooking and window global scanning
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
scripts/
build.sh # Build Chrome and Firefox zip packages
```
## Disclaimer
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
MIT
## Author
[@momenbassel](https://x.com/momenbassel) - [LinkedIn](https://www.linkedin.com/in/momenbasel/)
-57
View File
@@ -1,57 +0,0 @@
# Security Policy
## Reporting a vulnerability
Email **security reports** privately to the address on the maintainer's GitHub profile.
Do **not** open a public issue for unpatched vulnerabilities.
When reporting, include:
- Affected version (`manifest.json` `version` field)
- Browser and version
- Steps to reproduce
- Impact assessment (what an attacker gains)
A response is targeted within 7 days.
## Threat model
KeyFinder runs as a content script in every page the user visits and reports findings to a service worker. It is **client-side, passive, and read-only** with respect to the page.
### In scope
- Privilege escalation from a malicious page into the extension's service worker
- Persistent storage poisoning via crafted findings
- Cross-tab data leakage through `chrome.storage`
- CSV / JSON export injection (formulas, embedded HTML, JS)
- Manifest / CSP weaknesses enabling code injection into the extension's own pages
- Pattern-rule false positives that consistently leak benign data into findings
### Out of scope
- A malicious page generating **fake findings** in the user's results view. The MAIN-world interceptor and the ISOLATED content script communicate over `CustomEvent` with a per-page nonce stored as a `data-kf-verify` attribute on `documentElement`. The nonce is removed once the content script consumes it, but a page script that runs between `document_start` and `document_idle` can read the attribute and forge events. Mitigation cost is high (Symbols don't cross realms; postMessage is also page-visible). The impact is limited to **showing the user a finding that isn't real** - no data is exfiltrated, no privileged API is reached. Treat findings on a hostile page as advisory.
- Detection accuracy of individual regex rules. False positives and false negatives are expected; report a tuning issue rather than a CVE.
- Extension being uninstalled or disabled by the user.
## What the extension can see
| Surface | Scope |
|---|---|
| Page DOM | All pages (`<all_urls>`) at `document_idle` (ISOLATED world) and `document_start` (MAIN world interceptor) |
| Network | `fetch` and `XMLHttpRequest` responses initiated by the page (response bodies up to 500 KB are scanned) |
| Web storage | `localStorage`, `sessionStorage`, `document.cookie` on the page |
| Inter-extension | None. No host permissions beyond `activeTab` and `storage` |
The extension makes **no outbound network requests** other than fetching same-origin scripts already referenced by the page.
## Known limitations
- **Per-tab badge dot** is set on the first finding only; subsequent findings update the count but not the icon
- **5000 finding cap** with FIFO eviction. High-volume scans (heavy SPAs over a long session) will drop oldest findings
- **CSV export** prefixes a single-quote on cells starting with `=`, `+`, `-`, `@`, tab, or carriage return to neutralise Excel / Sheets formula injection. Line-feed prefix is not currently neutralised
- **Service worker restarts** drop the in-flight `storageQueue` Promise chain. Subsequent storage writes are still serialised; only pending writes from the killed worker are lost
## Supported versions
| Version | Supported |
|---|---|
| 2.1.x | yes |
| 2.0.x | no |
| < 2.0 | no |
-240
View File
@@ -1,240 +0,0 @@
* {
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
View File
@@ -1,341 +0,0 @@
* {
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;
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

+975
View File
@@ -0,0 +1,975 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>keyFinder - Find Leaked API Keys, Tokens & Secrets in Your Browser</title>
<meta name="description" content="Browser extension for Chrome and Firefox that passively finds leaked API keys, tokens, and secrets on every page you visit. 80+ detection patterns. 10 attack surfaces. Zero config. Manifest V3.">
<meta name="keywords" content="browser extension secret scanner, api key finder, leaked credentials detector, secret detection, api keys, api security, chrome extension, firefox addon, bug bounty, pentesting, infosec, javascript, manifest v3, osint, reconnaissance, secrets, aws keys, cybersecurity, hacking, leaked credentials, secret scanner, security tool">
<meta name="author" content="Moamen Basel">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://momenbasel.github.io/keyFinder/">
<!-- Open Graph -->
<meta property="og:title" content="keyFinder - Passively Find Leaked Secrets in Your Browser">
<meta property="og:description" content="Browser extension for Chrome and Firefox that scans every page for leaked API keys, tokens, and credentials. 80+ patterns. 10 attack surfaces. Zero config.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://momenbasel.github.io/keyFinder/">
<meta property="og:image" content="https://raw.githubusercontent.com/momenbasel/keyFinder/master/icons/social-preview.png">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@momenbassel">
<meta name="twitter:creator" content="@momenbassel">
<meta name="twitter:title" content="keyFinder - Passively Find Leaked Secrets in Your Browser">
<meta name="twitter:description" content="Browser extension for Chrome and Firefox scanning every page for leaked API keys, tokens, and credentials. 80+ patterns. 10 attack surfaces. Zero config.">
<meta name="twitter:image" content="https://raw.githubusercontent.com/momenbasel/keyFinder/master/icons/social-preview.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="https://raw.githubusercontent.com/momenbasel/keyFinder/master/icons/icon128.png">
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "keyFinder",
"applicationCategory": "BrowserExtension",
"operatingSystem": "Chrome, Firefox, Chromium",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
"description": "Browser extension for Chrome and Firefox that passively finds leaked API keys, tokens, and secrets on every page you visit. 80+ detection patterns across 10 attack surfaces with zero configuration.",
"url": "https://momenbasel.github.io/keyFinder/",
"downloadUrl": "https://github.com/momenbasel/keyFinder/releases",
"author": { "@type": "Person", "name": "Moamen Basel", "url": "https://github.com/momenbasel" },
"license": "https://opensource.org/licenses/MIT",
"programmingLanguage": "JavaScript",
"browserRequirements": "Requires Chrome, Firefox 128+, or Chromium-based browser with Manifest V3 support"
}
</script>
<style>
:root {
--bg: #0a0e17;
--bg-card: #111827;
--bg-code: #0d1520;
--border: #1e293b;
--text: #e2e8f0;
--text-dim: #94a3b8;
--text-muted: #64748b;
--accent: #10b981;
--accent-secondary: #22d3ee;
--accent-glow: rgba(16, 185, 129, 0.15);
--accent-glow-secondary: rgba(34, 211, 238, 0.1);
--green: #34d399;
--yellow: #fbbf24;
--purple: #a78bfa;
--pink: #f472b6;
--red: #f87171;
--cyan: #22d3ee;
--radius: 12px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.7;
overflow-x: hidden;
}
a { color: var(--accent); text-decoration: none; transition: opacity 0.2s; }
a:hover { opacity: 0.85; }
code, pre {
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace;
}
/* ---- NAV ---- */
nav {
position: fixed; top: 0; width: 100%; z-index: 100;
background: rgba(10, 14, 23, 0.8);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border);
padding: 0 2rem;
}
.nav-inner {
max-width: 1200px; margin: 0 auto;
display: flex; align-items: center; justify-content: space-between;
height: 64px;
}
.nav-logo {
display: flex; align-items: center; gap: 10px;
font-weight: 700; font-size: 1.1rem; color: var(--text);
}
.nav-logo img { height: 28px; width: 28px; border-radius: 6px; }
.nav-links { display: flex; gap: 2rem; align-items: center; }
.nav-links a { color: var(--text-dim); font-size: 0.9rem; font-weight: 500; }
.nav-links a:hover { color: var(--text); }
.nav-links .btn-sm {
background: var(--accent);
color: #0a0e17;
padding: 6px 16px;
border-radius: 8px;
font-weight: 600;
font-size: 0.85rem;
}
.nav-links .btn-sm:hover { opacity: 0.9; }
/* ---- HERO ---- */
.hero {
min-height: 100vh;
display: flex; align-items: center; justify-content: center;
padding: 120px 2rem 80px;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: -40%; left: 50%; transform: translateX(-50%);
width: 800px; height: 800px;
background: radial-gradient(circle, var(--accent-glow) 0%, transparent 70%);
pointer-events: none;
}
.hero-content {
max-width: 800px;
text-align: center;
position: relative; z-index: 1;
}
.hero-badge {
display: inline-flex; align-items: center; gap: 8px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 100px;
padding: 6px 16px;
font-size: 0.82rem;
color: var(--text-dim);
margin-bottom: 2rem;
}
.hero-badge span { color: var(--accent); font-weight: 600; }
.hero h1 {
font-size: clamp(2.5rem, 6vw, 4rem);
font-weight: 800;
line-height: 1.15;
letter-spacing: -0.03em;
margin-bottom: 1.25rem;
}
.hero h1 .gradient {
background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero p {
font-size: 1.2rem;
color: var(--text-dim);
max-width: 600px;
margin: 0 auto 2.5rem;
}
.hero-actions {
display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;
}
.btn {
display: inline-flex; align-items: center; gap: 8px;
padding: 12px 28px;
border-radius: 10px;
font-weight: 600;
font-size: 0.95rem;
transition: all 0.2s;
cursor: pointer;
border: none;
}
.btn-primary {
background: var(--accent);
color: #0a0e17;
}
.btn-primary:hover { box-shadow: 0 0 30px var(--accent-glow); transform: translateY(-1px); color: #0a0e17; }
.btn-secondary {
background: var(--bg-card);
color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover { border-color: var(--text-muted); transform: translateY(-1px); }
/* ---- SURFACES VISUAL ---- */
.surfaces-visual {
margin-top: 4rem;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
max-width: 640px;
margin-left: auto;
margin-right: auto;
}
.surface-chip {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px 8px;
text-align: center;
transition: border-color 0.3s, transform 0.2s;
}
.surface-chip:hover { border-color: var(--accent); transform: translateY(-2px); }
.surface-chip .s-icon {
font-size: 1.4rem;
margin-bottom: 6px;
display: block;
color: var(--accent);
}
.surface-chip .s-icon svg { width: 24px; height: 24px; display: inline-block; }
.surface-chip .s-label {
font-size: 0.7rem;
color: var(--text-dim);
line-height: 1.3;
font-weight: 500;
}
/* ---- SECTIONS ---- */
section {
padding: 100px 2rem;
max-width: 1200px;
margin: 0 auto;
}
.section-label {
text-transform: uppercase;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--accent);
margin-bottom: 0.75rem;
}
.section-title {
font-size: clamp(1.8rem, 4vw, 2.5rem);
font-weight: 800;
letter-spacing: -0.02em;
margin-bottom: 1rem;
}
.section-desc {
color: var(--text-dim);
font-size: 1.05rem;
max-width: 600px;
margin-bottom: 3rem;
}
/* ---- FEATURES GRID ---- */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.25rem;
}
.feature-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 28px;
transition: border-color 0.3s, transform 0.2s;
}
.feature-card:hover { border-color: var(--accent); transform: translateY(-2px); }
.feature-icon {
width: 44px; height: 44px;
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 1.3rem;
margin-bottom: 16px;
}
.feature-icon.emerald { background: rgba(16, 185, 129, 0.12); color: var(--accent); }
.feature-icon.cyan { background: rgba(34, 211, 238, 0.12); color: var(--cyan); }
.feature-icon.purple { background: rgba(167, 139, 250, 0.12); color: var(--purple); }
.feature-icon.yellow { background: rgba(251, 191, 36, 0.12); color: var(--yellow); }
.feature-icon.pink { background: rgba(244, 114, 182, 0.12); color: var(--pink); }
.feature-icon.red { background: rgba(248, 113, 113, 0.12); color: var(--red); }
.feature-card h3 {
font-size: 1.05rem; font-weight: 700;
margin-bottom: 8px;
}
.feature-card p {
color: var(--text-dim); font-size: 0.9rem; line-height: 1.6;
}
/* ---- DETECTION COVERAGE ---- */
.detection-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.25rem;
}
.detection-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
transition: border-color 0.3s;
}
.detection-card:hover { border-color: var(--accent); }
.detection-card h4 {
font-size: 0.95rem;
font-weight: 700;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.detection-card h4 .d-dot {
width: 8px; height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.detection-card .providers-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.detection-card .prov-tag {
display: inline-block;
background: rgba(16, 185, 129, 0.08);
border: 1px solid rgba(16, 185, 129, 0.15);
color: var(--text-dim);
padding: 3px 10px;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 500;
}
/* ---- ATTACK SURFACES ---- */
.surfaces-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
gap: 1rem;
}
.surface-item {
display: flex;
align-items: flex-start;
gap: 16px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px 24px;
transition: border-color 0.3s;
}
.surface-item:hover { border-color: var(--accent); }
.surface-num {
flex-shrink: 0;
width: 32px; height: 32px;
border-radius: 8px;
background: rgba(16, 185, 129, 0.12);
color: var(--accent);
display: flex; align-items: center; justify-content: center;
font-weight: 700;
font-size: 0.85rem;
}
.surface-item h4 {
font-size: 0.95rem;
font-weight: 700;
margin-bottom: 4px;
}
.surface-item p {
color: var(--text-dim);
font-size: 0.85rem;
line-height: 1.5;
}
/* ---- INSTALL SECTION ---- */
.install-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
gap: 1.5rem;
}
.install-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.install-header {
padding: 18px 24px;
border-bottom: 1px solid var(--border);
font-weight: 700;
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 10px;
}
.install-header .tag {
display: inline-block;
background: rgba(16, 185, 129, 0.1);
color: var(--accent);
padding: 2px 8px;
border-radius: 6px;
font-size: 0.72rem;
font-weight: 600;
}
.install-body {
padding: 20px 24px;
}
.install-step {
display: flex;
gap: 12px;
padding: 8px 0;
align-items: flex-start;
}
.install-step .step-num {
flex-shrink: 0;
width: 24px; height: 24px;
border-radius: 6px;
background: rgba(16, 185, 129, 0.1);
color: var(--accent);
display: flex; align-items: center; justify-content: center;
font-weight: 700;
font-size: 0.72rem;
}
.install-step .step-text {
color: var(--text-dim);
font-size: 0.88rem;
line-height: 1.6;
}
.install-step .step-text code {
background: var(--bg-code);
border: 1px solid var(--border);
padding: 1px 6px;
border-radius: 4px;
font-size: 0.82rem;
color: var(--text);
}
/* ---- STATS ---- */
.stats-row {
display: flex; gap: 2rem; justify-content: center; flex-wrap: wrap;
margin-top: 4rem;
}
.stat {
text-align: center;
min-width: 140px;
}
.stat-val {
font-size: 2.2rem;
font-weight: 800;
background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-label {
font-size: 0.85rem;
color: var(--text-muted);
margin-top: 4px;
}
/* ---- CTA ---- */
.cta {
text-align: center;
padding: 100px 2rem;
position: relative;
}
.cta::before {
content: '';
position: absolute;
bottom: 0; left: 50%; transform: translateX(-50%);
width: 600px; height: 400px;
background: radial-gradient(circle, var(--accent-glow) 0%, transparent 70%);
pointer-events: none;
}
.cta h2 {
font-size: clamp(1.8rem, 4vw, 2.5rem);
font-weight: 800;
margin-bottom: 1rem;
}
.cta p {
color: var(--text-dim);
font-size: 1.05rem;
max-width: 500px;
margin: 0 auto 2rem;
}
/* ---- FOOTER ---- */
footer {
border-top: 1px solid var(--border);
padding: 40px 2rem;
text-align: center;
color: var(--text-muted);
font-size: 0.85rem;
}
footer a { color: var(--text-dim); }
.footer-links {
display: flex; gap: 2rem; justify-content: center;
margin-bottom: 1rem;
}
/* ---- RESPONSIVE ---- */
@media (max-width: 768px) {
.nav-links a:not(.btn-sm) { display: none; }
.features-grid { grid-template-columns: 1fr; }
.detection-grid { grid-template-columns: 1fr; }
.surfaces-list { grid-template-columns: 1fr; }
.install-grid { grid-template-columns: 1fr; }
.surfaces-visual { grid-template-columns: repeat(3, 1fr); }
section { padding: 60px 1.25rem; }
}
@media (max-width: 480px) {
.surfaces-visual { grid-template-columns: repeat(2, 1fr); }
}
/* ---- ANIMATIONS ---- */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate {
opacity: 0;
animation: fadeUp 0.6s ease forwards;
}
.animate.d1 { animation-delay: 0.1s; }
.animate.d2 { animation-delay: 0.2s; }
.animate.d3 { animation-delay: 0.3s; }
.animate.d4 { animation-delay: 0.4s; }
</style>
</head>
<body>
<!-- NAV -->
<nav>
<div class="nav-inner">
<a href="#" class="nav-logo">
<img src="https://raw.githubusercontent.com/momenbasel/keyFinder/master/icons/icon128.png" alt="keyFinder">
keyFinder
</a>
<div class="nav-links">
<a href="#features">Features</a>
<a href="#detection">Detection</a>
<a href="#surfaces">Surfaces</a>
<a href="#install">Install</a>
<a href="https://github.com/momenbasel/keyFinder" class="btn-sm">GitHub</a>
</div>
</div>
</nav>
<!-- HERO -->
<section class="hero">
<div class="hero-content">
<div class="hero-badge animate">
<span>558+ stars</span> Trusted by security researchers
</div>
<h1 class="animate d1">
Find leaked secrets,<br>
<span class="gradient">passively.</span>
</h1>
<p class="animate d2">
A browser extension for Chrome and Firefox that scans every page you visit for API keys, tokens, and credentials.
80+ patterns. 10 attack surfaces. Zero config.
</p>
<div class="hero-actions animate d3">
<a href="https://chromewebstore.google.com/detail/keyfinder/oogbljkilkfgdlkbmajlolpanilnnkim" class="btn btn-primary">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="8" cy="8" r="3"/></svg>
Chrome Web Store
</a>
<a href="https://addons.mozilla.org/en-US/firefox/addon/keyfinder-original/" class="btn btn-primary" style="background: #ff7139; color: #fff;">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M14.4 4.8c-.5-1.2-1.5-2.2-2.7-2.9.3.6.5 1.2.5 1.9 0 1.5-.9 2.8-2.2 3.4.1-.4.2-.9.2-1.4 0-2-1.3-3.6-3.1-4.2.3.5.5 1.1.5 1.8 0 1.9-1.3 3.4-3 3.8C3 7.7 2 9.3 2 11.1 2 13.8 4.7 16 8 16s6-2.2 6-5c0-2.3-1-4.3-1.6-5.2-.5.5-1.1.9-1.8 1.1.9-.7 1.5-1.7 1.8-2.1z"/></svg>
Firefox Add-ons
</a>
<a href="https://github.com/momenbasel/keyFinder" class="btn btn-secondary">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
View Source
</a>
</div>
<!-- 10 Attack Surfaces Visual -->
<div class="surfaces-visual animate d4">
<div class="surface-chip">
<span class="s-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg></span>
<span class="s-label">Script URLs</span>
</div>
<div class="surface-chip">
<span class="s-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg></span>
<span class="s-label">Inline Scripts</span>
</div>
<div class="surface-chip">
<span class="s-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 2H3v16h5v4l4-4h5l4-4V2z"/></svg></span>
<span class="s-label">External JS</span>
</div>
<div class="surface-chip">
<span class="s-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/></svg></span>
<span class="s-label">Meta Tags</span>
</div>
<div class="surface-chip">
<span class="s-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg></span>
<span class="s-label">Hidden Fields</span>
</div>
<div class="surface-chip">
<span class="s-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg></span>
<span class="s-label">Data Attrs</span>
</div>
<div class="surface-chip">
<span class="s-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg></span>
<span class="s-label">Comments</span>
</div>
<div class="surface-chip">
<span class="s-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg></span>
<span class="s-label">URL Params</span>
</div>
<div class="surface-chip">
<span class="s-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg></span>
<span class="s-label">Web Storage</span>
</div>
<div class="surface-chip">
<span class="s-icon"><svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span>
<span class="s-label">Network</span>
</div>
</div>
</div>
</section>
<!-- FEATURES -->
<section id="features">
<div class="section-label">Features</div>
<h2 class="section-title">Powerful detection, zero friction</h2>
<p class="section-desc">Install once. Every page you visit is automatically scanned for leaked secrets across all attack surfaces.</p>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon emerald">
<svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/><path d="M9 14l2 2 4-4"/></svg>
</div>
<h3>80+ Detection Patterns</h3>
<p>Covers cloud providers, payment platforms, communication tools, AI services, databases, SaaS, infrastructure keys, crypto secrets, and generic patterns.</p>
</div>
<div class="feature-card">
<div class="feature-icon cyan">
<svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
</div>
<h3>10 Attack Surfaces</h3>
<p>Scans script URLs, inline scripts, external JS, meta tags, hidden form fields, data attributes, HTML comments, URL parameters, web storage, and network responses.</p>
</div>
<div class="feature-card">
<div class="feature-icon purple">
<svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
</div>
<h3>Shannon Entropy Analysis</h3>
<p>Calculates Shannon entropy for detected strings. High-entropy values get flagged as likely secrets, reducing false positives on random-looking tokens.</p>
</div>
<div class="feature-card">
<div class="feature-icon yellow">
<svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
</div>
<h3>Zero Dependencies</h3>
<p>Pure vanilla JavaScript. No external libraries, no build step, no framework. Lightweight, fast, and auditable. Just the extension code and nothing else.</p>
</div>
<div class="feature-card">
<div class="feature-icon pink">
<svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</div>
<h3>Manifest V3</h3>
<p>Built on the latest extension architecture with MV3. Works on Chrome, Firefox 128+, and all modern Chromium browsers out of the box.</p>
</div>
<div class="feature-card">
<div class="feature-icon red">
<svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
</div>
<h3>Export and Report</h3>
<p>Professional dashboard with filtering, sorting, and search. Export findings as JSON or CSV. Badge counter on the extension icon shows live results.</p>
</div>
</div>
<div class="stats-row">
<div class="stat">
<div class="stat-val">80+</div>
<div class="stat-label">Detection Patterns</div>
</div>
<div class="stat">
<div class="stat-val">10</div>
<div class="stat-label">Attack Surfaces</div>
</div>
<div class="stat">
<div class="stat-val">558+</div>
<div class="stat-label">GitHub Stars</div>
</div>
<div class="stat">
<div class="stat-val">0</div>
<div class="stat-label">Dependencies</div>
</div>
</div>
</section>
<!-- DETECTION COVERAGE -->
<section id="detection">
<div class="section-label">Detection Coverage</div>
<h2 class="section-title">Secrets across every category</h2>
<p class="section-desc">From cloud provider keys to cryptocurrency wallet seeds, keyFinder recognizes credentials across the entire modern stack.</p>
<div class="detection-grid">
<div class="detection-card">
<h4><span class="d-dot" style="background: var(--accent)"></span>Cloud</h4>
<div class="providers-list">
<span class="prov-tag">AWS</span>
<span class="prov-tag">GCP</span>
<span class="prov-tag">Azure</span>
<span class="prov-tag">DigitalOcean</span>
<span class="prov-tag">Heroku</span>
</div>
</div>
<div class="detection-card">
<h4><span class="d-dot" style="background: var(--cyan)"></span>Source Control</h4>
<div class="providers-list">
<span class="prov-tag">GitHub</span>
<span class="prov-tag">GitLab</span>
<span class="prov-tag">Bitbucket</span>
</div>
</div>
<div class="detection-card">
<h4><span class="d-dot" style="background: var(--yellow)"></span>Payments</h4>
<div class="providers-list">
<span class="prov-tag">Stripe</span>
<span class="prov-tag">PayPal</span>
<span class="prov-tag">Square</span>
<span class="prov-tag">Braintree</span>
</div>
</div>
<div class="detection-card">
<h4><span class="d-dot" style="background: var(--purple)"></span>Communication</h4>
<div class="providers-list">
<span class="prov-tag">Slack</span>
<span class="prov-tag">Discord</span>
<span class="prov-tag">Telegram</span>
<span class="prov-tag">Twilio</span>
<span class="prov-tag">SendGrid</span>
</div>
</div>
<div class="detection-card">
<h4><span class="d-dot" style="background: var(--pink)"></span>AI / ML</h4>
<div class="providers-list">
<span class="prov-tag">OpenAI</span>
<span class="prov-tag">Anthropic</span>
<span class="prov-tag">Cohere</span>
<span class="prov-tag">HuggingFace</span>
</div>
</div>
<div class="detection-card">
<h4><span class="d-dot" style="background: var(--red)"></span>Databases</h4>
<div class="providers-list">
<span class="prov-tag">MongoDB</span>
<span class="prov-tag">PostgreSQL</span>
<span class="prov-tag">Redis</span>
<span class="prov-tag">MySQL</span>
</div>
</div>
<div class="detection-card">
<h4><span class="d-dot" style="background: var(--green)"></span>SaaS</h4>
<div class="providers-list">
<span class="prov-tag">Mailchimp</span>
<span class="prov-tag">Algolia</span>
<span class="prov-tag">Firebase</span>
<span class="prov-tag">Shopify</span>
<span class="prov-tag">Zendesk</span>
</div>
</div>
<div class="detection-card">
<h4><span class="d-dot" style="background: #f59e0b"></span>Infrastructure</h4>
<div class="providers-list">
<span class="prov-tag">Docker</span>
<span class="prov-tag">Kubernetes</span>
<span class="prov-tag">Terraform</span>
<span class="prov-tag">Vault</span>
<span class="prov-tag">NPM</span>
</div>
</div>
<div class="detection-card">
<h4><span class="d-dot" style="background: #fb923c"></span>Crypto</h4>
<div class="providers-list">
<span class="prov-tag">Private Keys</span>
<span class="prov-tag">Seed Phrases</span>
<span class="prov-tag">Wallet Keys</span>
<span class="prov-tag">JWT Secrets</span>
</div>
</div>
<div class="detection-card">
<h4><span class="d-dot" style="background: var(--text-muted)"></span>Generic</h4>
<div class="providers-list">
<span class="prov-tag">API Keys</span>
<span class="prov-tag">Bearer Tokens</span>
<span class="prov-tag">Basic Auth</span>
<span class="prov-tag">Passwords in URLs</span>
<span class="prov-tag">High Entropy</span>
</div>
</div>
</div>
</section>
<!-- ATTACK SURFACES -->
<section id="surfaces">
<div class="section-label">Attack Surfaces</div>
<h2 class="section-title">10 scanning vectors per page</h2>
<p class="section-desc">Every page load triggers a comprehensive scan across all the places where secrets commonly leak.</p>
<div class="surfaces-list">
<div class="surface-item">
<div class="surface-num">1</div>
<div>
<h4>Script src URLs</h4>
<p>Examines URLs in script tags for embedded API keys and tokens passed as query parameters.</p>
</div>
</div>
<div class="surface-item">
<div class="surface-num">2</div>
<div>
<h4>Inline Scripts</h4>
<p>Parses all inline JavaScript blocks on the page for hardcoded credentials and secret assignments.</p>
</div>
</div>
<div class="surface-item">
<div class="surface-num">3</div>
<div>
<h4>External Scripts</h4>
<p>Fetches and analyzes external JavaScript files loaded by the page for leaked keys and tokens.</p>
</div>
</div>
<div class="surface-item">
<div class="surface-num">4</div>
<div>
<h4>Meta Tags</h4>
<p>Inspects meta tag content attributes where configuration keys and tokens are sometimes exposed.</p>
</div>
</div>
<div class="surface-item">
<div class="surface-num">5</div>
<div>
<h4>Hidden Form Fields</h4>
<p>Scans hidden input fields that developers use to pass tokens and API keys through forms.</p>
</div>
</div>
<div class="surface-item">
<div class="surface-num">6</div>
<div>
<h4>Data Attributes</h4>
<p>Checks HTML data-* attributes where frontend frameworks often store configuration secrets.</p>
</div>
</div>
<div class="surface-item">
<div class="surface-num">7</div>
<div>
<h4>HTML Comments</h4>
<p>Extracts and scans HTML comments for accidentally committed credentials and debug tokens.</p>
</div>
</div>
<div class="surface-item">
<div class="surface-num">8</div>
<div>
<h4>URL Parameters</h4>
<p>Analyzes query strings and URL fragments for API keys and authentication tokens passed in the clear.</p>
</div>
</div>
<div class="surface-item">
<div class="surface-num">9</div>
<div>
<h4>Web Storage</h4>
<p>Monitors localStorage and sessionStorage for secrets stored client-side by web applications.</p>
</div>
</div>
<div class="surface-item">
<div class="surface-num">10</div>
<div>
<h4>Network Responses</h4>
<p>Intercepts XHR and Fetch responses to detect secrets returned by APIs and backend services.</p>
</div>
</div>
</div>
</section>
<!-- INSTALLATION -->
<section id="install">
<div class="section-label">Installation</div>
<h2 class="section-title">Up and running in under a minute</h2>
<p class="section-desc">Two ways to install. Both take less than 60 seconds. No build tools required.</p>
<div class="install-grid">
<div class="install-card">
<div class="install-header">
Chrome
<span class="tag">Recommended</span>
</div>
<div class="install-body">
<div class="install-step">
<div class="step-num">1</div>
<div class="step-text">Visit the <a href="https://chromewebstore.google.com/detail/keyfinder/oogbljkilkfgdlkbmajlolpanilnnkim">Chrome Web Store</a></div>
</div>
<div class="install-step">
<div class="step-num">2</div>
<div class="step-text">Click <strong>Add to Chrome</strong></div>
</div>
<div class="install-step">
<div class="step-num">3</div>
<div class="step-text">Done - KeyFinder starts scanning automatically</div>
</div>
</div>
</div>
<div class="install-card">
<div class="install-header">
Firefox
<span class="tag">New</span>
</div>
<div class="install-body">
<div class="install-step">
<div class="step-num">1</div>
<div class="step-text">Visit <a href="https://addons.mozilla.org/en-US/firefox/addon/keyfinder-original/">Firefox Add-ons</a></div>
</div>
<div class="install-step">
<div class="step-num">2</div>
<div class="step-text">Click <strong>Add to Firefox</strong></div>
</div>
<div class="install-step">
<div class="step-num">3</div>
<div class="step-text">Done - requires Firefox 128 or later</div>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="cta">
<h2>Start finding leaked secrets today</h2>
<p>Install keyFinder and let it passively scan every page you visit. No configuration needed.</p>
<div class="hero-actions">
<a href="https://chromewebstore.google.com/detail/keyfinder/oogbljkilkfgdlkbmajlolpanilnnkim" class="btn btn-primary">
Install for Chrome
</a>
<a href="https://addons.mozilla.org/en-US/firefox/addon/keyfinder-original/" class="btn btn-primary" style="background: #ff7139; color: #fff;">
Install for Firefox
</a>
</div>
</section>
<!-- FOOTER -->
<footer>
<div class="footer-links">
<a href="https://chromewebstore.google.com/detail/keyfinder/oogbljkilkfgdlkbmajlolpanilnnkim">Chrome Web Store</a>
<a href="https://addons.mozilla.org/en-US/firefox/addon/keyfinder-original/">Firefox Add-ons</a>
<a href="https://github.com/momenbasel/keyFinder">GitHub</a>
<a href="https://github.com/momenbasel/keyFinder/releases">Releases</a>
<a href="https://github.com/momenbasel/keyFinder/blob/master/LICENSE">MIT License</a>
<a href="https://twitter.com/momenbassel">Twitter</a>
</div>
<p>Built by <a href="https://github.com/momenbasel">Moamen Basel</a></p>
</footer>
<!-- Intersection Observer for animations -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.classList.add('animate');
observer.unobserve(e.target);
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.feature-card, .detection-card, .surface-item, .install-card, .surface-chip').forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
el.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
observer.observe(el);
});
const style = document.createElement('style');
style.textContent = `.feature-card.animate, .detection-card.animate, .surface-item.animate, .install-card.animate, .surface-chip.animate { opacity: 1 !important; transform: translateY(0) !important; }`;
document.head.appendChild(style);
});
</script>
</body>
</html>
-187
View File
@@ -1,187 +0,0 @@
const KEYWORDS_KEY = "kf_keywords";
const FINDINGS_KEY = "kf_findings";
const MAX_FINDINGS = 5000;
const MAX_KEYWORDS = 50;
const MAX_KEYWORD_LENGTH = 50;
const DEFAULT_KEYWORDS = [
"key", "api_key", "apikey", "api-key", "secret", "token",
"access_token", "auth", "credential", "password",
"client_id", "client_secret"
];
// Serialize all storage writes to prevent race conditions
let storageQueue = Promise.resolve();
function enqueue(fn) {
storageQueue = storageQueue.then(fn, fn);
return storageQueue;
}
// --- Per-tab alert icon ---
const alertTabs = new Set();
let alertIconCache = null;
async function buildAlertIcons() {
if (alertIconCache) return alertIconCache;
const sizes = [16, 48];
const imageData = {};
for (const size of sizes) {
const resp = await fetch(chrome.runtime.getURL(`icons/icon${size}.png`));
const blob = await resp.blob();
const bitmap = await createImageBitmap(blob);
const canvas = new OffscreenCanvas(size, size);
const ctx = canvas.getContext("2d");
ctx.drawImage(bitmap, 0, 0, size, size);
// Red alert dot in top-right
const r = Math.max(3, Math.round(size * 0.22));
const cx = size - r - 1;
const cy = r + 1;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fillStyle = "#ff4444";
ctx.fill();
ctx.lineWidth = size >= 48 ? 2 : 1;
ctx.strokeStyle = "#0f0f0f";
ctx.stroke();
imageData[size] = ctx.getImageData(0, 0, size, size);
}
alertIconCache = imageData;
return imageData;
}
async function setAlertIcon(tabId) {
if (alertTabs.has(tabId)) return;
alertTabs.add(tabId);
try {
const imageData = await buildAlertIcons();
await chrome.action.setIcon({ tabId, imageData });
} catch {}
}
function resetTabIcon(tabId) {
if (!alertTabs.delete(tabId)) return;
try {
chrome.action.setIcon({
tabId,
path: { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" }
});
} catch {}
}
// Reset icon when a tab navigates to a new page
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo.status === "loading") {
resetTabIcon(tabId);
}
});
// Clean up when a tab is closed
chrome.tabs.onRemoved.addListener((tabId) => {
alertTabs.delete(tabId);
});
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") {
if (sender.tab?.id) setAlertIcon(sender.tab.id);
enqueue(() => 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") {
enqueue(() => addKeyword(request.keyword)).then((result) => sendResponse(result));
return true;
}
if (request.type === "removeKeyword") {
enqueue(() => removeKeyword(request.keyword)).then(() => sendResponse({ ok: true }));
return true;
}
if (request.type === "removeFinding") {
enqueue(() => removeFinding(request.findingId)).then(() => sendResponse({ ok: true }));
return true;
}
if (request.type === "clearFindings") {
enqueue(() => 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;
}
async function addKeyword(keyword) {
const keywords = await getKeywords();
const normalized = keyword.trim().toLowerCase();
if (!normalized) return { ok: false, error: "Keyword cannot be empty." };
if (normalized.length > MAX_KEYWORD_LENGTH) return { ok: false, error: `Keyword must be ${MAX_KEYWORD_LENGTH} characters or fewer.` };
if (keywords.length >= MAX_KEYWORDS) return { ok: false, error: `Maximum of ${MAX_KEYWORDS} keywords allowed.` };
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;
finding.id = crypto.randomUUID();
findings.push(finding);
// Evict oldest findings when cap is exceeded
if (findings.length > MAX_FINDINGS) {
findings.splice(0, findings.length - MAX_FINDINGS);
}
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(findingId) {
const findings = await getFindings();
const updated = findings.filter((f) => f.id !== findingId);
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: "" });
}
-466
View File
@@ -1,466 +0,0 @@
(async function () {
"use strict";
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;
}
function isHighEntropy(str) {
if (str.length < 12) return false;
return shannonEntropy(str) > 3.5;
}
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;
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, "\\$&");
// Require word boundary around keyword to avoid matching "hotkey", "monkey", "turkey" for "key"
const kwRegex = new RegExp(
`(?:^|[^a-zA-Z])(?:${escaped})(?:[^a-zA-Z]|$)\\s*[:=]\\s*['"\`]([^'"\`\\n]{8,200})['"\`]`,
"gi"
);
let m;
while ((m = kwRegex.exec(text)) !== null) {
const val = m[1];
if (isFalsePositive(val)) continue;
// Skip values that look like JS function/method names (camelCase identifiers)
if (/^[a-z][a-zA-Z0-9_]*$/.test(val) && val.length < 60) 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 {}
}
}
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");
}
}
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;
// Skip known framework CSRF tokens — these are ephemeral anti-CSRF nonces, not secrets
const csrfNames = ["authenticity_token", "csrf_token", "csrf", "_csrf", "__requestverificationtoken",
"csrfmiddlewaretoken", "react-codespace-csrf", "_token", "xsrf-token", "anticsrf"];
if (csrfNames.some((c) => name === c || name.startsWith(c))) continue;
// Skip common non-secret hidden fields
const benignNames = ["return_to", "redirect", "redirect_uri", "next", "ref", "referer",
"utm_source", "utm_medium", "utm_campaign", "notice_name", "host", "method",
"pinned_items_id_and_type[]", "repo_topics[]", "timestamp_secret"];
if (benignNames.some((b) => name === b || name.startsWith(b))) continue;
const sensitive = ["api_key", "apikey", "secret_key", "access_token", "private_key", "password"];
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("*");
// Attribute names that contain "key" but are not secrets
const ignoredAttrs = [
"data-hotkey", "data-hotkey-scope", "data-hotkey-within", // Keyboard shortcuts
"data-provider-key", // UI provider identifiers
"data-pjax-key", "data-turbo-key", // Framework routing keys
];
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;
// Skip known non-secret data attributes
if (ignoredAttrs.includes(attr.name)) continue;
// Skip if the value looks like a keyboard shortcut (contains Mod+, Shift+, etc.)
if (/(?:Mod|Shift|Alt|Ctrl|Meta)\+/i.test(attr.value)) 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]");
// URL param names that look sensitive but aren't
const benignParams = ["author", "assignee", "reviewer", "creator", "user", "username",
"sort", "order", "page", "per_page", "tab", "type", "language", "q", "query",
"ref", "branch", "path", "since", "until", "direction", "state", "label",
"source", "plan", "return_to", "redirect", "onload", "render", "style",
"method", "host", "fromHostedPage", "countryBlackList"];
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();
if (benignParams.includes(p)) continue;
const sensitive = ["api_key", "apikey", "token", "secret", "access_token", "password", "session_id", "private_key"];
// Require exact match on the param name, not substring — "author" was matching "auth"
if (sensitive.some((s) => p === s || p.endsWith(`_${s}`) || p.startsWith(`${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" },
];
// Keys that are known non-sensitive framework/platform storage — never flag these
const ignoredKeyPrefixes = [
"ref-selector:", // GitHub branch selector cache
"jump_to:", // GitHub navigation cache
"soft-nav:", // GitHub SPA navigation state
"react-router-scroll", // React Router scroll positions
"COPILOT_SELECTED_MODEL", // GitHub Copilot UI preference
"rc::", // reCAPTCHA state
"debug:", // Debug flags
"ajs_", // Analytics.js state
"_ga", // Google Analytics
"intercom", // Intercom chat widget
"amplitude_", // Amplitude analytics
"mp_", // Mixpanel
"optimizely", // Optimizely experiments
];
// Specific exact keys that look sensitive but aren't
const ignoredExactKeys = [
"COPILOT_AUTH_TOKEN", // GitHub Copilot ephemeral session (browser-local, not extractable)
"COPILOT_AUTH_TOKEN:expiry",
"id", // Generic session IDs in iframes (e.g., Stripe m.stripe.network)
];
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();
// Skip known benign keys
if (ignoredKeyPrefixes.some((p) => key.startsWith(p))) continue;
if (ignoredExactKeys.includes(key)) continue;
// Skip keys whose values are clearly JSON branch/ref data (GitHub caches)
if (value.startsWith('{"refs":') || value.startsWith('{"billing":')) continue;
const sensitive = ["token", "secret", "auth", "credential", "password", "jwt", "bearer", "private_key"];
// Require a stronger match — "key" alone is too broad (matches "hotkey", "monkey", etc.)
// Remove "key" and "session" from sensitive list to reduce noise
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 {}
}
const kfNonce = document.documentElement.getAttribute("data-kf-verify") || "";
document.documentElement.removeAttribute("data-kf-verify");
window.addEventListener("__kf_finding__", (e) => {
const data = e.detail;
if (!data) return;
if (data.__kfNonce !== kfNonce) 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",
});
}
});
scanScriptSrcUrls();
scanInlineScripts();
scanMetaTags();
scanHiddenInputs();
scanDataAttributes();
scanHtmlComments();
scanLinkHrefs();
scanWebStorage();
scanCookies();
await scanExternalScripts();
// Observe DOM mutations for SPA support
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue;
if (node.tagName === "SCRIPT") {
if (node.src) {
// New external script added
for (const kw of keywords) {
if (node.src.toLowerCase().includes(kw)) {
report({
url: node.src, match: node.src, type: "script-src",
patternName: `Script URL contains: ${kw}`,
severity: "medium", confidence: "medium", provider: "URL Scan",
});
}
}
} else if (node.textContent) {
scanText(node.textContent, pageUrl, "inline-script");
}
}
// Scan any new hidden inputs
const hiddenInputs = node.matches && node.matches('input[type="hidden"]')
? [node]
: (node.querySelectorAll ? Array.from(node.querySelectorAll('input[type="hidden"]')) : []);
for (const input of hiddenInputs) {
const name = (input.name || input.id || "").toLowerCase();
const value = input.value;
if (!value || value.length < 8) continue;
// Skip known CSRF tokens
const csrfNames = ["authenticity_token", "csrf_token", "csrf", "_csrf", "__requestverificationtoken",
"csrfmiddlewaretoken", "react-codespace-csrf", "_token", "xsrf-token", "anticsrf"];
if (csrfNames.some((c) => name === c || name.startsWith(c))) continue;
const benignNames = ["return_to", "redirect", "redirect_uri", "next", "ref",
"notice_name", "host", "method", "pinned_items_id_and_type[]", "repo_topics[]", "timestamp_secret"];
if (benignNames.some((b) => name === b || name.startsWith(b))) continue;
const sensitive = ["api_key", "apikey", "secret_key", "access_token", "private_key", "password"];
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",
});
}
}
// Scan data attributes on new elements
const elementsToCheck = node.querySelectorAll ? [node, ...node.querySelectorAll("*")] : [node];
const ignoredAttrsMut = ["data-hotkey", "data-hotkey-scope", "data-hotkey-within", "data-provider-key", "data-pjax-key", "data-turbo-key"];
for (const el of elementsToCheck) {
if (!el.attributes) continue;
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;
if (ignoredAttrsMut.includes(attr.name)) continue;
if (/(?:Mod|Shift|Alt|Ctrl|Meta)\+/i.test(attr.value)) 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",
});
}
}
}
}
});
observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
if (seen.size > 0) {
console.log(`[KeyFinder] ${seen.size} potential secret(s) found on ${pageDomain}`);
}
})();
-16
View File
@@ -1,16 +0,0 @@
(function () {
"use strict";
const nonce = crypto.randomUUID();
// Store nonce where both MAIN world (interceptor) and ISOLATED world (content.js) can read it.
// The interceptor removes data-kf-nonce after reading; data-kf-verify stays for content.js.
const el = document.documentElement;
el.setAttribute("data-kf-nonce", nonce);
el.setAttribute("data-kf-verify", nonce);
const script = document.createElement("script");
script.src = chrome.runtime.getURL("js/interceptor.js");
(document.head || document.documentElement).appendChild(script);
script.onload = () => script.remove();
})();
-125
View File
@@ -1,125 +0,0 @@
(function () {
"use strict";
const EVENT_NAME = "__kf_finding__";
const nonce = document.documentElement.getAttribute("data-kf-nonce") || "";
document.documentElement.removeAttribute("data-kf-nonce");
function emit(data) {
data.__kfNonce = nonce;
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",
];
const scannedGlobals = new Set();
function scanGlobals() {
for (const name of globalNames) {
if (scannedGlobals.has(name)) continue;
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;
scannedGlobals.add(name);
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 {}
}
}
// Run at document_start (likely empty), DOMContentLoaded, and load to catch
// globals set at any phase. Each name reports at most once via scannedGlobals.
scanGlobals();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", scanGlobals, { once: true });
}
if (document.readyState !== "complete") {
window.addEventListener("load", scanGlobals, { once: true });
} else {
scanGlobals();
}
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;
};
})();
-134
View File
@@ -1,134 +0,0 @@
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: "low", 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" },
];
-84
View File
@@ -1,84 +0,0 @@
document.addEventListener("DOMContentLoaded", init);
async function init() {
const versionLabel = document.getElementById("versionLabel");
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
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 = "";
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;
}
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);
}
-258
View File
@@ -1,258 +0,0 @@
let allFindings = [];
document.addEventListener("DOMContentLoaded", init);
async function init() {
const versionLabel = document.getElementById("versionLabel");
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
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;
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 = "";
if (filtered.length === 0) {
empty.hidden = false;
return;
}
empty.hidden = true;
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 {
tdSource.textContent = f.url ? truncateUrl(f.url, 40) : "-";
}
const tdTime = document.createElement("td");
tdTime.textContent = f.timestamp ? formatTime(f.timestamp) : "-";
tdTime.className = "td-time";
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", findingId: f.id });
allFindings = allFindings.filter((x) => x.id !== f.id);
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 csvSafe(value) {
let str = String(value || "");
if (/^[=+\-@\t\r\n]/.test(str)) str = "'" + str;
str = str.replace(/"/g, '""');
return `"${str}"`;
}
function exportCsv() {
const filtered = getFiltered();
const headers = ["Severity", "Provider", "Pattern", "Match", "Type", "Domain", "URL", "Page URL", "Timestamp"];
const rows = filtered.map((f) => [
csvSafe(f.severity),
csvSafe(f.provider),
csvSafe(f.patternName),
csvSafe(f.match),
csvSafe(f.type),
csvSafe(f.domain),
csvSafe(f.url),
csvSafe(f.pageUrl),
csvSafe(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();
}
-55
View File
@@ -1,55 +0,0 @@
{
"name": "KeyFinder",
"description": "Passively discovers API keys, tokens, and secrets leaked in page scripts, DOM, network responses, and browser storage. Available for Chrome and Firefox.",
"version": "2.1.0",
"manifest_version": 3,
"browser_specific_settings": {
"gecko": {
"id": "keyfinder@momenbasel.com",
"strict_min_version": "128.0",
"data_collection_permissions": {
"required": ["none"]
}
}
},
"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/patterns.js", "js/content.js"],
"run_at": "document_idle",
"all_frames": true
},
{
"matches": ["<all_urls>"],
"js": ["js/interceptor-loader.js"],
"run_at": "document_start",
"all_frames": true
}
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
},
"background": {
"scripts": ["js/background.js"]
},
"web_accessible_resources": [
{
"resources": ["js/interceptor.js"],
"matches": ["<all_urls>"]
}
],
"permissions": ["activeTab", "storage"]
}
-46
View File
@@ -1,46 +0,0 @@
{
"name": "KeyFinder",
"description": "Passively discovers API keys, tokens, and secrets leaked in page scripts, DOM, network responses, and browser storage. Available for Chrome and Firefox.",
"version": "2.1.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/patterns.js", "js/content.js"],
"run_at": "document_idle",
"all_frames": true
},
{
"matches": ["<all_urls>"],
"js": ["js/interceptor-loader.js"],
"run_at": "document_start",
"all_frames": true
}
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
},
"background": {
"service_worker": "js/background.js"
},
"web_accessible_resources": [
{
"resources": ["js/interceptor.js"],
"matches": ["<all_urls>"]
}
],
"permissions": ["activeTab", "storage"]
}
-54
View File
@@ -1,54 +0,0 @@
<!DOCTYPE html>
<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" id="versionLabel"></span>
</div>
<p class="header-tagline">Passive API key & secret discovery</p>
</header>
<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>
-68
View File
@@ -1,68 +0,0 @@
<!DOCTYPE html>
<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" id="versionLabel"></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>
<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">&#128269;</div>
<p>No findings yet. Browse some pages and KeyFinder will passively scan for leaked secrets.</p>
</div>
</main>
<script src="js/results.js"></script>
</body>
</html>
-48
View File
@@ -1,48 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DIST="$ROOT/dist"
VERSION=$(grep '"version"' "$ROOT/manifest.json" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
rm -rf "$DIST"
mkdir -p "$DIST"
SHARED_FILES=(
js/background.js
js/content.js
js/interceptor.js
js/interceptor-loader.js
js/patterns.js
js/popup.js
js/results.js
css/popup.css
css/results.css
icons/icon16.png
icons/icon48.png
icons/icon128.png
popup.html
results.html
)
# --- Chrome build ---
CHROME_DIR="$DIST/chrome"
mkdir -p "$CHROME_DIR"/{js,css,icons}
cp "$ROOT/manifest.json" "$CHROME_DIR/manifest.json"
for f in "${SHARED_FILES[@]}"; do
cp "$ROOT/$f" "$CHROME_DIR/$f"
done
(cd "$CHROME_DIR" && zip -r "$DIST/keyfinder-v${VERSION}-chrome.zip" . -x '.*')
echo "Built: dist/keyfinder-v${VERSION}-chrome.zip"
# --- Firefox build ---
FF_DIR="$DIST/firefox"
mkdir -p "$FF_DIR"/{js,css,icons}
cp "$ROOT/manifest.firefox.json" "$FF_DIR/manifest.json"
for f in "${SHARED_FILES[@]}"; do
cp "$ROOT/$f" "$FF_DIR/$f"
done
(cd "$FF_DIR" && zip -r "$DIST/keyfinder-v${VERSION}-firefox.zip" . -x '.*')
echo "Built: dist/keyfinder-v${VERSION}-firefox.zip"
echo "Done. Upload dist/keyfinder-v${VERSION}-firefox.zip to addons.mozilla.org"