Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a3ff3c957 | |||
| 1bbd19f155 | |||
| 524bffdaec | |||
| ef5b0ba8a2 | |||
| d32a4d789c | |||
| e4b77ae871 | |||
| 948337cbb5 | |||
| c70db2b677 | |||
| 4d8e4df8fc | |||
| 882e298d85 | |||
| 5d606109dc | |||
| 50a2f8b482 | |||
| 72f324adae |
@@ -60,7 +60,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Attach to GitHub Release
|
- name: Attach to GitHub Release
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
uses: softprops/action-gh-release@v3
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
dist/keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip
|
dist/keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip
|
||||||
|
|||||||
@@ -2,7 +2,26 @@
|
|||||||
|
|
||||||
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).
|
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]
|
## [2.2.1] - 2026-07-18
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Anti-slop UI refinement: removed scanline/vignette overlays, colored glows, gradient washes, and card grids in favor of flat value-step surfaces, segmented stat strips with hairline dividers, sharp corners, reverse-video active states and hover emphasis, and a titled-border (`findings.log`) table shell — stricter terminal discipline throughout
|
||||||
|
- Store listing screenshots regenerated from the flattened UI
|
||||||
|
|
||||||
|
## [2.2.0] - 2026-07-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Ops-console UI redesign of the popup and findings dashboard: radar-sweep brand mark, ARMED/PASSIVE status LEDs, scanline texture, monospace UI chrome, and a semantic color system (amber = actions, teal = recovered secrets, red/orange/yellow/teal severity scale)
|
||||||
|
- Popup: count-up stat numerals, numbered watchlist entries, command-prompt keyword input with glowing focus ring, and a terminal status line typed with the live keyword/finding counts
|
||||||
|
- Dashboard: clickable severity stat tiles that filter the table, LATEST ticker for the most recent finding, `findings.log` table shell with severity-coded row edges, LED severity badges, click-to-copy match chips, relative timestamps (absolute on hover), skeleton loading rows, staggered first-paint rows, and an animated radar empty state that distinguishes "no findings yet" from "filters hide everything"
|
||||||
|
- Keyboard shortcuts on the dashboard: `/` focuses search, `Esc` clears and blurs it
|
||||||
|
- `prefers-reduced-motion` support across all animations
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Severity sort treated `critical` as lowest priority (`severityOrder[s] || 5` maps `critical: 0` to the fallback), so critical findings rendered last; they now lead the table
|
||||||
|
- Latest-finding ticker no longer stays visible when storage is empty (`display: flex` was overriding the `hidden` attribute)
|
||||||
|
|
||||||
|
## [2.1.1] - 2026-05-14
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- `SECURITY.md` with threat model, disclosure policy, and known limitations of the MAIN <-> ISOLATED nonce bridge
|
- `SECURITY.md` with threat model, disclosure policy, and known limitations of the MAIN <-> ISOLATED nonce bridge
|
||||||
@@ -12,6 +31,8 @@ All notable changes to KeyFinder are documented here. Format follows [Keep a Cha
|
|||||||
### Changed
|
### Changed
|
||||||
- CSV export sanitiser now also prefixes cells starting with LF (`\n`), not just `=`, `+`, `-`, `@`, tab, CR
|
- 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
|
- Popup and results page version label is now read from the manifest at runtime instead of being hardcoded
|
||||||
|
|
||||||
|
### Fixed
|
||||||
- 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
|
- 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
|
## [2.1.0] - 2026-04-14
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<video src="https://cdn.jsdelivr.net/gh/momenbasel/keyFinder@master/store/store-demo.mp4" poster="https://raw.githubusercontent.com/momenbasel/keyFinder/master/store/store-1-dashboard.png" controls muted playsinline></video>
|
||||||
|
</p>
|
||||||
|
|
||||||
## What It Detects
|
## What It Detects
|
||||||
|
|
||||||
KeyFinder ships with **80+ detection patterns** covering secrets from:
|
KeyFinder ships with **80+ detection patterns** covering secrets from:
|
||||||
@@ -60,10 +64,14 @@ Additionally, **Shannon entropy analysis** is applied to detect random high-entr
|
|||||||
- **Zero dependencies** - Pure vanilla JavaScript, no jQuery, no external libraries
|
- **Zero dependencies** - Pure vanilla JavaScript, no jQuery, no external libraries
|
||||||
- **Manifest V3** - Built for modern Chrome and Firefox with service worker architecture
|
- **Manifest V3** - Built for modern Chrome and Firefox with service worker architecture
|
||||||
- **Passive scanning** - Runs automatically on every page load
|
- **Passive scanning** - Runs automatically on every page load
|
||||||
- **Custom keywords** - Add your own search terms to scan for
|
- **SPA-aware** - MutationObserver re-scans dynamically injected DOM
|
||||||
- **Dashboard** - Professional results page with filtering, sorting, and search
|
- **Per-tab alert badge** - Red-dot icon overlay when a tab has findings
|
||||||
- **Export** - Download findings as JSON or CSV
|
- **Custom keywords** - Add your own search terms to scan for (validated, 50 max)
|
||||||
- **Badge counter** - Shows finding count on the extension icon
|
- **Ops-console dashboard** - Flat terminal UI: severity-sorted findings, click-to-filter severity tiles, latest-finding ticker, and live search (`/` focuses, `Esc` clears)
|
||||||
|
- **One-click triage** - Click any match chip to copy the secret, relative timestamps with absolute on hover, per-finding delete
|
||||||
|
- **Export** - Download filtered findings as JSON or CSV (with formula-injection sanitiser)
|
||||||
|
- **Hardened bridge** - Per-page nonce on MAIN <-> ISOLATED CustomEvent channel
|
||||||
|
- **Bounded storage** - 5000-finding cap with FIFO eviction; serialised writes across tabs
|
||||||
- **Low footprint** - Minimal CPU and memory usage
|
- **Low footprint** - Minimal CPU and memory usage
|
||||||
- **All frames** - Scans iframes and embedded content
|
- **All frames** - Scans iframes and embedded content
|
||||||
|
|
||||||
@@ -97,7 +105,7 @@ git clone https://github.com/momenbasel/keyFinder.git
|
|||||||
2. **Browse** the web normally - KeyFinder scans every page in the background
|
2. **Browse** the web normally - KeyFinder scans every page in the background
|
||||||
3. Click the **extension icon** to see stats and manage keywords
|
3. Click the **extension icon** to see stats and manage keywords
|
||||||
4. Click **View Findings** to open the full results dashboard
|
4. Click **View Findings** to open the full results dashboard
|
||||||
5. **Filter** by severity, provider, or type
|
5. **Filter** by severity, provider, or type - or click a severity tile to filter instantly; press `/` to search
|
||||||
6. **Export** findings as JSON or CSV for reporting
|
6. **Export** findings as JSON or CSV for reporting
|
||||||
|
|
||||||
## Adding Custom Keywords
|
## Adding Custom Keywords
|
||||||
@@ -117,8 +125,9 @@ keyFinder/
|
|||||||
js/
|
js/
|
||||||
background.js # Service worker - storage and message handling
|
background.js # Service worker - storage and message handling
|
||||||
patterns.js # 80+ secret detection regex patterns
|
patterns.js # 80+ secret detection regex patterns
|
||||||
content.js # Page scanner - DOM, scripts, network interception
|
content.js # ISOLATED-world page scanner - DOM, scripts, network
|
||||||
interceptor.js # XHR/Fetch hooking and window global scanning
|
interceptor-loader.js # ISOLATED loader - sets nonce, injects MAIN-world interceptor
|
||||||
|
interceptor.js # MAIN-world XHR/Fetch hooks + window global scanning
|
||||||
popup.js # Popup logic
|
popup.js # Popup logic
|
||||||
results.js # Dashboard logic with filtering and export
|
results.js # Dashboard logic with filtering and export
|
||||||
css/
|
css/
|
||||||
@@ -132,10 +141,22 @@ keyFinder/
|
|||||||
build.sh # Build Chrome and Firefox zip packages
|
build.sh # Build Chrome and Firefox zip packages
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
See [SECURITY.md](SECURITY.md) for the threat model, disclosure policy, and known limitations. Release notes are in [CHANGELOG.md](CHANGELOG.md).
|
||||||
|
|
||||||
## Disclaimer
|
## 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.
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
## Professional services
|
||||||
|
|
||||||
|
keyFinder is built and maintained by [GreyCore Labs](https://greycorelabs.com), a US-incorporated offensive security firm. Want the same eye on your own product?
|
||||||
|
|
||||||
|
- [Penetration testing](https://greycorelabs.com/#plans) - web, API, mobile, cloud. Fixed quote within 24 hours, redacted sample report on request.
|
||||||
|
- [Free external attack-surface scan](https://greycorelabs.com/#free-scan) - one-page report in 48 hours, no strings attached.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
@@ -1,240 +1,435 @@
|
|||||||
* {
|
/* ============================================================
|
||||||
margin: 0;
|
KeyFinder — ops console popup
|
||||||
padding: 0;
|
flat terminal discipline: value-step surfaces, 1px hairlines,
|
||||||
box-sizing: border-box;
|
reverse-video emphasis, zero glow, zero surface effects
|
||||||
|
amber = action · teal = recovered secrets
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #05070c;
|
||||||
|
--bg-raise: #090d14;
|
||||||
|
--bg-overlay: #0d131d;
|
||||||
|
--line: #1a2233;
|
||||||
|
--line-strong: #2a3650;
|
||||||
|
--text: #d6dde8;
|
||||||
|
--text-dim: #8b96a8;
|
||||||
|
--text-faint: #525d70;
|
||||||
|
--ink: #05070c;
|
||||||
|
--amber: #f5a524;
|
||||||
|
--amber-hot: #ffc45c;
|
||||||
|
--teal: #45d0c4;
|
||||||
|
--red: #ff5c5c;
|
||||||
|
--green: #3ddc97;
|
||||||
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco,
|
||||||
|
Consolas, "Liberation Mono", monospace;
|
||||||
|
--ease: cubic-bezier(.24, .58, .47, .99);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
html { color-scheme: dark; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
width: 360px;
|
width: 372px;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
font-family: var(--mono);
|
||||||
background: #0f0f0f;
|
background: var(--bg);
|
||||||
color: #e0e0e0;
|
color: var(--text);
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
font-feature-settings: "tnum";
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::selection { background: var(--amber); color: var(--ink); }
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 1px solid var(--amber);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------ header */
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
padding: 16px 16px 12px;
|
padding: 14px 14px 12px;
|
||||||
border-bottom: 1px solid #1e1e1e;
|
border-bottom: 1px solid var(--line);
|
||||||
background: linear-gradient(135deg, #0f0f0f 0%, #1a1a2e 100%);
|
background: var(--bg-raise);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-brand {
|
.header-brand { display: flex; align-items: center; gap: 11px; }
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-icon {
|
/* radar: rotating sweep ring + blip around the key */
|
||||||
width: 28px;
|
.radar {
|
||||||
height: 28px;
|
position: relative;
|
||||||
|
flex: none;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
background: var(--bg);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
}
|
}
|
||||||
|
.radar::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -1px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: conic-gradient(from 0deg,
|
||||||
|
var(--amber) 0deg, rgba(245,165,36,.15) 60deg, transparent 95deg);
|
||||||
|
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 1.5px));
|
||||||
|
mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 1.5px));
|
||||||
|
animation: radar-sweep 4.2s linear infinite;
|
||||||
|
}
|
||||||
|
.radar::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 7px;
|
||||||
|
left: 9px;
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--teal);
|
||||||
|
animation: blip 2.1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.header-icon { width: 20px; height: 20px; display: block; }
|
||||||
|
|
||||||
|
@keyframes radar-sweep { to { transform: rotate(360deg); } }
|
||||||
|
@keyframes blip { 0%, 100% { opacity: .15; } 50% { opacity: 1; } }
|
||||||
|
|
||||||
|
.brand-text { flex: 1; min-width: 0; }
|
||||||
|
|
||||||
.header-brand h1 {
|
.header-brand h1 {
|
||||||
font-size: 18px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #ffffff;
|
color: #fff;
|
||||||
letter-spacing: -0.3px;
|
letter-spacing: .22em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version {
|
.version {
|
||||||
font-size: 10px;
|
font-size: 9px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #666;
|
letter-spacing: .08em;
|
||||||
background: #1a1a1a;
|
color: var(--text-faint);
|
||||||
padding: 2px 6px;
|
border: 1px solid var(--line);
|
||||||
border-radius: 4px;
|
padding: 2px 5px;
|
||||||
margin-left: 4px;
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
.version::before { content: "["; color: var(--line-strong); }
|
||||||
|
.version::after { content: "]"; color: var(--line-strong); }
|
||||||
|
|
||||||
.header-tagline {
|
.header-tagline {
|
||||||
margin-top: 4px;
|
margin-top: 3px;
|
||||||
font-size: 11px;
|
font-size: 9.5px;
|
||||||
color: #666;
|
letter-spacing: .1em;
|
||||||
|
color: var(--text-faint);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.header-tagline::before { content: "// "; color: var(--amber); }
|
||||||
|
|
||||||
|
/* armed badge — flat status LED */
|
||||||
|
.live-badge {
|
||||||
|
flex: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 8.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .2em;
|
||||||
|
color: var(--green);
|
||||||
|
border: 1px solid rgba(61,220,151,.4);
|
||||||
|
padding: 4px 7px 4px 6px;
|
||||||
|
}
|
||||||
|
.live-dot {
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--green);
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
|
||||||
|
|
||||||
|
/* ------------------------------------------------ stats: one segmented strip, no cards */
|
||||||
|
|
||||||
.stats {
|
.stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
margin: 10px 14px;
|
||||||
padding: 12px 16px;
|
border: 1px solid var(--line);
|
||||||
border-bottom: 1px solid #1e1e1e;
|
background: var(--bg-raise);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: 4px;
|
||||||
padding: 10px;
|
padding: 9px 11px 8px;
|
||||||
background: #141414;
|
}
|
||||||
border-radius: 8px;
|
.stat-card + .stat-card { border-left: 1px solid var(--line); }
|
||||||
border: 1px solid #1e1e1e;
|
|
||||||
|
.stat-top { display: flex; align-items: center; gap: 6px; }
|
||||||
|
|
||||||
|
.stat-led {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent, var(--amber));
|
||||||
|
}
|
||||||
|
.stat-findings { --accent: var(--amber); }
|
||||||
|
.stat-keywords { --accent: var(--teal); }
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 8.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-faint);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .18em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-number {
|
.stat-number {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
line-height: 1.1;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
/* ------------------------------------------------ watchlist */
|
||||||
font-size: 10px;
|
|
||||||
color: #666;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
.section { padding: 2px 14px 10px; }
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 11px;
|
font-size: 9px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #666;
|
color: var(--text-dim);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.8px;
|
letter-spacing: .22em;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 9px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
.section-title::after {
|
||||||
|
content: "";
|
||||||
|
flex: 1;
|
||||||
|
border-top: 1px dashed var(--line);
|
||||||
|
}
|
||||||
|
.section-sub {
|
||||||
|
font-size: 8.5px;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: .1em;
|
||||||
|
color: var(--text-faint);
|
||||||
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* command-prompt input bar */
|
||||||
.keyword-form {
|
.keyword-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
align-items: stretch;
|
||||||
|
background: var(--bg-raise);
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
|
||||||
|
}
|
||||||
|
.keyword-form:focus-within {
|
||||||
|
border-color: var(--amber);
|
||||||
|
box-shadow: 0 0 0 1px var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0 4px 0 10px;
|
||||||
|
color: var(--amber);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.keyword-form input {
|
.keyword-form input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 7px 10px;
|
min-width: 0;
|
||||||
background: #141414;
|
padding: 8px 8px 8px 2px;
|
||||||
border: 1px solid #2a2a2a;
|
background: transparent;
|
||||||
border-radius: 6px;
|
border: none;
|
||||||
color: #e0e0e0;
|
color: var(--text);
|
||||||
font-size: 12px;
|
font-family: var(--mono);
|
||||||
|
font-size: 11.5px;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.15s;
|
box-shadow: none;
|
||||||
}
|
|
||||||
|
|
||||||
.keyword-form input:focus {
|
|
||||||
border-color: #4a9eff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keyword-form input::placeholder {
|
|
||||||
color: #444;
|
|
||||||
}
|
}
|
||||||
|
.keyword-form input::placeholder { color: var(--text-faint); }
|
||||||
|
|
||||||
.keyword-form button {
|
.keyword-form button {
|
||||||
padding: 7px 14px;
|
margin: 3px;
|
||||||
background: #4a9eff;
|
padding: 0 13px;
|
||||||
color: #fff;
|
background: var(--amber);
|
||||||
|
color: var(--ink);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
font-family: var(--mono);
|
||||||
font-size: 12px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .12em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s;
|
transition: filter .15s var(--ease), transform .05s var(--ease);
|
||||||
}
|
|
||||||
|
|
||||||
.keyword-form button:hover {
|
|
||||||
background: #3a8eef;
|
|
||||||
}
|
}
|
||||||
|
.keyword-form button:hover { filter: brightness(1.12); }
|
||||||
|
.keyword-form button:active { transform: translateY(1px); }
|
||||||
|
|
||||||
.error-msg {
|
.error-msg {
|
||||||
padding: 6px 10px;
|
padding: 6px 9px;
|
||||||
background: #2a1515;
|
background: rgba(255,92,92,.08);
|
||||||
border: 1px solid #4a2020;
|
border: 1px solid rgba(255,92,92,.35);
|
||||||
border-radius: 6px;
|
border-left-width: 2px;
|
||||||
color: #ff6b6b;
|
color: var(--red);
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
|
letter-spacing: .04em;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
animation: shake .3s var(--ease);
|
||||||
|
}
|
||||||
|
.error-msg::before { content: "ERR // "; font-weight: 700; }
|
||||||
|
@keyframes shake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
25% { transform: translateX(-3px); }
|
||||||
|
75% { transform: translateX(3px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* keyword list — numbered watchlist entries */
|
||||||
.keyword-list {
|
.keyword-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
max-height: 240px;
|
counter-reset: kw;
|
||||||
|
max-height: 218px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
margin: 0 -4px;
|
||||||
|
padding: 0 4px;
|
||||||
.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 {
|
.keyword-item {
|
||||||
|
counter-increment: kw;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 6px 10px;
|
gap: 9px;
|
||||||
border-radius: 6px;
|
padding: 5px 7px;
|
||||||
transition: background 0.1s;
|
transition: background .12s var(--ease);
|
||||||
}
|
}
|
||||||
|
.keyword-item::before {
|
||||||
.keyword-item:hover {
|
content: counter(kw, decimal-leading-zero);
|
||||||
background: #1a1a1a;
|
font-size: 8.5px;
|
||||||
|
color: var(--text-faint);
|
||||||
|
letter-spacing: .05em;
|
||||||
|
flex: none;
|
||||||
}
|
}
|
||||||
|
.keyword-item:hover { background: var(--bg-raise); }
|
||||||
|
.keyword-item:hover .keyword-label { color: var(--teal); }
|
||||||
|
|
||||||
.keyword-label {
|
.keyword-label {
|
||||||
font-family: "SF Mono", "Fira Code", "Consolas", monospace;
|
flex: 1;
|
||||||
font-size: 12px;
|
min-width: 0;
|
||||||
color: #ccc;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text);
|
||||||
|
transition: color .12s var(--ease);
|
||||||
}
|
}
|
||||||
|
|
||||||
.keyword-remove {
|
.keyword-remove {
|
||||||
width: 22px;
|
flex: none;
|
||||||
height: 22px;
|
width: 20px;
|
||||||
display: flex;
|
height: 20px;
|
||||||
align-items: center;
|
display: grid;
|
||||||
justify-content: center;
|
place-items: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: 1px solid transparent;
|
||||||
color: #555;
|
color: var(--text-faint);
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 4px;
|
opacity: .45;
|
||||||
transition: all 0.1s;
|
transition: all .12s var(--ease);
|
||||||
}
|
}
|
||||||
|
.keyword-item:hover .keyword-remove { opacity: 1; }
|
||||||
.keyword-remove:hover {
|
.keyword-remove:hover {
|
||||||
background: #2a1515;
|
background: var(--red);
|
||||||
color: #ff6b6b;
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 16px;
|
padding: 14px 10px;
|
||||||
color: #444;
|
color: var(--text-faint);
|
||||||
font-size: 12px;
|
font-size: 10.5px;
|
||||||
|
letter-spacing: .06em;
|
||||||
|
border: 1px dashed var(--line-strong);
|
||||||
}
|
}
|
||||||
|
.empty-state::before { content: "[ empty watchlist ]"; display: block; margin-bottom: 2px; opacity: .6; }
|
||||||
|
|
||||||
|
/* ------------------------------------------------ footer */
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
padding: 12px 16px;
|
padding: 10px 14px 14px;
|
||||||
border-top: 1px solid #1e1e1e;
|
border-top: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* typed terminal status line */
|
||||||
|
.term-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 16px;
|
||||||
|
margin-bottom: 9px;
|
||||||
|
font-size: 9.5px;
|
||||||
|
letter-spacing: .06em;
|
||||||
|
color: var(--text-faint);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.term-text { overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.term-cursor {
|
||||||
|
flex: none;
|
||||||
|
width: 6px;
|
||||||
|
height: 11px;
|
||||||
|
margin-left: 3px;
|
||||||
|
background: var(--amber);
|
||||||
|
animation: cursor-blink 1.06s steps(1) infinite;
|
||||||
|
}
|
||||||
|
@keyframes cursor-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
|
||||||
|
|
||||||
.results-btn {
|
.results-btn {
|
||||||
display: block;
|
display: flex;
|
||||||
text-align: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
background: var(--amber);
|
||||||
color: #4a9eff;
|
color: var(--ink);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-radius: 8px;
|
font-size: 11px;
|
||||||
font-size: 13px;
|
font-weight: 700;
|
||||||
font-weight: 600;
|
text-transform: uppercase;
|
||||||
border: 1px solid #1e2d4a;
|
letter-spacing: .16em;
|
||||||
transition: all 0.15s;
|
transition: filter .15s var(--ease), transform .05s var(--ease);
|
||||||
}
|
}
|
||||||
|
.results-btn:hover { filter: brightness(1.12); }
|
||||||
|
.results-btn:active { transform: translateY(1px); }
|
||||||
|
.btn-arrow { transition: transform .18s var(--ease); }
|
||||||
|
.results-btn:hover .btn-arrow { transform: translateX(3px); }
|
||||||
|
|
||||||
.results-btn:hover {
|
/* ------------------------------------------------ scrollbars */
|
||||||
background: linear-gradient(135deg, #1e2040 0%, #1a2848 100%);
|
|
||||||
border-color: #2a4070;
|
::-webkit-scrollbar { width: 5px; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #1c2536; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: #2a3650; }
|
||||||
|
|
||||||
|
/* ------------------------------------------------ reduced motion */
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after {
|
||||||
|
animation-duration: .01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: .01ms !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,341 +1,671 @@
|
|||||||
* {
|
/* ============================================================
|
||||||
margin: 0;
|
KeyFinder — findings console
|
||||||
padding: 0;
|
flat terminal discipline: value-step surfaces, 1px hairlines,
|
||||||
box-sizing: border-box;
|
reverse-video emphasis, zero glow, zero surface effects
|
||||||
|
severity scale: red > orange > yellow > teal
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #05070c;
|
||||||
|
--bg-raise: #090d14;
|
||||||
|
--bg-overlay: #0d131d;
|
||||||
|
--line: #1a2233;
|
||||||
|
--line-strong: #2a3650;
|
||||||
|
--text: #d6dde8;
|
||||||
|
--text-dim: #8b96a8;
|
||||||
|
--text-faint: #525d70;
|
||||||
|
--ink: #05070c;
|
||||||
|
--amber: #f5a524;
|
||||||
|
--amber-hot: #ffc45c;
|
||||||
|
--teal: #45d0c4;
|
||||||
|
--red: #ff5c5c;
|
||||||
|
--orange: #ff9431;
|
||||||
|
--yellow: #ffd166;
|
||||||
|
--green: #3ddc97;
|
||||||
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco,
|
||||||
|
Consolas, "Liberation Mono", monospace;
|
||||||
|
--ease: cubic-bezier(.24, .58, .47, .99);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
html { color-scheme: dark; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
font-family: var(--mono);
|
||||||
background: #0a0a0a;
|
background: var(--bg);
|
||||||
color: #e0e0e0;
|
color: var(--text);
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
font-feature-settings: "tnum";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::selection { background: var(--amber); color: var(--ink); }
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 1px solid var(--amber);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------ header */
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
padding: 16px 24px;
|
padding: 14px 24px 12px;
|
||||||
background: #0f0f0f;
|
border-bottom: 1px solid var(--line);
|
||||||
border-bottom: 1px solid #1e1e1e;
|
background: var(--bg-raise);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left {
|
.header-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-icon {
|
.header-left { display: flex; align-items: center; gap: 12px; }
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left h1 {
|
/* radar sweep around the key */
|
||||||
font-size: 20px;
|
.radar {
|
||||||
|
position: relative;
|
||||||
|
flex: none;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
background: var(--bg);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.radar::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -1px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: conic-gradient(from 0deg,
|
||||||
|
var(--amber) 0deg, rgba(245,165,36,.15) 60deg, transparent 95deg);
|
||||||
|
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 1.5px));
|
||||||
|
mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 1.5px));
|
||||||
|
animation: radar-sweep 4.2s linear infinite;
|
||||||
|
}
|
||||||
|
.radar::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 10px;
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--teal);
|
||||||
|
animation: blip 2.1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.header-icon { width: 22px; height: 22px; display: block; }
|
||||||
|
|
||||||
|
@keyframes radar-sweep { to { transform: rotate(360deg); } }
|
||||||
|
@keyframes blip { 0%, 100% { opacity: .15; } 50% { opacity: 1; } }
|
||||||
|
|
||||||
|
.brand-text h1 {
|
||||||
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
letter-spacing: .22em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version {
|
.version {
|
||||||
font-size: 11px;
|
font-size: 9px;
|
||||||
color: #555;
|
font-weight: 500;
|
||||||
font-weight: 400;
|
letter-spacing: .08em;
|
||||||
|
color: var(--text-faint);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
padding: 2px 5px;
|
||||||
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
.version::before { content: "["; color: var(--line-strong); }
|
||||||
|
.version::after { content: "]"; color: var(--line-strong); }
|
||||||
|
|
||||||
|
.header-sub {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: .18em;
|
||||||
|
color: var(--text-faint);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.header-sub::before { content: "// "; color: var(--amber); }
|
||||||
|
|
||||||
|
.live-badge {
|
||||||
|
flex: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 8.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .2em;
|
||||||
|
color: var(--green);
|
||||||
|
border: 1px solid rgba(61,220,151,.4);
|
||||||
|
padding: 4px 8px 4px 7px;
|
||||||
|
}
|
||||||
|
.live-dot {
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--green);
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
|
||||||
|
|
||||||
|
/* ------------------------------------------------ header actions */
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* terminal-style search bar */
|
||||||
|
.search-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
padding: 0 8px;
|
||||||
|
transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
|
||||||
|
}
|
||||||
|
.search-wrap:focus-within {
|
||||||
|
border-color: var(--amber);
|
||||||
|
box-shadow: 0 0 0 1px var(--amber);
|
||||||
|
}
|
||||||
|
.search-glyph { color: var(--amber); font-size: 13px; user-select: none; }
|
||||||
|
.search-wrap input {
|
||||||
|
width: 210px;
|
||||||
|
padding: 7px 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11.5px;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.search-wrap input::placeholder { color: var(--text-faint); }
|
||||||
|
|
||||||
|
.kbd {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-faint);
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group { display: flex; gap: 6px; }
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .1em;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .15s var(--ease);
|
||||||
|
}
|
||||||
|
.btn:active { transform: translateY(1px); }
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--line-strong);
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--amber);
|
||||||
|
border-color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: transparent;
|
||||||
|
border-color: rgba(255,92,92,.4);
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
.btn-danger:hover { background: var(--red); border-color: var(--red); color: var(--ink); }
|
||||||
|
|
||||||
|
/* ------------------------------------------------ filter row */
|
||||||
|
|
||||||
|
.header-row-filters { gap: 10px; }
|
||||||
|
|
||||||
.filter-group {
|
.filter-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-group select,
|
.filter {
|
||||||
.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;
|
display: flex;
|
||||||
gap: 6px;
|
align-items: center;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color .15s var(--ease);
|
||||||
|
}
|
||||||
|
.filter:focus-within { border-color: var(--amber); }
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-size: 8.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--amber);
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
|
padding: 7px 7px 7px 9px;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.filter select {
|
||||||
padding: 6px 14px;
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
padding: 6px 24px 6px 9px;
|
||||||
|
background-color: transparent;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M1 1l3 3 3-3' stroke='%238b96a8' fill='none'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 8px center;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
color: var(--text);
|
||||||
font-size: 12px;
|
font-family: var(--mono);
|
||||||
font-weight: 600;
|
font-size: 10.5px;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s;
|
max-width: 180px;
|
||||||
}
|
}
|
||||||
|
.filter select option { background: var(--bg-overlay); color: var(--text); }
|
||||||
|
|
||||||
.btn-secondary {
|
.meta-line {
|
||||||
background: #1a1a1a;
|
margin-left: auto;
|
||||||
color: #ccc;
|
font-size: 9.5px;
|
||||||
border: 1px solid #2a2a2a;
|
letter-spacing: .1em;
|
||||||
|
color: var(--text-faint);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.meta-line strong { color: var(--text-dim); font-weight: 600; }
|
||||||
|
|
||||||
.btn-secondary:hover {
|
/* ------------------------------------------------ stats: one segmented strip, no cards */
|
||||||
background: #222;
|
|
||||||
border-color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: #2a1515;
|
|
||||||
color: #ff6b6b;
|
|
||||||
border: 1px solid #4a2020;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background: #3a1a1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-bar {
|
.stats-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1px;
|
flex-wrap: wrap;
|
||||||
padding: 12px 24px;
|
margin: 12px 24px;
|
||||||
background: #0f0f0f;
|
border: 1px solid var(--line);
|
||||||
border-bottom: 1px solid #1e1e1e;
|
background: var(--bg-raise);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item {
|
.stat-item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 96px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: 4px;
|
||||||
padding: 10px;
|
padding: 9px 12px 8px;
|
||||||
background: #111;
|
position: relative;
|
||||||
border-radius: 6px;
|
transition: background .15s var(--ease);
|
||||||
margin: 0 4px;
|
|
||||||
}
|
}
|
||||||
|
.stat-item + .stat-item { border-left: 1px solid var(--line); }
|
||||||
|
.stat-item[data-filter] { cursor: pointer; }
|
||||||
|
.stat-item[data-filter]:hover { background: var(--bg-overlay); }
|
||||||
|
|
||||||
|
/* reverse-video active filter */
|
||||||
|
.stat-item.active { background: var(--accent, var(--amber)); }
|
||||||
|
.stat-item.active .stat-num,
|
||||||
|
.stat-item.active .stat-lbl { color: var(--ink); }
|
||||||
|
.stat-item.active .stat-led { background: var(--ink); }
|
||||||
|
|
||||||
|
.stat-total { --accent: #d6dde8; }
|
||||||
|
.stat-critical { --accent: var(--red); }
|
||||||
|
.stat-high { --accent: var(--orange); }
|
||||||
|
.stat-medium { --accent: var(--yellow); }
|
||||||
|
.stat-low { --accent: var(--teal); }
|
||||||
|
.stat-domains { --accent: var(--text-faint); }
|
||||||
|
|
||||||
|
.stat-top { display: flex; align-items: center; gap: 6px; }
|
||||||
|
|
||||||
|
.stat-led {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent, var(--text-dim));
|
||||||
|
}
|
||||||
|
.stat-critical .stat-led { animation: pulse 1.5s ease-in-out infinite; }
|
||||||
|
|
||||||
.stat-num {
|
.stat-num {
|
||||||
font-size: 20px;
|
font-size: 21px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #fff;
|
line-height: 1.1;
|
||||||
|
color: var(--accent, #fff);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
.stat-total .stat-num { color: #fff; }
|
||||||
|
|
||||||
.stat-lbl {
|
.stat-lbl {
|
||||||
font-size: 10px;
|
font-size: 8.5px;
|
||||||
color: #555;
|
font-weight: 600;
|
||||||
|
color: var(--text-faint);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: .18em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-critical .stat-num { color: #ff4444; }
|
/* ------------------------------------------------ ticker */
|
||||||
.stat-high .stat-num { color: #ff8c42; }
|
|
||||||
.stat-medium .stat-num { color: #ffd166; }
|
|
||||||
.stat-low .stat-num { color: #4ecdc4; }
|
|
||||||
|
|
||||||
.main {
|
.ticker {
|
||||||
padding: 16px 24px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
padding: 7px 24px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: .05em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.ticker[hidden] { display: none; }
|
||||||
|
|
||||||
|
.ticker-dot {
|
||||||
|
flex: none;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--amber);
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.ticker-label {
|
||||||
|
flex: none;
|
||||||
|
font-size: 8.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .2em;
|
||||||
|
color: var(--amber);
|
||||||
|
}
|
||||||
|
.ticker-text {
|
||||||
|
color: var(--text-dim);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.ticker-text code { color: var(--teal); }
|
||||||
|
|
||||||
|
/* ------------------------------------------------ table shell: titled border */
|
||||||
|
|
||||||
|
.main { padding: 14px 24px 20px; }
|
||||||
|
|
||||||
|
.titled {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--bg-raise);
|
||||||
|
}
|
||||||
|
.titled-label {
|
||||||
|
position: absolute;
|
||||||
|
top: -7px;
|
||||||
|
left: 12px;
|
||||||
|
z-index: 3;
|
||||||
|
background: var(--bg);
|
||||||
|
padding: 0 7px;
|
||||||
|
font-size: 9.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .18em;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.titled-label::before { content: "● "; color: var(--amber); font-size: 7px; vertical-align: 1.5px; }
|
||||||
|
.titled-meta {
|
||||||
|
position: absolute;
|
||||||
|
top: -7px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 3;
|
||||||
|
background: var(--bg);
|
||||||
|
padding: 0 7px;
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: .1em;
|
||||||
|
color: var(--text-faint);
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.findings-table {
|
.findings-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 980px;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.findings-table thead th {
|
.findings-table thead th {
|
||||||
padding: 8px 10px;
|
padding: 12px 12px 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 10px;
|
font-size: 8.5px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #555;
|
color: var(--text-faint);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: .16em;
|
||||||
border-bottom: 1px solid #1e1e1e;
|
border-bottom: 1px solid var(--line);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
background: #0a0a0a;
|
background: var(--bg-raise);
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.findings-table tbody tr {
|
.findings-table tbody tr {
|
||||||
border-bottom: 1px solid #141414;
|
border-bottom: 1px solid rgba(26,34,51,.55);
|
||||||
transition: background 0.1s;
|
transition: background .1s var(--ease);
|
||||||
}
|
}
|
||||||
|
.findings-table tbody tr:last-child { border-bottom: none; }
|
||||||
|
.findings-table tbody tr:hover { background: var(--bg-overlay); }
|
||||||
|
|
||||||
.findings-table tbody tr:hover {
|
/* severity accent on the row's leading edge — functional color coding */
|
||||||
background: #111;
|
.findings-table tbody tr td:first-child { box-shadow: inset 2px 0 0 transparent; }
|
||||||
|
.findings-table tbody tr.sev-critical td:first-child { box-shadow: inset 2px 0 0 var(--red); }
|
||||||
|
.findings-table tbody tr.sev-high td:first-child { box-shadow: inset 2px 0 0 var(--orange); }
|
||||||
|
.findings-table tbody tr.sev-medium td:first-child { box-shadow: inset 2px 0 0 var(--yellow); }
|
||||||
|
.findings-table tbody tr.sev-low td:first-child { box-shadow: inset 2px 0 0 var(--teal); }
|
||||||
|
.findings-table tbody tr.sev-info td:first-child { box-shadow: inset 2px 0 0 var(--text-faint); }
|
||||||
|
|
||||||
|
/* first-paint stagger */
|
||||||
|
.findings-table tbody.fresh tr {
|
||||||
|
opacity: 0;
|
||||||
|
animation: row-in .35s var(--ease) forwards;
|
||||||
|
animation-delay: calc(var(--i, 0) * 16ms);
|
||||||
|
}
|
||||||
|
@keyframes row-in {
|
||||||
|
from { opacity: 0; transform: translateY(3px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.findings-table td {
|
.findings-table td {
|
||||||
padding: 8px 10px;
|
padding: 7px 12px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
max-width: 280px;
|
max-width: 220px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.td-num { color: var(--text-faint); font-size: 9.5px; }
|
||||||
|
|
||||||
|
/* severity badge — flat LED + label */
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
padding: 2px 8px;
|
align-items: center;
|
||||||
border-radius: 4px;
|
gap: 6px;
|
||||||
font-size: 10px;
|
padding: 3px 8px 3px 7px;
|
||||||
|
font-size: 8.5px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: .14em;
|
||||||
|
color: var(--sev, var(--text-dim));
|
||||||
|
border: 1px solid color-mix(in srgb, var(--sev, #8b96a8) 45%, transparent);
|
||||||
}
|
}
|
||||||
|
.badge::before {
|
||||||
.badge-critical {
|
content: "";
|
||||||
background: #3a0a0a;
|
width: 4px;
|
||||||
color: #ff4444;
|
height: 4px;
|
||||||
border: 1px solid #5a1515;
|
border-radius: 50%;
|
||||||
}
|
background: var(--sev, var(--text-dim));
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
.badge-critical { --sev: var(--red); }
|
||||||
|
.badge-critical::before { animation: pulse 1.5s ease-in-out infinite; }
|
||||||
|
.badge-high { --sev: var(--orange); }
|
||||||
|
.badge-medium { --sev: var(--yellow); }
|
||||||
|
.badge-low { --sev: var(--teal); }
|
||||||
|
.badge-info { --sev: var(--text-dim); }
|
||||||
|
|
||||||
.type-badge {
|
.type-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 2px 6px;
|
padding: 2px 7px;
|
||||||
background: #1a1a1a;
|
background: var(--bg);
|
||||||
border: 1px solid #2a2a2a;
|
border: 1px solid var(--line);
|
||||||
border-radius: 4px;
|
font-size: 9px;
|
||||||
font-size: 10px;
|
letter-spacing: .06em;
|
||||||
color: #888;
|
color: var(--text-dim);
|
||||||
font-family: "SF Mono", "Fira Code", monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* the recovered secret — teal mono chip, reverse video on hover */
|
||||||
.match-value {
|
.match-value {
|
||||||
font-family: "SF Mono", "Fira Code", "Consolas", monospace;
|
font-family: var(--mono);
|
||||||
font-size: 11px;
|
font-size: 10.5px;
|
||||||
background: #141414;
|
background: var(--bg);
|
||||||
padding: 2px 6px;
|
padding: 3px 8px;
|
||||||
border-radius: 4px;
|
border: 1px solid rgba(69,208,196,.35);
|
||||||
border: 1px solid #1e1e1e;
|
color: var(--teal);
|
||||||
color: #4ecdc4;
|
max-width: 220px;
|
||||||
max-width: 260px;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: copy;
|
||||||
|
transition: all .12s var(--ease);
|
||||||
|
}
|
||||||
|
.match-value:hover {
|
||||||
|
background: var(--teal);
|
||||||
|
border-color: var(--teal);
|
||||||
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.td-provider {
|
.td-provider { font-weight: 700; color: var(--text); font-size: 10.5px; letter-spacing: .04em; max-width: 130px; }
|
||||||
font-weight: 600;
|
.td-pattern { color: var(--text-dim); font-size: 10px; max-width: 170px; }
|
||||||
color: #aaa;
|
.td-domain { color: var(--amber); font-size: 10.5px; max-width: 180px; }
|
||||||
}
|
.td-source { max-width: 200px; }
|
||||||
|
.td-time { color: var(--text-faint); font-size: 10px; white-space: nowrap; }
|
||||||
|
|
||||||
.td-pattern {
|
a { color: var(--teal); text-decoration: none; border-bottom: 1px dashed rgba(69,208,196,.4); }
|
||||||
color: #888;
|
a:hover { color: var(--ink); background: var(--teal); border-bottom-color: var(--teal); }
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.td-domain {
|
/* row actions — revealed on row hover */
|
||||||
color: #4a9eff;
|
.td-actions { white-space: nowrap; text-align: right; }
|
||||||
}
|
|
||||||
|
|
||||||
.td-time {
|
|
||||||
color: #555;
|
|
||||||
font-size: 11px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #4a9eff;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
|
font-family: var(--mono);
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
background: #1a1a1a;
|
background: transparent;
|
||||||
border: 1px solid #2a2a2a;
|
border: 1px solid var(--line-strong);
|
||||||
border-radius: 4px;
|
color: var(--text-faint);
|
||||||
color: #888;
|
font-size: 9px;
|
||||||
font-size: 10px;
|
font-weight: 700;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
text-transform: uppercase;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
transition: all 0.1s;
|
opacity: .35;
|
||||||
|
transition: all .12s var(--ease);
|
||||||
}
|
}
|
||||||
|
.findings-table tbody tr:hover .btn-icon,
|
||||||
|
.btn-icon:focus-visible { opacity: 1; }
|
||||||
|
.btn-icon:hover { color: var(--ink); background: var(--amber); border-color: var(--amber); }
|
||||||
|
.btn-icon.copied { color: var(--ink); background: var(--green); border-color: var(--green); }
|
||||||
|
.btn-icon-danger:hover { color: var(--ink); background: var(--red); border-color: var(--red); }
|
||||||
|
|
||||||
.btn-icon:hover {
|
/* skeleton loading rows */
|
||||||
background: #222;
|
.skl-row td { padding: 9px 12px; }
|
||||||
color: #ccc;
|
.skl {
|
||||||
|
height: 9px;
|
||||||
|
background: linear-gradient(90deg, #0c1119 25%, #151c2b 50%, #0c1119 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.15s linear infinite;
|
||||||
}
|
}
|
||||||
|
.skl-w1 { width: 34%; } .skl-w2 { width: 62%; } .skl-w3 { width: 48%; } .skl-w4 { width: 78%; }
|
||||||
|
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
||||||
|
|
||||||
.btn-icon-danger:hover {
|
/* ------------------------------------------------ empty state */
|
||||||
background: #2a1515;
|
|
||||||
color: #ff6b6b;
|
|
||||||
border-color: #4a2020;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 60px 20px;
|
padding: 56px 20px 50px;
|
||||||
color: #444;
|
color: var(--text-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-icon {
|
.empty-radar { width: 72px; height: 72px; margin-bottom: 18px; opacity: .85; }
|
||||||
font-size: 48px;
|
.er-ring { fill: none; stroke: var(--line-strong); stroke-width: 1; }
|
||||||
margin-bottom: 16px;
|
.er-cross { stroke: var(--line); stroke-width: 1; }
|
||||||
opacity: 0.3;
|
.er-core { fill: var(--teal); }
|
||||||
|
.er-sweep {
|
||||||
|
fill: rgba(245,165,36,.16);
|
||||||
|
transform-origin: 36px 36px;
|
||||||
|
animation: radar-sweep 4.2s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state p {
|
.empty-title {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
max-width: 400px;
|
font-weight: 700;
|
||||||
margin: 0 auto;
|
letter-spacing: .18em;
|
||||||
line-height: 1.6;
|
text-transform: uppercase;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
.empty-copy {
|
||||||
|
font-size: 10.5px;
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 0 auto 12px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text-faint);
|
||||||
|
}
|
||||||
|
.empty-hint {
|
||||||
|
font-size: 9.5px;
|
||||||
|
letter-spacing: .1em;
|
||||||
|
color: var(--amber);
|
||||||
|
}
|
||||||
|
.empty-hint::before { content: "» "; }
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
/* ------------------------------------------------ page footer */
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
.page-footer {
|
||||||
background: transparent;
|
margin-top: 14px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: .14em;
|
||||||
|
color: var(--text-faint);
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
.footer-sep { margin: 0 8px; color: var(--line-strong); }
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
/* ------------------------------------------------ scrollbars */
|
||||||
background: #333;
|
|
||||||
border-radius: 4px;
|
::-webkit-scrollbar { width: 7px; height: 7px; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #1c2536; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: #2a3650; }
|
||||||
|
|
||||||
|
/* ------------------------------------------------ reduced motion */
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after {
|
||||||
|
animation-duration: .01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: .01ms !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 175 KiB |
@@ -1,23 +1,71 @@
|
|||||||
document.addEventListener("DOMContentLoaded", init);
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
|
const REDUCE_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
|
||||||
|
let lastKeywords = 0;
|
||||||
|
let lastFindings = 0;
|
||||||
|
let termBooted = false;
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const versionLabel = document.getElementById("versionLabel");
|
const versionLabel = document.getElementById("versionLabel");
|
||||||
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
|
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
|
||||||
await renderKeywords();
|
await renderKeywords();
|
||||||
await renderStats();
|
await renderStats();
|
||||||
|
updateTermLine(true);
|
||||||
|
termBooted = true;
|
||||||
document.getElementById("keywordForm").addEventListener("submit", handleAddKeyword);
|
document.getElementById("keywordForm").addEventListener("submit", handleAddKeyword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eased count-up for stat numerals */
|
||||||
|
function setNumber(el, target) {
|
||||||
|
if (!el) return;
|
||||||
|
const from = parseInt(el.textContent, 10);
|
||||||
|
const start = Number.isFinite(from) ? from : 0;
|
||||||
|
if (REDUCE_MOTION || start === target) {
|
||||||
|
el.textContent = target;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const duration = 380;
|
||||||
|
const t0 = performance.now();
|
||||||
|
(function tick(t) {
|
||||||
|
const p = Math.min(1, (t - t0) / duration);
|
||||||
|
const eased = 1 - Math.pow(1 - p, 3);
|
||||||
|
el.textContent = Math.round(start + (target - start) * eased);
|
||||||
|
if (p < 1) requestAnimationFrame(tick);
|
||||||
|
})(t0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* terminal status line — types once per popup open, instant after */
|
||||||
|
function updateTermLine(typed) {
|
||||||
|
const el = document.getElementById("termText");
|
||||||
|
if (!el) return;
|
||||||
|
const text = `kf> scan armed :: ${lastKeywords} keywords :: ${lastFindings} findings`;
|
||||||
|
if (!typed || REDUCE_MOTION) {
|
||||||
|
el.textContent = text;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let i = 0;
|
||||||
|
(function type() {
|
||||||
|
el.textContent = text.slice(0, ++i);
|
||||||
|
if (i < text.length) setTimeout(type, 14);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
async function renderKeywords() {
|
async function renderKeywords() {
|
||||||
const response = await chrome.runtime.sendMessage({ type: "getKeywords" });
|
const response = await chrome.runtime.sendMessage({ type: "getKeywords" });
|
||||||
const keywords = response.keywords || [];
|
const keywords = response.keywords || [];
|
||||||
const list = document.getElementById("keywordList");
|
const list = document.getElementById("keywordList");
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
|
|
||||||
document.getElementById("keywordCount").textContent = keywords.length;
|
lastKeywords = keywords.length;
|
||||||
|
setNumber(document.getElementById("keywordCount"), keywords.length);
|
||||||
|
if (termBooted) updateTermLine(false);
|
||||||
|
|
||||||
if (keywords.length === 0) {
|
if (keywords.length === 0) {
|
||||||
list.innerHTML = '<li class="empty-state">No keywords configured</li>';
|
const empty = document.createElement("li");
|
||||||
|
empty.className = "empty-state";
|
||||||
|
empty.textContent = "no keywords configured";
|
||||||
|
list.appendChild(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +79,7 @@ async function renderKeywords() {
|
|||||||
|
|
||||||
const removeBtn = document.createElement("button");
|
const removeBtn = document.createElement("button");
|
||||||
removeBtn.className = "keyword-remove";
|
removeBtn.className = "keyword-remove";
|
||||||
removeBtn.textContent = "\u00D7";
|
removeBtn.textContent = "×";
|
||||||
removeBtn.title = `Remove "${kw}"`;
|
removeBtn.title = `Remove "${kw}"`;
|
||||||
removeBtn.addEventListener("click", () => handleRemoveKeyword(kw));
|
removeBtn.addEventListener("click", () => handleRemoveKeyword(kw));
|
||||||
|
|
||||||
@@ -44,7 +92,9 @@ async function renderKeywords() {
|
|||||||
async function renderStats() {
|
async function renderStats() {
|
||||||
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
|
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
|
||||||
const findings = response.findings || [];
|
const findings = response.findings || [];
|
||||||
document.getElementById("findingCount").textContent = findings.length;
|
lastFindings = findings.length;
|
||||||
|
setNumber(document.getElementById("findingCount"), findings.length);
|
||||||
|
if (termBooted) updateTermLine(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAddKeyword(e) {
|
async function handleAddKeyword(e) {
|
||||||
@@ -68,6 +118,7 @@ async function handleAddKeyword(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input.value = "";
|
input.value = "";
|
||||||
|
input.focus();
|
||||||
await renderKeywords();
|
await renderKeywords();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
let allFindings = [];
|
let allFindings = [];
|
||||||
|
let firstRenderDone = false;
|
||||||
|
|
||||||
|
const REDUCE_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", init);
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const versionLabel = document.getElementById("versionLabel");
|
const versionLabel = document.getElementById("versionLabel");
|
||||||
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
|
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
|
||||||
|
|
||||||
|
renderSkeleton();
|
||||||
|
|
||||||
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
|
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
|
||||||
allFindings = response.findings || [];
|
allFindings = response.findings || [];
|
||||||
|
|
||||||
populateFilters();
|
populateFilters();
|
||||||
renderStats();
|
renderStats();
|
||||||
renderFindings();
|
renderFindings();
|
||||||
|
renderTicker();
|
||||||
|
firstRenderDone = true;
|
||||||
|
|
||||||
document.getElementById("severityFilter").addEventListener("change", renderFindings);
|
document.getElementById("severityFilter").addEventListener("change", renderFindings);
|
||||||
document.getElementById("typeFilter").addEventListener("change", renderFindings);
|
document.getElementById("typeFilter").addEventListener("change", renderFindings);
|
||||||
@@ -19,6 +27,57 @@ async function init() {
|
|||||||
document.getElementById("exportJsonBtn").addEventListener("click", exportJson);
|
document.getElementById("exportJsonBtn").addEventListener("click", exportJson);
|
||||||
document.getElementById("exportCsvBtn").addEventListener("click", exportCsv);
|
document.getElementById("exportCsvBtn").addEventListener("click", exportCsv);
|
||||||
document.getElementById("clearBtn").addEventListener("click", clearAll);
|
document.getElementById("clearBtn").addEventListener("click", clearAll);
|
||||||
|
|
||||||
|
const searchBox = document.getElementById("searchBox");
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
const tag = document.activeElement && document.activeElement.tagName;
|
||||||
|
const typing = tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA";
|
||||||
|
if (e.key === "/" && !typing) {
|
||||||
|
e.preventDefault();
|
||||||
|
searchBox.focus();
|
||||||
|
} else if (e.key === "Escape" && document.activeElement === searchBox) {
|
||||||
|
searchBox.value = "";
|
||||||
|
renderFindings();
|
||||||
|
searchBox.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eased count-up for stat numerals */
|
||||||
|
function setNumber(el, target) {
|
||||||
|
if (!el) return;
|
||||||
|
const from = parseInt(el.textContent, 10);
|
||||||
|
const start = Number.isFinite(from) ? from : 0;
|
||||||
|
if (REDUCE_MOTION || start === target) {
|
||||||
|
el.textContent = target;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const duration = 400;
|
||||||
|
const t0 = performance.now();
|
||||||
|
(function tick(t) {
|
||||||
|
const p = Math.min(1, (t - t0) / duration);
|
||||||
|
const eased = 1 - Math.pow(1 - p, 3);
|
||||||
|
el.textContent = Math.round(start + (target - start) * eased);
|
||||||
|
if (p < 1) requestAnimationFrame(tick);
|
||||||
|
})(t0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSkeleton() {
|
||||||
|
const tbody = document.getElementById("findingsBody");
|
||||||
|
tbody.classList.remove("fresh");
|
||||||
|
tbody.innerHTML = "";
|
||||||
|
const widths = ["skl-w2", "skl-w1", "skl-w3", "skl-w4", "skl-w2", "skl-w3"];
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.className = "skl-row";
|
||||||
|
const td = document.createElement("td");
|
||||||
|
td.colSpan = 10;
|
||||||
|
const bar = document.createElement("div");
|
||||||
|
bar.className = "skl " + widths[i % widths.length];
|
||||||
|
td.appendChild(bar);
|
||||||
|
tr.appendChild(td);
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFiltered() {
|
function getFiltered() {
|
||||||
@@ -67,54 +126,96 @@ function renderStats() {
|
|||||||
|
|
||||||
bar.innerHTML = "";
|
bar.innerHTML = "";
|
||||||
const stats = [
|
const stats = [
|
||||||
{ label: "Total", value: allFindings.length, cls: "stat-total" },
|
{ label: "Total", value: allFindings.length, cls: "stat-total", filter: "all" },
|
||||||
{ label: "Critical", value: critical, cls: "stat-critical" },
|
{ label: "Critical", value: critical, cls: "stat-critical", filter: "critical" },
|
||||||
{ label: "High", value: high, cls: "stat-high" },
|
{ label: "High", value: high, cls: "stat-high", filter: "high" },
|
||||||
{ label: "Medium", value: medium, cls: "stat-medium" },
|
{ label: "Medium", value: medium, cls: "stat-medium", filter: "medium" },
|
||||||
{ label: "Low", value: low, cls: "stat-low" },
|
{ label: "Low", value: low, cls: "stat-low", filter: "low" },
|
||||||
{ label: "Domains", value: domains, cls: "stat-domains" },
|
{ label: "Domains", value: domains, cls: "stat-domains", filter: null },
|
||||||
];
|
];
|
||||||
for (const s of stats) {
|
for (const s of stats) {
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = `stat-item ${s.cls}`;
|
el.className = `stat-item ${s.cls}`;
|
||||||
const num = document.createElement("span");
|
|
||||||
num.className = "stat-num";
|
const top = document.createElement("span");
|
||||||
num.textContent = s.value;
|
top.className = "stat-top";
|
||||||
|
const led = document.createElement("span");
|
||||||
|
led.className = "stat-led";
|
||||||
const lbl = document.createElement("span");
|
const lbl = document.createElement("span");
|
||||||
lbl.className = "stat-lbl";
|
lbl.className = "stat-lbl";
|
||||||
lbl.textContent = s.label;
|
lbl.textContent = s.label;
|
||||||
|
top.appendChild(led);
|
||||||
|
top.appendChild(lbl);
|
||||||
|
|
||||||
|
const num = document.createElement("span");
|
||||||
|
num.className = "stat-num";
|
||||||
|
|
||||||
|
el.appendChild(top);
|
||||||
el.appendChild(num);
|
el.appendChild(num);
|
||||||
el.appendChild(lbl);
|
setNumber(num, s.value);
|
||||||
|
|
||||||
|
if (s.filter) {
|
||||||
|
el.dataset.filter = s.filter;
|
||||||
|
el.title = s.filter === "all" ? "Show all severities" : `Filter: ${s.label.toLowerCase()} only`;
|
||||||
|
el.addEventListener("click", () => {
|
||||||
|
document.getElementById("severityFilter").value = s.filter;
|
||||||
|
renderFindings();
|
||||||
|
});
|
||||||
|
}
|
||||||
bar.appendChild(el);
|
bar.appendChild(el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncStatActive() {
|
||||||
|
const current = document.getElementById("severityFilter").value;
|
||||||
|
document.querySelectorAll(".stat-item[data-filter]").forEach((el) => {
|
||||||
|
el.classList.toggle("active", el.dataset.filter === current);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderFindings() {
|
function renderFindings() {
|
||||||
const filtered = getFiltered();
|
const filtered = getFiltered();
|
||||||
const tbody = document.getElementById("findingsBody");
|
const tbody = document.getElementById("findingsBody");
|
||||||
const empty = document.getElementById("emptyState");
|
const empty = document.getElementById("emptyState");
|
||||||
|
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
|
tbody.classList.toggle("fresh", !firstRenderDone && !REDUCE_MOTION);
|
||||||
|
|
||||||
|
updateMeta(filtered.length);
|
||||||
|
syncStatActive();
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
if (filtered.length === 0) {
|
||||||
|
const title = document.getElementById("emptyTitle");
|
||||||
|
const copy = document.getElementById("emptyCopy");
|
||||||
|
if (allFindings.length === 0) {
|
||||||
|
title.textContent = "No findings yet";
|
||||||
|
copy.textContent = "KeyFinder passively scans every page you visit. Browse around — leaked keys, tokens and credentials will surface here.";
|
||||||
|
} else {
|
||||||
|
title.textContent = "No matches in view";
|
||||||
|
copy.textContent = "The current filters hide every finding. Adjust or clear them to bring results back.";
|
||||||
|
}
|
||||||
empty.hidden = false;
|
empty.hidden = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
empty.hidden = true;
|
empty.hidden = true;
|
||||||
|
|
||||||
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
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.sort((a, b) => (severityOrder[a.severity] ?? 5) - (severityOrder[b.severity] ?? 5));
|
||||||
|
|
||||||
filtered.forEach((f, i) => {
|
filtered.forEach((f, i) => {
|
||||||
|
const sev = f.severity || "medium";
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
|
tr.className = "sev-" + sev;
|
||||||
|
if (i < 30) tr.style.setProperty("--i", i);
|
||||||
|
|
||||||
const tdNum = document.createElement("td");
|
const tdNum = document.createElement("td");
|
||||||
tdNum.textContent = i + 1;
|
tdNum.textContent = String(i + 1).padStart(2, "0");
|
||||||
|
tdNum.className = "td-num";
|
||||||
|
|
||||||
const tdSev = document.createElement("td");
|
const tdSev = document.createElement("td");
|
||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
badge.className = `badge badge-${f.severity || "medium"}`;
|
badge.className = `badge badge-${sev}`;
|
||||||
badge.textContent = (f.severity || "medium").toUpperCase();
|
badge.textContent = sev.toUpperCase();
|
||||||
tdSev.appendChild(badge);
|
tdSev.appendChild(badge);
|
||||||
|
|
||||||
const tdProvider = document.createElement("td");
|
const tdProvider = document.createElement("td");
|
||||||
@@ -129,7 +230,11 @@ function renderFindings() {
|
|||||||
const matchCode = document.createElement("code");
|
const matchCode = document.createElement("code");
|
||||||
matchCode.textContent = f.match || "-";
|
matchCode.textContent = f.match || "-";
|
||||||
matchCode.className = "match-value";
|
matchCode.className = "match-value";
|
||||||
matchCode.title = f.match || "";
|
matchCode.title = "click to copy\n" + (f.match || "");
|
||||||
|
matchCode.addEventListener("click", () => {
|
||||||
|
navigator.clipboard.writeText(f.match || "");
|
||||||
|
flashCopied(copyBtn);
|
||||||
|
});
|
||||||
tdMatch.appendChild(matchCode);
|
tdMatch.appendChild(matchCode);
|
||||||
|
|
||||||
const tdType = document.createElement("td");
|
const tdType = document.createElement("td");
|
||||||
@@ -143,42 +248,45 @@ function renderFindings() {
|
|||||||
tdDomain.className = "td-domain";
|
tdDomain.className = "td-domain";
|
||||||
|
|
||||||
const tdSource = document.createElement("td");
|
const tdSource = document.createElement("td");
|
||||||
|
tdSource.className = "td-source";
|
||||||
if (f.url && f.url.startsWith("http")) {
|
if (f.url && f.url.startsWith("http")) {
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = f.url;
|
a.href = f.url;
|
||||||
a.target = "_blank";
|
a.target = "_blank";
|
||||||
a.rel = "noopener";
|
a.rel = "noopener";
|
||||||
a.textContent = truncateUrl(f.url, 40);
|
a.textContent = truncateUrl(f.url, 30);
|
||||||
a.title = f.url;
|
a.title = f.url;
|
||||||
tdSource.appendChild(a);
|
tdSource.appendChild(a);
|
||||||
} else {
|
} else {
|
||||||
tdSource.textContent = f.url ? truncateUrl(f.url, 40) : "-";
|
tdSource.textContent = f.url ? truncateUrl(f.url, 30) : "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
const tdTime = document.createElement("td");
|
const tdTime = document.createElement("td");
|
||||||
tdTime.textContent = f.timestamp ? formatTime(f.timestamp) : "-";
|
tdTime.textContent = f.timestamp ? formatTime(f.timestamp) : "-";
|
||||||
tdTime.className = "td-time";
|
tdTime.className = "td-time";
|
||||||
|
if (f.timestamp) tdTime.title = new Date(f.timestamp).toLocaleString();
|
||||||
|
|
||||||
const tdActions = document.createElement("td");
|
const tdActions = document.createElement("td");
|
||||||
|
tdActions.className = "td-actions";
|
||||||
const copyBtn = document.createElement("button");
|
const copyBtn = document.createElement("button");
|
||||||
copyBtn.className = "btn-icon";
|
copyBtn.className = "btn-icon";
|
||||||
copyBtn.textContent = "Copy";
|
copyBtn.textContent = "copy";
|
||||||
copyBtn.title = "Copy match value";
|
copyBtn.title = "Copy match value";
|
||||||
copyBtn.addEventListener("click", () => {
|
copyBtn.addEventListener("click", () => {
|
||||||
navigator.clipboard.writeText(f.match || "");
|
navigator.clipboard.writeText(f.match || "");
|
||||||
copyBtn.textContent = "Done";
|
flashCopied(copyBtn);
|
||||||
setTimeout(() => (copyBtn.textContent = "Copy"), 1500);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const delBtn = document.createElement("button");
|
const delBtn = document.createElement("button");
|
||||||
delBtn.className = "btn-icon btn-icon-danger";
|
delBtn.className = "btn-icon btn-icon-danger";
|
||||||
delBtn.textContent = "Del";
|
delBtn.textContent = "del";
|
||||||
delBtn.title = "Remove finding";
|
delBtn.title = "Remove finding";
|
||||||
delBtn.addEventListener("click", async () => {
|
delBtn.addEventListener("click", async () => {
|
||||||
await chrome.runtime.sendMessage({ type: "removeFinding", findingId: f.id });
|
await chrome.runtime.sendMessage({ type: "removeFinding", findingId: f.id });
|
||||||
allFindings = allFindings.filter((x) => x.id !== f.id);
|
allFindings = allFindings.filter((x) => x.id !== f.id);
|
||||||
renderStats();
|
renderStats();
|
||||||
renderFindings();
|
renderFindings();
|
||||||
|
renderTicker();
|
||||||
});
|
});
|
||||||
|
|
||||||
tdActions.appendChild(copyBtn);
|
tdActions.appendChild(copyBtn);
|
||||||
@@ -198,6 +306,58 @@ function renderFindings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flashCopied(btn) {
|
||||||
|
if (!btn) return;
|
||||||
|
btn.textContent = "copied";
|
||||||
|
btn.classList.add("copied");
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = "copy";
|
||||||
|
btn.classList.remove("copied");
|
||||||
|
}, 1200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMeta(shown) {
|
||||||
|
const metaLine = document.getElementById("metaLine");
|
||||||
|
if (metaLine) {
|
||||||
|
metaLine.innerHTML = "";
|
||||||
|
metaLine.append(
|
||||||
|
"showing ",
|
||||||
|
strong(shown),
|
||||||
|
" of ",
|
||||||
|
strong(allFindings.length)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const shellMeta = document.getElementById("shellMeta");
|
||||||
|
if (shellMeta) shellMeta.textContent = shown + (shown === 1 ? " row" : " rows");
|
||||||
|
}
|
||||||
|
|
||||||
|
function strong(n) {
|
||||||
|
const el = document.createElement("strong");
|
||||||
|
el.textContent = n;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTicker() {
|
||||||
|
const ticker = document.getElementById("ticker");
|
||||||
|
const tickerText = document.getElementById("tickerText");
|
||||||
|
if (!ticker || !tickerText) return;
|
||||||
|
if (!allFindings.length) {
|
||||||
|
ticker.hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const latest = [...allFindings].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0];
|
||||||
|
tickerText.innerHTML = "";
|
||||||
|
const head = document.createElement("span");
|
||||||
|
head.textContent = `${latest.provider || "unknown"} · ${latest.patternName || "match"} @ ${latest.domain || "—"} `;
|
||||||
|
tickerText.appendChild(head);
|
||||||
|
if (latest.match) {
|
||||||
|
const code = document.createElement("code");
|
||||||
|
code.textContent = truncateUrl(latest.match, 28);
|
||||||
|
tickerText.appendChild(code);
|
||||||
|
}
|
||||||
|
ticker.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
function truncateUrl(url, max) {
|
function truncateUrl(url, max) {
|
||||||
if (url.length <= max) return url;
|
if (url.length <= max) return url;
|
||||||
return url.substring(0, max - 3) + "...";
|
return url.substring(0, max - 3) + "...";
|
||||||
@@ -205,7 +365,15 @@ function truncateUrl(url, max) {
|
|||||||
|
|
||||||
function formatTime(ts) {
|
function formatTime(ts) {
|
||||||
const d = new Date(ts);
|
const d = new Date(ts);
|
||||||
return d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
const diff = Date.now() - d.getTime();
|
||||||
|
const m = Math.floor(diff / 60000);
|
||||||
|
if (m < 1) return "just now";
|
||||||
|
if (m < 60) return m + "m ago";
|
||||||
|
const h = Math.floor(m / 60);
|
||||||
|
if (h < 24) return h + "h ago";
|
||||||
|
const days = Math.floor(h / 24);
|
||||||
|
if (days < 7) return days + "d ago";
|
||||||
|
return d.toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportJson() {
|
function exportJson() {
|
||||||
@@ -255,4 +423,5 @@ async function clearAll() {
|
|||||||
allFindings = [];
|
allFindings = [];
|
||||||
renderStats();
|
renderStats();
|
||||||
renderFindings();
|
renderFindings();
|
||||||
|
renderTicker();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "KeyFinder",
|
"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.",
|
"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",
|
"version": "2.2.1",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "KeyFinder",
|
"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.",
|
"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",
|
"version": "2.2.1",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"action": {
|
"action": {
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
|
|||||||
@@ -9,31 +9,36 @@
|
|||||||
<body>
|
<body>
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="header-brand">
|
<div class="header-brand">
|
||||||
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
|
<span class="radar" aria-hidden="true">
|
||||||
<h1>KeyFinder</h1>
|
<img src="icons/icon48.png" alt="" class="header-icon">
|
||||||
<span class="version" id="versionLabel"></span>
|
</span>
|
||||||
|
<div class="brand-text">
|
||||||
|
<h1>KEYFINDER<span class="version" id="versionLabel"></span></h1>
|
||||||
|
<p class="header-tagline">passive secret discovery</p>
|
||||||
|
</div>
|
||||||
|
<span class="live-badge" title="Passive scanning is active"><span class="live-dot"></span>ARMED</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="header-tagline">Passive API key & secret discovery</p>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="stats" id="stats">
|
<section class="stats" id="stats">
|
||||||
<div class="stat-card">
|
<div class="stat-card stat-findings">
|
||||||
<span class="stat-number" id="findingCount">-</span>
|
<span class="stat-top"><span class="stat-led" aria-hidden="true"></span><span class="stat-label">Findings</span></span>
|
||||||
<span class="stat-label">Findings</span>
|
<span class="stat-number" id="findingCount">–</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card stat-keywords">
|
||||||
<span class="stat-number" id="keywordCount">-</span>
|
<span class="stat-top"><span class="stat-led" aria-hidden="true"></span><span class="stat-label">Keywords</span></span>
|
||||||
<span class="stat-label">Keywords</span>
|
<span class="stat-number" id="keywordCount">–</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2 class="section-title">Keywords</h2>
|
<h2 class="section-title">Watchlist<span class="section-sub">// custom keywords</span></h2>
|
||||||
<form id="keywordForm" class="keyword-form">
|
<form id="keywordForm" class="keyword-form">
|
||||||
|
<span class="prompt" aria-hidden="true">›</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="keywordInput"
|
id="keywordInput"
|
||||||
placeholder="Add a keyword (e.g. api_key)"
|
placeholder="add keyword, e.g. api_key"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
>
|
>
|
||||||
@@ -44,8 +49,9 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
|
<div class="term-line" aria-live="polite"><span class="term-text" id="termText"></span><span class="term-cursor" aria-hidden="true"></span></div>
|
||||||
<a href="results.html" target="_blank" id="resultsLink" class="results-btn">
|
<a href="results.html" target="_blank" id="resultsLink" class="results-btn">
|
||||||
View Findings
|
View Findings <span class="btn-arrow" aria-hidden="true">→</span>
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,127 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>KeyFinder — banner</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
width: 1280px;
|
||||||
|
height: 640px;
|
||||||
|
background: #05070c;
|
||||||
|
font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
color: #d6dde8;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.lockup { text-align: center; }
|
||||||
|
|
||||||
|
/* radar ring around the key */
|
||||||
|
.radar {
|
||||||
|
position: relative;
|
||||||
|
width: 108px;
|
||||||
|
height: 108px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #2a3650;
|
||||||
|
background: #090d14;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.radar::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -1px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: conic-gradient(from 0deg, #f5a524 0deg, rgba(245,165,36,.15) 60deg, transparent 95deg);
|
||||||
|
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 1.5px));
|
||||||
|
mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 1.5px));
|
||||||
|
}
|
||||||
|
.radar::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 22px; left: 26px;
|
||||||
|
width: 4px; height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #45d0c4;
|
||||||
|
}
|
||||||
|
.radar img { width: 58px; height: 58px; }
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 26px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 62px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .22em;
|
||||||
|
padding-left: .22em;
|
||||||
|
}
|
||||||
|
.ver {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: 14px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
color: #525d70;
|
||||||
|
border: 1px solid #1a2233;
|
||||||
|
padding: 3px 7px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
margin-top: 16px;
|
||||||
|
color: #f5a524;
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: .26em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chips {
|
||||||
|
margin-top: 34px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.chip {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 7px 14px;
|
||||||
|
border: 1px solid #2a3650;
|
||||||
|
color: #8b96a8;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
.chip i {
|
||||||
|
width: 4px; height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
.chip-amber { color: #f5a524; border-color: rgba(245,165,36,.45); }
|
||||||
|
.chip-teal { color: #45d0c4; border-color: rgba(69,208,196,.45); }
|
||||||
|
|
||||||
|
.providers {
|
||||||
|
margin-top: 30px;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: .14em;
|
||||||
|
color: #525d70;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.providers b { color: #f5a524; font-weight: 600; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="lockup">
|
||||||
|
<div class="radar"><img src="../icons/icon128.png" alt=""></div>
|
||||||
|
<h1>KEYFINDER<span class="ver">[v2.2.1]</span></h1>
|
||||||
|
<div class="tag">// PASSIVE API KEY & SECRET DISCOVERY</div>
|
||||||
|
<div class="chips">
|
||||||
|
<span class="chip chip-amber"><i></i>Chrome extension</span>
|
||||||
|
<span class="chip chip-teal"><i></i>Firefox add-on</span>
|
||||||
|
<span class="chip"><i></i>Manifest V3</span>
|
||||||
|
<span class="chip"><i></i>80+ patterns</span>
|
||||||
|
</div>
|
||||||
|
<div class="providers"><b>AWS</b> · GCP · Azure · Stripe · GitHub · Slack · OpenAI · and 70+ more</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>KeyFinder — film title</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
width: 1280px;
|
||||||
|
height: 720px;
|
||||||
|
background: #05070c;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
}
|
||||||
|
.card { text-align: center; }
|
||||||
|
.icon { width: 76px; height: 76px; }
|
||||||
|
h1 {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 46px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .28em;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-left: .28em; /* optical centering with tracking */
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
color: #f5a524;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: .22em;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.spec {
|
||||||
|
color: #525d70;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: .16em;
|
||||||
|
margin-top: 26px;
|
||||||
|
}
|
||||||
|
.spec b { color: #8b96a8; font-weight: 600; }
|
||||||
|
.led {
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px; height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #3ddc97;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<img class="icon" src="../icons/icon128.png" alt="">
|
||||||
|
<h1>KEYFINDER</h1>
|
||||||
|
<div class="tag">// PASSIVE SECRET DISCOVERY</div>
|
||||||
|
<div class="spec"><span class="led"></span><b>v2.2.1</b> · 80+ PATTERNS · 10 ATTACK SURFACES · LOCAL-ONLY</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 452 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,61 @@
|
|||||||
|
<!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">
|
||||||
|
<span class="radar" aria-hidden="true">
|
||||||
|
<img src="../icons/icon48.png" alt="" class="header-icon">
|
||||||
|
</span>
|
||||||
|
<div class="brand-text">
|
||||||
|
<h1>KEYFINDER<span class="version" id="versionLabel"></span></h1>
|
||||||
|
<p class="header-tagline">passive secret discovery</p>
|
||||||
|
</div>
|
||||||
|
<span class="live-badge" title="Passive scanning is active"><span class="live-dot"></span>ARMED</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="stats" id="stats">
|
||||||
|
<div class="stat-card stat-findings">
|
||||||
|
<span class="stat-top"><span class="stat-led" aria-hidden="true"></span><span class="stat-label">Findings</span></span>
|
||||||
|
<span class="stat-number" id="findingCount">–</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-keywords">
|
||||||
|
<span class="stat-top"><span class="stat-led" aria-hidden="true"></span><span class="stat-label">Keywords</span></span>
|
||||||
|
<span class="stat-number" id="keywordCount">–</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2 class="section-title">Watchlist<span class="section-sub">// custom keywords</span></h2>
|
||||||
|
<form id="keywordForm" class="keyword-form">
|
||||||
|
<span class="prompt" aria-hidden="true">›</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="keywordInput"
|
||||||
|
placeholder="add 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">
|
||||||
|
<div class="term-line" aria-live="polite"><span class="term-text" id="termText"></span><span class="term-cursor" aria-hidden="true"></span></div>
|
||||||
|
<a href="../results.html" target="_blank" id="resultsLink" class="results-btn">
|
||||||
|
View Findings <span class="btn-arrow" aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="preview-stub.js"></script>
|
||||||
|
<script src="../js/popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Preview-only stub for the chrome.* extension API with realistic sample data.
|
||||||
|
// Loaded before the real popup.js / results.js in preview/*.html files.
|
||||||
|
// NOT shipped — build.sh only copies the explicit SHARED_FILES list.
|
||||||
|
(function () {
|
||||||
|
const NOW = Date.now();
|
||||||
|
const MIN = 60 * 1000;
|
||||||
|
const HOUR = 60 * MIN;
|
||||||
|
const DAY = 24 * HOUR;
|
||||||
|
|
||||||
|
const SAMPLE_FINDINGS = [
|
||||||
|
{ id: "f1", severity: "critical", provider: "AWS", patternName: "AWS Access Key ID", match: "AKIAIOSFODNN7EXAMPLE", type: "inline-script", domain: "dashboard.acme.io", url: "https://dashboard.acme.io/assets/app.js", pageUrl: "https://dashboard.acme.io/", timestamp: NOW - 4 * MIN },
|
||||||
|
{ id: "f2", severity: "critical", provider: "Stripe", patternName: "Stripe Live Secret Key", match: "sk_live_4eC39HqLyj…T1zdp7dc", type: "network-response", domain: "api.shopfront.dev", url: "https://api.shopfront.dev/v2/config", pageUrl: "https://shopfront.dev/", timestamp: NOW - 26 * MIN },
|
||||||
|
{ id: "f3", severity: "critical", provider: "OpenAI", patternName: "OpenAI API Key", match: "sk-proj-abc123def456ghi789jkl", type: "web-storage", domain: "playground.internal.tools", url: "https://playground.internal.tools/", pageUrl: "https://playground.internal.tools/", timestamp: NOW - 2 * HOUR },
|
||||||
|
{ id: "f4", severity: "high", provider: "GitHub", patternName: "GitHub Personal Access Token", match: "ghp_16C7e42F292c6912E7710c838347Ae178B4a", type: "html-comment", domain: "gitmirror.example.org", url: "https://gitmirror.example.org/login", pageUrl: "https://gitmirror.example.org/", timestamp: NOW - 3 * HOUR },
|
||||||
|
{ id: "f5", severity: "high", provider: "Slack", patternName: "Slack Bot Token", match: "xoxb-17653672481-19874698323-pdLjLl…CwXgE", type: "inline-script", domain: "status.teamchat.app", url: "https://status.teamchat.app/embed.js", pageUrl: "https://status.teamchat.app/", timestamp: NOW - 5 * HOUR },
|
||||||
|
{ id: "f6", severity: "high", provider: "MongoDB", patternName: "MongoDB Connection String", match: "mongodb+srv://admin:p4ssw0rd@cluster0.mongodb.net/prod", type: "data-attribute", domain: "metrics.saasly.co", url: "https://metrics.saasly.co/", pageUrl: "https://metrics.saasly.co/", timestamp: NOW - 9 * HOUR },
|
||||||
|
{ id: "f7", severity: "high", provider: "DOM Scan", patternName: "Sensitive Meta Tag", match: "csrf-token=9f8e7d6c5b4a3210", type: "meta-tag", domain: "legacy.corpnet.com", url: "https://legacy.corpnet.com/index.html", pageUrl: "https://legacy.corpnet.com/", timestamp: NOW - 14 * HOUR },
|
||||||
|
{ id: "f8", severity: "medium", provider: "Generic", patternName: "JSON Web Token (JWT)", match: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U", type: "url-param", domain: "auth.redirectlab.net", url: "https://auth.redirectlab.net/callback?token=eyJhbGciOi...", pageUrl: "https://redirectlab.net/", timestamp: NOW - 20 * HOUR },
|
||||||
|
{ id: "f9", severity: "medium", provider: "DOM Scan", patternName: "Hidden Form Field", match: "session_id=a8f5f167f44f4964e6c998dee827110c", type: "hidden-input", domain: "shop.example.com", url: "https://shop.example.com/checkout", pageUrl: "https://shop.example.com/", timestamp: NOW - 1 * DAY },
|
||||||
|
{ id: "f10", severity: "medium", provider: "URL Scan", patternName: "High-Entropy URL Parameter", match: "sig=4c4f6e6f2d9a4b8c8d7e6f5a4b3c2d1e", type: "url-param", domain: "cdn.medialab.io", url: "https://cdn.medialab.io/v/assets?sig=4c4f6e...", pageUrl: "https://medialab.io/", timestamp: NOW - 2 * DAY },
|
||||||
|
{ id: "f11", severity: "medium", provider: "Web Storage", patternName: "localStorage Secret", match: "refresh_token=def502001a2b3c4d5e6f", type: "web-storage", domain: "app.notesync.dev", url: "https://app.notesync.dev/", pageUrl: "https://app.notesync.dev/", timestamp: NOW - 3 * DAY },
|
||||||
|
{ id: "f12", severity: "low", provider: "Generic", patternName: "Keyword: client_id", match: "client_id=Iv1.8a61f9b3a7aba766", type: "script-src", domain: "login.oauthflow.app", url: "https://login.oauthflow.app/js/oauth.js?client_id=Iv1.8a61f9b3a7aba766", pageUrl: "https://login.oauthflow.app/", timestamp: NOW - 4 * DAY },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SAMPLE_KEYWORDS = [
|
||||||
|
"key", "api_key", "apikey", "api-key", "secret", "token",
|
||||||
|
"access_token", "auth", "credential", "password", "client_id", "client_secret",
|
||||||
|
];
|
||||||
|
|
||||||
|
window.chrome = {
|
||||||
|
runtime: {
|
||||||
|
getManifest: () => ({ version: "2.2.1" }),
|
||||||
|
sendMessage: async (msg) => {
|
||||||
|
switch (msg && msg.type) {
|
||||||
|
case "getFindings": return { findings: [] };
|
||||||
|
case "getKeywords": return { keywords: SAMPLE_KEYWORDS.slice() };
|
||||||
|
case "addKeyword": return { ok: true };
|
||||||
|
case "removeKeyword": return { ok: true };
|
||||||
|
case "removeFinding": return { ok: true };
|
||||||
|
case "clearFindings": return { ok: true };
|
||||||
|
default: return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Preview-only stub for the chrome.* extension API with realistic sample data.
|
||||||
|
// Loaded before the real popup.js / results.js in preview/*.html files.
|
||||||
|
// NOT shipped — build.sh only copies the explicit SHARED_FILES list.
|
||||||
|
(function () {
|
||||||
|
const NOW = Date.now();
|
||||||
|
const MIN = 60 * 1000;
|
||||||
|
const HOUR = 60 * MIN;
|
||||||
|
const DAY = 24 * HOUR;
|
||||||
|
|
||||||
|
const SAMPLE_FINDINGS = [
|
||||||
|
{ id: "f1", severity: "critical", provider: "AWS", patternName: "AWS Access Key ID", match: "AKIAIOSFODNN7EXAMPLE", type: "inline-script", domain: "dashboard.acme.io", url: "https://dashboard.acme.io/assets/app.js", pageUrl: "https://dashboard.acme.io/", timestamp: NOW - 4 * MIN },
|
||||||
|
{ id: "f2", severity: "critical", provider: "Stripe", patternName: "Stripe Live Secret Key", match: "sk_live_4eC39HqLyj…T1zdp7dc", type: "network-response", domain: "api.shopfront.dev", url: "https://api.shopfront.dev/v2/config", pageUrl: "https://shopfront.dev/", timestamp: NOW - 26 * MIN },
|
||||||
|
{ id: "f3", severity: "critical", provider: "OpenAI", patternName: "OpenAI API Key", match: "sk-proj-abc123def456ghi789jkl", type: "web-storage", domain: "playground.internal.tools", url: "https://playground.internal.tools/", pageUrl: "https://playground.internal.tools/", timestamp: NOW - 2 * HOUR },
|
||||||
|
{ id: "f4", severity: "high", provider: "GitHub", patternName: "GitHub Personal Access Token", match: "ghp_16C7e42F292c6912E7710c838347Ae178B4a", type: "html-comment", domain: "gitmirror.example.org", url: "https://gitmirror.example.org/login", pageUrl: "https://gitmirror.example.org/", timestamp: NOW - 3 * HOUR },
|
||||||
|
{ id: "f5", severity: "high", provider: "Slack", patternName: "Slack Bot Token", match: "xoxb-17653672481-19874698323-pdLjLl…CwXgE", type: "inline-script", domain: "status.teamchat.app", url: "https://status.teamchat.app/embed.js", pageUrl: "https://status.teamchat.app/", timestamp: NOW - 5 * HOUR },
|
||||||
|
{ id: "f6", severity: "high", provider: "MongoDB", patternName: "MongoDB Connection String", match: "mongodb+srv://admin:p4ssw0rd@cluster0.mongodb.net/prod", type: "data-attribute", domain: "metrics.saasly.co", url: "https://metrics.saasly.co/", pageUrl: "https://metrics.saasly.co/", timestamp: NOW - 9 * HOUR },
|
||||||
|
{ id: "f7", severity: "high", provider: "DOM Scan", patternName: "Sensitive Meta Tag", match: "csrf-token=9f8e7d6c5b4a3210", type: "meta-tag", domain: "legacy.corpnet.com", url: "https://legacy.corpnet.com/index.html", pageUrl: "https://legacy.corpnet.com/", timestamp: NOW - 14 * HOUR },
|
||||||
|
{ id: "f8", severity: "medium", provider: "Generic", patternName: "JSON Web Token (JWT)", match: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U", type: "url-param", domain: "auth.redirectlab.net", url: "https://auth.redirectlab.net/callback?token=eyJhbGciOi...", pageUrl: "https://redirectlab.net/", timestamp: NOW - 20 * HOUR },
|
||||||
|
{ id: "f9", severity: "medium", provider: "DOM Scan", patternName: "Hidden Form Field", match: "session_id=a8f5f167f44f4964e6c998dee827110c", type: "hidden-input", domain: "shop.example.com", url: "https://shop.example.com/checkout", pageUrl: "https://shop.example.com/", timestamp: NOW - 1 * DAY },
|
||||||
|
{ id: "f10", severity: "medium", provider: "URL Scan", patternName: "High-Entropy URL Parameter", match: "sig=4c4f6e6f2d9a4b8c8d7e6f5a4b3c2d1e", type: "url-param", domain: "cdn.medialab.io", url: "https://cdn.medialab.io/v/assets?sig=4c4f6e...", pageUrl: "https://medialab.io/", timestamp: NOW - 2 * DAY },
|
||||||
|
{ id: "f11", severity: "medium", provider: "Web Storage", patternName: "localStorage Secret", match: "refresh_token=def502001a2b3c4d5e6f", type: "web-storage", domain: "app.notesync.dev", url: "https://app.notesync.dev/", pageUrl: "https://app.notesync.dev/", timestamp: NOW - 3 * DAY },
|
||||||
|
{ id: "f12", severity: "low", provider: "Generic", patternName: "Keyword: client_id", match: "client_id=Iv1.8a61f9b3a7aba766", type: "script-src", domain: "login.oauthflow.app", url: "https://login.oauthflow.app/js/oauth.js?client_id=Iv1.8a61f9b3a7aba766", pageUrl: "https://login.oauthflow.app/", timestamp: NOW - 4 * DAY },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SAMPLE_KEYWORDS = [
|
||||||
|
"key", "api_key", "apikey", "api-key", "secret", "token",
|
||||||
|
"access_token", "auth", "credential", "password", "client_id", "client_secret",
|
||||||
|
];
|
||||||
|
|
||||||
|
window.chrome = {
|
||||||
|
runtime: {
|
||||||
|
getManifest: () => ({ version: "2.2.1" }),
|
||||||
|
sendMessage: async (msg) => {
|
||||||
|
switch (msg && msg.type) {
|
||||||
|
case "getFindings": return { findings: SAMPLE_FINDINGS.slice() };
|
||||||
|
case "getKeywords": return { keywords: SAMPLE_KEYWORDS.slice() };
|
||||||
|
case "addKeyword": return { ok: true };
|
||||||
|
case "removeKeyword": return { ok: true };
|
||||||
|
case "removeFinding": return { ok: true };
|
||||||
|
case "clearFindings": return { ok: true };
|
||||||
|
default: return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>KeyFinder — Findings Console</title>
|
||||||
|
<link rel="stylesheet" href="../css/results.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-row">
|
||||||
|
<div class="header-left">
|
||||||
|
<span class="radar" aria-hidden="true">
|
||||||
|
<img src="../icons/icon48.png" alt="" class="header-icon">
|
||||||
|
</span>
|
||||||
|
<div class="brand-text">
|
||||||
|
<h1>KEYFINDER<span class="version" id="versionLabel"></span></h1>
|
||||||
|
<p class="header-sub">findings console</p>
|
||||||
|
</div>
|
||||||
|
<span class="live-badge" title="Passive scanning is active"><span class="live-dot"></span>PASSIVE</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<div class="search-wrap">
|
||||||
|
<span class="search-glyph" aria-hidden="true">⚲</span>
|
||||||
|
<input type="text" id="searchBox" placeholder="search findings…" autocomplete="off" spellcheck="false">
|
||||||
|
<kbd class="kbd" title="Press / to focus search">/</kbd>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button id="exportJsonBtn" class="btn btn-secondary" title="Download filtered findings as JSON">↓ JSON</button>
|
||||||
|
<button id="exportCsvBtn" class="btn btn-secondary" title="Download filtered findings as CSV">↓ CSV</button>
|
||||||
|
<button id="clearBtn" class="btn btn-danger" title="Remove all findings">× Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-row header-row-filters">
|
||||||
|
<div class="filter-group">
|
||||||
|
<label class="filter">
|
||||||
|
<span class="filter-label">sev</span>
|
||||||
|
<select id="severityFilter">
|
||||||
|
<option value="all">all</option>
|
||||||
|
<option value="critical">critical</option>
|
||||||
|
<option value="high">high</option>
|
||||||
|
<option value="medium">medium</option>
|
||||||
|
<option value="low">low</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="filter">
|
||||||
|
<span class="filter-label">type</span>
|
||||||
|
<select id="typeFilter">
|
||||||
|
<option value="all">all</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="filter">
|
||||||
|
<span class="filter-label">provider</span>
|
||||||
|
<select id="providerFilter">
|
||||||
|
<option value="all">all</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="meta-line" id="metaLine"></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="stats-bar" id="statsBar"></div>
|
||||||
|
|
||||||
|
<div class="ticker" id="ticker" hidden>
|
||||||
|
<span class="ticker-dot" aria-hidden="true"></span>
|
||||||
|
<span class="ticker-label">LATEST</span>
|
||||||
|
<span class="ticker-text" id="tickerText"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="table-shell">
|
||||||
|
<div class="shell-head">
|
||||||
|
<span class="shell-title">findings.log</span>
|
||||||
|
<span class="shell-meta" id="shellMeta"></span>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<svg class="empty-radar" viewBox="0 0 72 72" aria-hidden="true">
|
||||||
|
<circle cx="36" cy="36" r="30" class="er-ring"/>
|
||||||
|
<circle cx="36" cy="36" r="20" class="er-ring"/>
|
||||||
|
<circle cx="36" cy="36" r="10" class="er-ring"/>
|
||||||
|
<line x1="36" y1="6" x2="36" y2="66" class="er-cross"/>
|
||||||
|
<line x1="6" y1="36" x2="66" y2="36" class="er-cross"/>
|
||||||
|
<path d="M36 36 L36 6 A30 30 0 0 1 57.2 14.8 Z" class="er-sweep"/>
|
||||||
|
<circle cx="36" cy="36" r="2.5" class="er-core"/>
|
||||||
|
</svg>
|
||||||
|
<p class="empty-title" id="emptyTitle">No findings yet</p>
|
||||||
|
<p class="empty-copy" id="emptyCopy">KeyFinder passively scans every page you visit. Browse around — leaked keys, tokens and credentials will surface here.</p>
|
||||||
|
<p class="empty-hint">tip · add custom keywords from the extension popup</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="page-footer">
|
||||||
|
<span>local storage only</span>
|
||||||
|
<span class="footer-sep">·</span>
|
||||||
|
<span>5000-finding ring buffer</span>
|
||||||
|
<span class="footer-sep">·</span>
|
||||||
|
<span>no network egress</span>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="preview-stub-empty.js"></script>
|
||||||
|
<script src="../js/results.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>KeyFinder — Findings Console</title>
|
||||||
|
<link rel="stylesheet" href="../css/results.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-row">
|
||||||
|
<div class="header-left">
|
||||||
|
<span class="radar" aria-hidden="true">
|
||||||
|
<img src="../icons/icon48.png" alt="" class="header-icon">
|
||||||
|
</span>
|
||||||
|
<div class="brand-text">
|
||||||
|
<h1>KEYFINDER<span class="version" id="versionLabel"></span></h1>
|
||||||
|
<p class="header-sub">findings console</p>
|
||||||
|
</div>
|
||||||
|
<span class="live-badge" title="Passive scanning is active"><span class="live-dot"></span>PASSIVE</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<div class="search-wrap">
|
||||||
|
<span class="search-glyph" aria-hidden="true">⚲</span>
|
||||||
|
<input type="text" id="searchBox" placeholder="search findings…" autocomplete="off" spellcheck="false">
|
||||||
|
<kbd class="kbd" title="Press / to focus search">/</kbd>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button id="exportJsonBtn" class="btn btn-secondary" title="Download filtered findings as JSON">↓ JSON</button>
|
||||||
|
<button id="exportCsvBtn" class="btn btn-secondary" title="Download filtered findings as CSV">↓ CSV</button>
|
||||||
|
<button id="clearBtn" class="btn btn-danger" title="Remove all findings">× Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-row header-row-filters">
|
||||||
|
<div class="filter-group">
|
||||||
|
<label class="filter">
|
||||||
|
<span class="filter-label">sev</span>
|
||||||
|
<select id="severityFilter">
|
||||||
|
<option value="all">all</option>
|
||||||
|
<option value="critical">critical</option>
|
||||||
|
<option value="high">high</option>
|
||||||
|
<option value="medium">medium</option>
|
||||||
|
<option value="low">low</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="filter">
|
||||||
|
<span class="filter-label">type</span>
|
||||||
|
<select id="typeFilter">
|
||||||
|
<option value="all">all</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="filter">
|
||||||
|
<span class="filter-label">provider</span>
|
||||||
|
<select id="providerFilter">
|
||||||
|
<option value="all">all</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="meta-line" id="metaLine"></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="stats-bar" id="statsBar"></div>
|
||||||
|
|
||||||
|
<div class="ticker" id="ticker" hidden>
|
||||||
|
<span class="ticker-dot" aria-hidden="true"></span>
|
||||||
|
<span class="ticker-label">LATEST</span>
|
||||||
|
<span class="ticker-text" id="tickerText"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="table-shell titled">
|
||||||
|
<span class="titled-label">findings.log</span>
|
||||||
|
<span class="titled-meta" id="shellMeta"></span>
|
||||||
|
<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>
|
||||||
|
<svg class="empty-radar" viewBox="0 0 72 72" aria-hidden="true">
|
||||||
|
<circle cx="36" cy="36" r="30" class="er-ring"/>
|
||||||
|
<circle cx="36" cy="36" r="20" class="er-ring"/>
|
||||||
|
<circle cx="36" cy="36" r="10" class="er-ring"/>
|
||||||
|
<line x1="36" y1="6" x2="36" y2="66" class="er-cross"/>
|
||||||
|
<line x1="6" y1="36" x2="66" y2="36" class="er-cross"/>
|
||||||
|
<path d="M36 36 L36 6 A30 30 0 0 1 57.2 14.8 Z" class="er-sweep"/>
|
||||||
|
<circle cx="36" cy="36" r="2.5" class="er-core"/>
|
||||||
|
</svg>
|
||||||
|
<p class="empty-title" id="emptyTitle">No findings yet</p>
|
||||||
|
<p class="empty-copy" id="emptyCopy">KeyFinder passively scans every page you visit. Browse around — leaked keys, tokens and credentials will surface here.</p>
|
||||||
|
<p class="empty-hint">tip · add custom keywords from the extension popup</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="page-footer">
|
||||||
|
<span>local storage only</span>
|
||||||
|
<span class="footer-sep">·</span>
|
||||||
|
<span>5000-finding ring buffer</span>
|
||||||
|
<span class="footer-sep">·</span>
|
||||||
|
<span>no network egress</span>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="preview-stub.js"></script>
|
||||||
|
<script src="../js/results.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 260 KiB |
@@ -0,0 +1,149 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>KeyFinder — social preview</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
width: 1280px;
|
||||||
|
height: 640px;
|
||||||
|
background: #05070c;
|
||||||
|
font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
color: #d6dde8;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.stage {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 64px;
|
||||||
|
padding: 0 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* left — brand copy */
|
||||||
|
.copy { flex: none; width: 470px; }
|
||||||
|
.brand { display: flex; align-items: center; gap: 16px; }
|
||||||
|
.brand img { width: 64px; height: 64px; }
|
||||||
|
h1 {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 44px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .18em;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .2em;
|
||||||
|
color: #3ddc97;
|
||||||
|
border: 1px solid rgba(61,220,151,.4);
|
||||||
|
padding: 5px 9px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.badge::before {
|
||||||
|
content: "";
|
||||||
|
width: 5px; height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #3ddc97;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
margin-top: 18px;
|
||||||
|
color: #f5a524;
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: .2em;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
margin-top: 14px;
|
||||||
|
color: #8b96a8;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
.spec {
|
||||||
|
margin-top: 26px;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.spec li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
letter-spacing: .1em;
|
||||||
|
color: #525d70;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.spec li::before { content: "▸"; color: #45d0c4; font-size: 10px; }
|
||||||
|
.spec li b { color: #d6dde8; font-weight: 600; }
|
||||||
|
|
||||||
|
/* right — console frame */
|
||||||
|
.frame {
|
||||||
|
flex: 1;
|
||||||
|
height: 460px;
|
||||||
|
border: 1px solid #2a3650;
|
||||||
|
background: #090d14 url("film/dash.png") no-repeat;
|
||||||
|
background-size: 1400px 875px;
|
||||||
|
background-position: -16px -14px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.frame::before {
|
||||||
|
content: "● FINDINGS.LOG";
|
||||||
|
position: absolute;
|
||||||
|
top: -8px; left: 12px;
|
||||||
|
background: #05070c;
|
||||||
|
padding: 0 7px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .18em;
|
||||||
|
color: #8b96a8;
|
||||||
|
}
|
||||||
|
.frame::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(180deg, transparent 78%, rgba(5,7,12,.92) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* bottom strip */
|
||||||
|
.strip {
|
||||||
|
border-top: 1px solid #1a2233;
|
||||||
|
padding: 12px 72px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: .16em;
|
||||||
|
color: #525d70;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.strip .url { color: #45d0c4; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="stage">
|
||||||
|
<div class="copy">
|
||||||
|
<div class="brand">
|
||||||
|
<img src="../icons/icon128.png" alt="">
|
||||||
|
<h1>KEYFINDER</h1>
|
||||||
|
<span class="badge">v2.2.1</span>
|
||||||
|
</div>
|
||||||
|
<div class="tag">// PASSIVE SECRET DISCOVERY</div>
|
||||||
|
<p class="desc">Browser extension that passively scans every page you visit for leaked API keys, tokens and credentials — across scripts, DOM, storage and network responses.</p>
|
||||||
|
<ul class="spec">
|
||||||
|
<li><b>80+</b> detection patterns · entropy analysis</li>
|
||||||
|
<li><b>10</b> attack surfaces · SPA-aware rescanning</li>
|
||||||
|
<li><b>local-only</b> storage · zero config · no egress</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="frame"></div>
|
||||||
|
</div>
|
||||||
|
<div class="strip">
|
||||||
|
<span>chrome · firefox · manifest v3</span>
|
||||||
|
<span class="url">github.com/momenbasel/keyFinder</span>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>KeyFinder — store shot (popup)</title>
|
||||||
|
<link rel="stylesheet" href="../css/popup.css">
|
||||||
|
<link rel="stylesheet" href="store.css">
|
||||||
|
</head>
|
||||||
|
<body class="store">
|
||||||
|
<div class="stage">
|
||||||
|
<div class="copy">
|
||||||
|
<div class="kicker">KEYFINDER v2.2.1 — PASSIVE SCANNER</div>
|
||||||
|
<h1>Catch leaked keys<br>while you <em>browse</em>.</h1>
|
||||||
|
<p>KeyFinder passively scans every page for exposed API keys, tokens and credentials — across scripts, DOM, storage and network responses.</p>
|
||||||
|
<ul class="spec">
|
||||||
|
<li>80+ detection patterns · entropy analysis</li>
|
||||||
|
<li>10 attack surfaces · SPA-aware rescanning</li>
|
||||||
|
<li>local-only storage · zero config</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="popup-frame">
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-brand">
|
||||||
|
<span class="radar" aria-hidden="true">
|
||||||
|
<img src="../icons/icon48.png" alt="" class="header-icon">
|
||||||
|
</span>
|
||||||
|
<div class="brand-text">
|
||||||
|
<h1>KEYFINDER<span class="version" id="versionLabel"></span></h1>
|
||||||
|
<p class="header-tagline">passive secret discovery</p>
|
||||||
|
</div>
|
||||||
|
<span class="live-badge" title="Passive scanning is active"><span class="live-dot"></span>ARMED</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="stats" id="stats">
|
||||||
|
<div class="stat-card stat-findings">
|
||||||
|
<span class="stat-top"><span class="stat-led" aria-hidden="true"></span><span class="stat-label">Findings</span></span>
|
||||||
|
<span class="stat-number" id="findingCount">–</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card stat-keywords">
|
||||||
|
<span class="stat-top"><span class="stat-led" aria-hidden="true"></span><span class="stat-label">Keywords</span></span>
|
||||||
|
<span class="stat-number" id="keywordCount">–</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2 class="section-title">Watchlist<span class="section-sub">// custom keywords</span></h2>
|
||||||
|
<form id="keywordForm" class="keyword-form">
|
||||||
|
<span class="prompt" aria-hidden="true">›</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="keywordInput"
|
||||||
|
placeholder="add 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">
|
||||||
|
<div class="term-line" aria-live="polite"><span class="term-text" id="termText"></span><span class="term-cursor" aria-hidden="true"></span></div>
|
||||||
|
<a href="../results.html" target="_blank" id="resultsLink" class="results-btn">
|
||||||
|
View Findings <span class="btn-arrow" aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="preview-stub.js"></script>
|
||||||
|
<script src="../js/popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/* Store-listing composition backdrop (preview only, not shipped) */
|
||||||
|
|
||||||
|
body.store {
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
background: #04060a;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 72px;
|
||||||
|
width: 1280px;
|
||||||
|
height: 800px;
|
||||||
|
padding: 0 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ambient depth: a single flat value step, no glow washes */
|
||||||
|
.stage::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(700px 500px at 85% 80%, rgba(69,208,196,.045) 0%, transparent 65%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy { max-width: 560px; }
|
||||||
|
|
||||||
|
.kicker {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .22em;
|
||||||
|
color: var(--green);
|
||||||
|
border: 1px solid rgba(61,220,151,.32);
|
||||||
|
background: rgba(61,220,151,.06);
|
||||||
|
padding: 5px 9px;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-bottom: 26px;
|
||||||
|
}
|
||||||
|
.kicker::before {
|
||||||
|
content: "";
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--green);
|
||||||
|
box-shadow: 0 0 8px var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy h1 {
|
||||||
|
font-size: 46px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.08;
|
||||||
|
letter-spacing: .01em;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.copy h1 em {
|
||||||
|
font-style: normal;
|
||||||
|
color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy p {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-bottom: 26px;
|
||||||
|
max-width: 440px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
.spec li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
color: var(--text-faint);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.spec li::before {
|
||||||
|
content: "▸";
|
||||||
|
color: var(--teal);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* floating popup frame */
|
||||||
|
.popup-frame {
|
||||||
|
position: relative;
|
||||||
|
flex: none;
|
||||||
|
width: 372px;
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 24px 80px rgba(0,0,0,.6);
|
||||||
|
}
|
||||||
|
.popup-frame::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(245,165,36,.6), transparent);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
@@ -3,43 +3,76 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>KeyFinder - Findings</title>
|
<title>KeyFinder — Findings Console</title>
|
||||||
<link rel="stylesheet" href="css/results.css">
|
<link rel="stylesheet" href="css/results.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="header">
|
<header class="header">
|
||||||
|
<div class="header-row">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
|
<span class="radar" aria-hidden="true">
|
||||||
<h1>KeyFinder <span class="version" id="versionLabel"></span></h1>
|
<img src="icons/icon48.png" alt="" class="header-icon">
|
||||||
|
</span>
|
||||||
|
<div class="brand-text">
|
||||||
|
<h1>KEYFINDER<span class="version" id="versionLabel"></span></h1>
|
||||||
|
<p class="header-sub">findings console</p>
|
||||||
|
</div>
|
||||||
|
<span class="live-badge" title="Passive scanning is active"><span class="live-dot"></span>PASSIVE</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="filter-group">
|
<div class="search-wrap">
|
||||||
<select id="severityFilter">
|
<span class="search-glyph" aria-hidden="true">⚲</span>
|
||||||
<option value="all">All Severities</option>
|
<input type="text" id="searchBox" placeholder="search findings…" autocomplete="off" spellcheck="false">
|
||||||
<option value="critical">Critical</option>
|
<kbd class="kbd" title="Press / to focus search">/</kbd>
|
||||||
<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>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button id="exportJsonBtn" class="btn btn-secondary">Export JSON</button>
|
<button id="exportJsonBtn" class="btn btn-secondary" title="Download filtered findings as JSON">↓ JSON</button>
|
||||||
<button id="exportCsvBtn" class="btn btn-secondary">Export CSV</button>
|
<button id="exportCsvBtn" class="btn btn-secondary" title="Download filtered findings as CSV">↓ CSV</button>
|
||||||
<button id="clearBtn" class="btn btn-danger">Clear All</button>
|
<button id="clearBtn" class="btn btn-danger" title="Remove all findings">× Clear</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-row header-row-filters">
|
||||||
|
<div class="filter-group">
|
||||||
|
<label class="filter">
|
||||||
|
<span class="filter-label">sev</span>
|
||||||
|
<select id="severityFilter">
|
||||||
|
<option value="all">all</option>
|
||||||
|
<option value="critical">critical</option>
|
||||||
|
<option value="high">high</option>
|
||||||
|
<option value="medium">medium</option>
|
||||||
|
<option value="low">low</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="filter">
|
||||||
|
<span class="filter-label">type</span>
|
||||||
|
<select id="typeFilter">
|
||||||
|
<option value="all">all</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="filter">
|
||||||
|
<span class="filter-label">provider</span>
|
||||||
|
<select id="providerFilter">
|
||||||
|
<option value="all">all</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="meta-line" id="metaLine"></div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="stats-bar" id="statsBar"></div>
|
<div class="stats-bar" id="statsBar"></div>
|
||||||
|
|
||||||
|
<div class="ticker" id="ticker" hidden>
|
||||||
|
<span class="ticker-dot" aria-hidden="true"></span>
|
||||||
|
<span class="ticker-label">LATEST</span>
|
||||||
|
<span class="ticker-text" id="tickerText"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
|
<div class="table-shell titled">
|
||||||
|
<span class="titled-label">findings.log</span>
|
||||||
|
<span class="titled-meta" id="shellMeta"></span>
|
||||||
<table class="findings-table">
|
<table class="findings-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -58,9 +91,27 @@
|
|||||||
<tbody id="findingsBody"></tbody>
|
<tbody id="findingsBody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
<div id="emptyState" class="empty-state" hidden>
|
<div id="emptyState" class="empty-state" hidden>
|
||||||
<div class="empty-icon">🔍</div>
|
<svg class="empty-radar" viewBox="0 0 72 72" aria-hidden="true">
|
||||||
<p>No findings yet. Browse some pages and KeyFinder will passively scan for leaked secrets.</p>
|
<circle cx="36" cy="36" r="30" class="er-ring"/>
|
||||||
|
<circle cx="36" cy="36" r="20" class="er-ring"/>
|
||||||
|
<circle cx="36" cy="36" r="10" class="er-ring"/>
|
||||||
|
<line x1="36" y1="6" x2="36" y2="66" class="er-cross"/>
|
||||||
|
<line x1="6" y1="36" x2="66" y2="36" class="er-cross"/>
|
||||||
|
<path d="M36 36 L36 6 A30 30 0 0 1 57.2 14.8 Z" class="er-sweep"/>
|
||||||
|
<circle cx="36" cy="36" r="2.5" class="er-core"/>
|
||||||
|
</svg>
|
||||||
|
<p class="empty-title" id="emptyTitle">No findings yet</p>
|
||||||
|
<p class="empty-copy" id="emptyCopy">KeyFinder passively scans every page you visit. Browse around — leaked keys, tokens and credentials will surface here.</p>
|
||||||
|
<p class="empty-hint">tip · add custom keywords from the extension popup</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="page-footer">
|
||||||
|
<span>local storage only</span>
|
||||||
|
<span class="footer-sep">·</span>
|
||||||
|
<span>5000-finding ring buffer</span>
|
||||||
|
<span class="footer-sep">·</span>
|
||||||
|
<span>no network egress</span>
|
||||||
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="js/results.js"></script>
|
<script src="js/results.js"></script>
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# KeyFinder — Store Listing Kit (v2.2.1)
|
||||||
|
|
||||||
|
Copy-paste ready text for the Chrome Web Store and Firefox Add-ons (AMO) dashboards.
|
||||||
|
Screenshots referenced below are in `store/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Chrome Web Store
|
||||||
|
|
||||||
|
**Category:** Developer Tools
|
||||||
|
**Language:** English
|
||||||
|
|
||||||
|
### Short description (max 132 chars — this is 121)
|
||||||
|
|
||||||
|
```
|
||||||
|
Passive scanner for leaked API keys, tokens & secrets. 80+ patterns, 10 attack surfaces, zero config, local-only storage.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detailed description
|
||||||
|
|
||||||
|
```
|
||||||
|
KeyFinder passively discovers API keys, tokens, secrets, and credentials leaked in the pages you visit. It runs silently in the background — no configuration, no accounts, no cloud. Browse normally; KeyFinder watches for secrets that should never have shipped.
|
||||||
|
|
||||||
|
WHAT IT DETECTS
|
||||||
|
80+ detection patterns across the providers that matter:
|
||||||
|
• 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), GitLab, Bitbucket
|
||||||
|
• Payments — Stripe (secret, publishable, restricted, webhook), PayPal Braintree, Square
|
||||||
|
• Communication — Slack, Discord, Telegram, Twilio, SendGrid
|
||||||
|
• AI/ML — OpenAI, Anthropic, HuggingFace, Replicate
|
||||||
|
• Databases — MongoDB, PostgreSQL, MySQL, Redis connection strings
|
||||||
|
• SaaS & infra — Shopify, Sentry, Datadog, Cloudflare, DigitalOcean, HashiCorp Vault, NPM, Docker Hub, and more
|
||||||
|
• Generic — JWTs, Bearer tokens, Basic Auth, credential URLs, plus Shannon entropy analysis for undocumented formats
|
||||||
|
|
||||||
|
HOW IT WORKS
|
||||||
|
Every page is scanned across 10 attack surfaces: script src URLs, inline scripts, same-origin external scripts, meta tags, hidden form fields, data attributes, HTML comments, URL parameters, localStorage/sessionStorage, and intercepted XHR/Fetch responses. A MutationObserver re-scans dynamically injected DOM, so single-page apps are covered too.
|
||||||
|
|
||||||
|
THE FINDINGS CONSOLE
|
||||||
|
A flat, terminal-style dashboard built for triage:
|
||||||
|
• Severity-sorted findings (critical → high → medium → low)
|
||||||
|
• Click a severity tile to filter instantly; filter by type or provider
|
||||||
|
• Latest-finding ticker and live search (/ to focus, Esc to clear)
|
||||||
|
• Click any match to copy the secret; relative timestamps with absolute on hover
|
||||||
|
• One-click export of filtered results as JSON or CSV (formula-injection sanitised)
|
||||||
|
|
||||||
|
PRIVACY
|
||||||
|
All findings stay on your machine. Local storage only, no network egress, no analytics, no accounts. Open source: github.com/momenbasel/keyFinder
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
Intended for security research and authorized testing only. Use it on your own applications or during authorized penetration tests.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Screenshots (upload in this order)
|
||||||
|
|
||||||
|
| # | File | Why |
|
||||||
|
|---|------|-----|
|
||||||
|
| 1 | `store/store-1-dashboard.png` | The findings console fully loaded — the product's money shot |
|
||||||
|
| 2 | `store/store-2-popup.png` | Hero composition: what it is + the popup UI |
|
||||||
|
| 3 | `store/store-3-empty.png` | Zero-noise empty state — reinforces the privacy promise |
|
||||||
|
|
||||||
|
### Privacy tab — single-purpose & permission justifications
|
||||||
|
|
||||||
|
**Single purpose:** Passively scans visited pages for leaked API keys, tokens, and secrets, and displays them locally to the user.
|
||||||
|
|
||||||
|
**`activeTab`:** Required to scan the DOM and resources of the page the user is currently viewing for leaked secrets.
|
||||||
|
|
||||||
|
**`storage`:** Required to persist findings and the user's custom keyword list locally on the device. No data leaves the machine.
|
||||||
|
|
||||||
|
**Host permissions (`<all_urls>` content scripts):** Secret leakage can occur on any page. The extension must scan script URLs, inline code, DOM attributes, storage, and network responses on every visited page to fulfill its single purpose. All processing is local; nothing is transmitted.
|
||||||
|
|
||||||
|
**Remote code:** No remote code is executed. All scanning logic ships inside the extension package.
|
||||||
|
|
||||||
|
**Data collection:** None. No personal, sensitive, or browsing data is collected, transmitted, or shared. Findings are stored locally with a 5000-item cap and can be cleared at any time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Firefox Add-ons (AMO)
|
||||||
|
|
||||||
|
**Categories:** Security & Privacy; Developer Tools
|
||||||
|
**Tags:** security, api keys, secrets, pentest, developer tools, scanner, credentials, privacy
|
||||||
|
|
||||||
|
### Summary (max 250 chars — this is 229)
|
||||||
|
|
||||||
|
```
|
||||||
|
Passively discovers API keys, tokens, and secrets leaked in the pages you visit. 80+ patterns across 10 attack surfaces, entropy analysis, SPA-aware rescanning, terminal-style findings console. Zero config, local-only, no egress.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Description (AMO allows basic HTML)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<p><b>KeyFinder</b> passively discovers API keys, tokens, secrets, and credentials leaked in the pages you visit. No configuration, no accounts, no cloud — browse normally and KeyFinder watches for secrets that should never have shipped.</p>
|
||||||
|
|
||||||
|
<p><b>What it detects — 80+ patterns:</b></p>
|
||||||
|
<ul>
|
||||||
|
<li>Cloud: AWS, Google Cloud, Azure</li>
|
||||||
|
<li>Source control: GitHub, GitLab, Bitbucket</li>
|
||||||
|
<li>Payments: Stripe, PayPal Braintree, Square</li>
|
||||||
|
<li>Communication: Slack, Discord, Telegram, Twilio, SendGrid</li>
|
||||||
|
<li>AI/ML: OpenAI, Anthropic, HuggingFace, Replicate</li>
|
||||||
|
<li>Databases: MongoDB, PostgreSQL, MySQL, Redis</li>
|
||||||
|
<li>SaaS & infrastructure: Shopify, Sentry, Datadog, Cloudflare, DigitalOcean, Vault, NPM, Docker Hub</li>
|
||||||
|
<li>Generic: JWTs, Bearer tokens, Basic Auth, credential URLs, high-entropy strings (Shannon entropy analysis)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p><b>How it works:</b> every page is scanned across 10 attack surfaces — script URLs, inline and external scripts, meta tags, hidden form fields, data attributes, HTML comments, URL parameters, web storage, and intercepted XHR/Fetch responses. A MutationObserver re-scans dynamically injected DOM for SPA coverage.</p>
|
||||||
|
|
||||||
|
<p><b>The findings console:</b> a flat, terminal-style dashboard built for triage — severity-sorted findings, click-to-filter severity tiles, latest-finding ticker, live search (<code>/</code> to focus, <code>Esc</code> to clear), click-to-copy secrets, and one-click JSON/CSV export of filtered results.</p>
|
||||||
|
|
||||||
|
<p><b>Privacy:</b> all findings stay on your machine. Local storage only, no network egress, no analytics. Open source: <a href="https://github.com/momenbasel/keyFinder">github.com/momenbasel/keyFinder</a></p>
|
||||||
|
|
||||||
|
<p><i>Intended for security research and authorized testing only.</i></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Screenshots (same order as Chrome)
|
||||||
|
|
||||||
|
1. `store/store-1-dashboard.png`
|
||||||
|
2. `store/store-2-popup.png`
|
||||||
|
3. `store/store-3-empty.png`
|
||||||
|
|
||||||
|
### Reviewer notes (paste into "Notes for Reviewers" if asked)
|
||||||
|
|
||||||
|
```
|
||||||
|
The extension scans visited pages locally for leaked secrets and stores findings in browser storage only. Nothing is transmitted off the device — the manifest declares data_collection_permissions: none. All scanning logic is contained in the package; no remote code is loaded. Source: https://github.com/momenbasel/keyFinder (tag v2.2.1 matches this upload).
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Shared assets checklist
|
||||||
|
|
||||||
|
- [x] Icon set (16/48/128) — `icons/`
|
||||||
|
- [x] Screenshots 1280×800 — `store/`
|
||||||
|
- [x] Privacy policy — `PRIVACY.md` (host its raw GitHub URL in store dashboards that ask: `https://github.com/momenbasel/keyFinder/blob/master/PRIVACY.md`)
|
||||||
|
- [x] Homepage URL — `https://github.com/momenbasel/keyFinder`
|
||||||
|
- [x] Promotional video — `store/store-demo.mp4` (1280×720, 30 fps, 12.3 s: title card → console pan → popup; upload to YouTube as unlisted and paste the link in both store dashboards)
|
||||||
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 39 KiB |