mirror of
https://github.com/momenbasel/keyFinder.git
synced 2026-07-24 13:40:50 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72f324adae | |||
| 806e0a4a7d | |||
| fdd3be3d99 |
@@ -0,0 +1,10 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
groups:
|
||||||
|
actions-minor:
|
||||||
|
update-types: ["minor", "patch"]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Validate manifest JSON
|
||||||
|
run: |
|
||||||
|
python3 -c "import json,sys; json.load(open('manifest.json'))"
|
||||||
|
python3 -c "import json,sys; json.load(open('manifest.firefox.json'))"
|
||||||
|
|
||||||
|
- name: Manifest versions must match
|
||||||
|
run: |
|
||||||
|
C=$(python3 -c "import json; print(json.load(open('manifest.json'))['version'])")
|
||||||
|
F=$(python3 -c "import json; print(json.load(open('manifest.firefox.json'))['version'])")
|
||||||
|
[ "$C" = "$F" ] || { echo "Chrome=$C Firefox=$F"; exit 1; }
|
||||||
|
|
||||||
|
- name: Build runs cleanly
|
||||||
|
run: bash scripts/build.sh
|
||||||
|
|
||||||
|
- name: Install web-ext
|
||||||
|
run: npm install -g web-ext
|
||||||
|
|
||||||
|
- name: web-ext lint (Firefox)
|
||||||
|
run: web-ext lint --source-dir dist/firefox --self-hosted
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: "Tag to build (e.g. v2.1.0). Leave blank to build current ref."
|
||||||
|
required: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||||
|
|
||||||
|
- name: Read version from manifest
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep '"version"' manifest.json | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/')
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Verify tag matches manifest version
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
run: |
|
||||||
|
TAG="${GITHUB_REF#refs/tags/}"
|
||||||
|
if [ "$TAG" != "${{ steps.meta.outputs.tag }}" ] && [ "$TAG" != "${{ steps.meta.outputs.tag }}-firefox" ]; then
|
||||||
|
echo "Tag $TAG does not match manifest version ${{ steps.meta.outputs.tag }}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build Chrome + Firefox zips
|
||||||
|
run: bash scripts/build.sh
|
||||||
|
|
||||||
|
- name: Compute checksums
|
||||||
|
working-directory: dist
|
||||||
|
run: |
|
||||||
|
shasum -a 256 keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip > keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip.sha256
|
||||||
|
shasum -a 256 keyfinder-v${{ steps.meta.outputs.version }}-firefox.zip > keyfinder-v${{ steps.meta.outputs.version }}-firefox.zip.sha256
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: keyfinder-v${{ steps.meta.outputs.version }}
|
||||||
|
path: |
|
||||||
|
dist/keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip
|
||||||
|
dist/keyfinder-v${{ steps.meta.outputs.version }}-firefox.zip
|
||||||
|
dist/keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip.sha256
|
||||||
|
dist/keyfinder-v${{ steps.meta.outputs.version }}-firefox.zip.sha256
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Attach to GitHub Release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
dist/keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip
|
||||||
|
dist/keyfinder-v${{ steps.meta.outputs.version }}-firefox.zip
|
||||||
|
dist/keyfinder-v${{ steps.meta.outputs.version }}-chrome.zip.sha256
|
||||||
|
dist/keyfinder-v${{ steps.meta.outputs.version }}-firefox.zip.sha256
|
||||||
|
generate_release_notes: true
|
||||||
|
fail_on_unmatched_files: true
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
*.crx
|
*.crx
|
||||||
*.pem
|
*.pem
|
||||||
*.zip
|
*.zip
|
||||||
|
dist/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to KeyFinder are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [SemVer](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.1.1] - 2026-05-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `SECURITY.md` with threat model, disclosure policy, and known limitations of the MAIN <-> ISOLATED nonce bridge
|
||||||
|
- `.github/dependabot.yml` for weekly GitHub Actions version bumps
|
||||||
|
- `CHANGELOG.md`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- CSV export sanitiser now also prefixes cells starting with LF (`\n`), not just `=`, `+`, `-`, `@`, tab, CR
|
||||||
|
- Popup and results page version label is now read from the manifest at runtime instead of being hardcoded
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
## [2.1.0] - 2026-04-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Per-session nonce validation between MAIN-world interceptor and ISOLATED content script to prevent forged finding injection
|
||||||
|
- CSV formula-injection sanitiser on findings export
|
||||||
|
- Serialised storage writes to eliminate cross-tab race conditions
|
||||||
|
- 5000-finding cap with FIFO eviction
|
||||||
|
- Per-tab alert badge with red-dot icon overlay when secrets are detected
|
||||||
|
- MutationObserver scans dynamically-injected DOM nodes for SPA coverage
|
||||||
|
- Explicit Content Security Policy in Chrome and Firefox manifests
|
||||||
|
- `js/interceptor-loader.js` for both browsers, replacing direct MAIN-world content script on Firefox so the nonce handoff actually works
|
||||||
|
- GitHub Actions release pipeline (`.github/workflows/release.yml`): on `v*` tag, build Chrome + Firefox zips, compute SHA256, attach to GitHub Release
|
||||||
|
- GitHub Actions CI pipeline (`.github/workflows/ci.yml`): manifest JSON validation, Chrome <-> Firefox version parity check, build verification, `web-ext lint` on the Firefox bundle
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Keyword input validation: 50 character maximum, 50 keyword maximum
|
||||||
|
- Findings are now deleted by unique ID instead of URL substring match
|
||||||
|
- URL parameter scanner uses exact match instead of substring (was matching `author` as `auth`)
|
||||||
|
- Keyword scanner enforces word boundaries (was matching `key` inside `hotkey`, `monkey`)
|
||||||
|
- camelCase JS identifiers are now skipped in keyword value matches
|
||||||
|
- Sentry DSN downgraded from `high` to `low` severity (public by design)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Stored finding race conditions across concurrent tabs
|
||||||
|
- False positives from GitHub localStorage caches (`ref-selector:*`, `jump_to:*`, `soft-nav:*`, `COPILOT_*`)
|
||||||
|
- False positives from common CSRF tokens (`authenticity_token`, `csrf_token`, `__RequestVerificationToken`)
|
||||||
|
- False positives from keyboard shortcut data attributes (`data-hotkey`, `data-hotkey-scope`)
|
||||||
|
|
||||||
|
## [2.0.0] - 2026-04-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Complete rewrite to Manifest V3
|
||||||
|
- Enterprise-grade secret detection with 80+ regex patterns covering AWS, GCP, Azure, GitHub, GitLab, Stripe, PayPal, Square, Slack, Discord, and more
|
||||||
|
- Firefox support (MV3, Firefox 128+)
|
||||||
|
- Privacy policy
|
||||||
|
- Replaced demo gifs with professional logo
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Manifest V2 background page
|
||||||
|
- Legacy jQuery dependency
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
Email **security reports** privately to the address on the maintainer's GitHub profile.
|
||||||
|
Do **not** open a public issue for unpatched vulnerabilities.
|
||||||
|
|
||||||
|
When reporting, include:
|
||||||
|
- Affected version (`manifest.json` `version` field)
|
||||||
|
- Browser and version
|
||||||
|
- Steps to reproduce
|
||||||
|
- Impact assessment (what an attacker gains)
|
||||||
|
|
||||||
|
A response is targeted within 7 days.
|
||||||
|
|
||||||
|
## Threat model
|
||||||
|
|
||||||
|
KeyFinder runs as a content script in every page the user visits and reports findings to a service worker. It is **client-side, passive, and read-only** with respect to the page.
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
- Privilege escalation from a malicious page into the extension's service worker
|
||||||
|
- Persistent storage poisoning via crafted findings
|
||||||
|
- Cross-tab data leakage through `chrome.storage`
|
||||||
|
- CSV / JSON export injection (formulas, embedded HTML, JS)
|
||||||
|
- Manifest / CSP weaknesses enabling code injection into the extension's own pages
|
||||||
|
- Pattern-rule false positives that consistently leak benign data into findings
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
- A malicious page generating **fake findings** in the user's results view. The MAIN-world interceptor and the ISOLATED content script communicate over `CustomEvent` with a per-page nonce stored as a `data-kf-verify` attribute on `documentElement`. The nonce is removed once the content script consumes it, but a page script that runs between `document_start` and `document_idle` can read the attribute and forge events. Mitigation cost is high (Symbols don't cross realms; postMessage is also page-visible). The impact is limited to **showing the user a finding that isn't real** - no data is exfiltrated, no privileged API is reached. Treat findings on a hostile page as advisory.
|
||||||
|
- Detection accuracy of individual regex rules. False positives and false negatives are expected; report a tuning issue rather than a CVE.
|
||||||
|
- Extension being uninstalled or disabled by the user.
|
||||||
|
|
||||||
|
## What the extension can see
|
||||||
|
|
||||||
|
| Surface | Scope |
|
||||||
|
|---|---|
|
||||||
|
| Page DOM | All pages (`<all_urls>`) at `document_idle` (ISOLATED world) and `document_start` (MAIN world interceptor) |
|
||||||
|
| Network | `fetch` and `XMLHttpRequest` responses initiated by the page (response bodies up to 500 KB are scanned) |
|
||||||
|
| Web storage | `localStorage`, `sessionStorage`, `document.cookie` on the page |
|
||||||
|
| Inter-extension | None. No host permissions beyond `activeTab` and `storage` |
|
||||||
|
|
||||||
|
The extension makes **no outbound network requests** other than fetching same-origin scripts already referenced by the page.
|
||||||
|
|
||||||
|
## Known limitations
|
||||||
|
|
||||||
|
- **Per-tab badge dot** is set on the first finding only; subsequent findings update the count but not the icon
|
||||||
|
- **5000 finding cap** with FIFO eviction. High-volume scans (heavy SPAs over a long session) will drop oldest findings
|
||||||
|
- **CSV export** prefixes a single-quote on cells starting with `=`, `+`, `-`, `@`, tab, or carriage return to neutralise Excel / Sheets formula injection. Line-feed prefix is not currently neutralised
|
||||||
|
- **Service worker restarts** drop the in-flight `storageQueue` Promise chain. Subsequent storage writes are still serialised; only pending writes from the killed worker are lost
|
||||||
|
|
||||||
|
## Supported versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
|---|---|
|
||||||
|
| 2.1.x | yes |
|
||||||
|
| 2.0.x | no |
|
||||||
|
| < 2.0 | no |
|
||||||
+91
-7
@@ -1,5 +1,8 @@
|
|||||||
const KEYWORDS_KEY = "kf_keywords";
|
const KEYWORDS_KEY = "kf_keywords";
|
||||||
const FINDINGS_KEY = "kf_findings";
|
const FINDINGS_KEY = "kf_findings";
|
||||||
|
const MAX_FINDINGS = 5000;
|
||||||
|
const MAX_KEYWORDS = 50;
|
||||||
|
const MAX_KEYWORD_LENGTH = 50;
|
||||||
|
|
||||||
const DEFAULT_KEYWORDS = [
|
const DEFAULT_KEYWORDS = [
|
||||||
"key", "api_key", "apikey", "api-key", "secret", "token",
|
"key", "api_key", "apikey", "api-key", "secret", "token",
|
||||||
@@ -7,6 +10,76 @@ const DEFAULT_KEYWORDS = [
|
|||||||
"client_id", "client_secret"
|
"client_id", "client_secret"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Serialize all storage writes to prevent race conditions
|
||||||
|
let storageQueue = Promise.resolve();
|
||||||
|
function enqueue(fn) {
|
||||||
|
storageQueue = storageQueue.then(fn, fn);
|
||||||
|
return storageQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Per-tab alert icon ---
|
||||||
|
const alertTabs = new Set();
|
||||||
|
let alertIconCache = null;
|
||||||
|
|
||||||
|
async function buildAlertIcons() {
|
||||||
|
if (alertIconCache) return alertIconCache;
|
||||||
|
const sizes = [16, 48];
|
||||||
|
const imageData = {};
|
||||||
|
for (const size of sizes) {
|
||||||
|
const resp = await fetch(chrome.runtime.getURL(`icons/icon${size}.png`));
|
||||||
|
const blob = await resp.blob();
|
||||||
|
const bitmap = await createImageBitmap(blob);
|
||||||
|
const canvas = new OffscreenCanvas(size, size);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
ctx.drawImage(bitmap, 0, 0, size, size);
|
||||||
|
// Red alert dot in top-right
|
||||||
|
const r = Math.max(3, Math.round(size * 0.22));
|
||||||
|
const cx = size - r - 1;
|
||||||
|
const cy = r + 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = "#ff4444";
|
||||||
|
ctx.fill();
|
||||||
|
ctx.lineWidth = size >= 48 ? 2 : 1;
|
||||||
|
ctx.strokeStyle = "#0f0f0f";
|
||||||
|
ctx.stroke();
|
||||||
|
imageData[size] = ctx.getImageData(0, 0, size, size);
|
||||||
|
}
|
||||||
|
alertIconCache = imageData;
|
||||||
|
return imageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setAlertIcon(tabId) {
|
||||||
|
if (alertTabs.has(tabId)) return;
|
||||||
|
alertTabs.add(tabId);
|
||||||
|
try {
|
||||||
|
const imageData = await buildAlertIcons();
|
||||||
|
await chrome.action.setIcon({ tabId, imageData });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTabIcon(tabId) {
|
||||||
|
if (!alertTabs.delete(tabId)) return;
|
||||||
|
try {
|
||||||
|
chrome.action.setIcon({
|
||||||
|
tabId,
|
||||||
|
path: { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" }
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset icon when a tab navigates to a new page
|
||||||
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
|
||||||
|
if (changeInfo.status === "loading") {
|
||||||
|
resetTabIcon(tabId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up when a tab is closed
|
||||||
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||||
|
alertTabs.delete(tabId);
|
||||||
|
});
|
||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(async (details) => {
|
chrome.runtime.onInstalled.addListener(async (details) => {
|
||||||
if (details.reason === "install") {
|
if (details.reason === "install") {
|
||||||
await chrome.storage.local.set({
|
await chrome.storage.local.set({
|
||||||
@@ -18,7 +91,8 @@ chrome.runtime.onInstalled.addListener(async (details) => {
|
|||||||
|
|
||||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
if (request.type === "finding") {
|
if (request.type === "finding") {
|
||||||
saveFinding(request.data).then(() => sendResponse({ ok: true }));
|
if (sender.tab?.id) setAlertIcon(sender.tab.id);
|
||||||
|
enqueue(() => saveFinding(request.data)).then(() => sendResponse({ ok: true }));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (request.type === "getKeywords") {
|
if (request.type === "getKeywords") {
|
||||||
@@ -30,19 +104,19 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (request.type === "addKeyword") {
|
if (request.type === "addKeyword") {
|
||||||
addKeyword(request.keyword).then((result) => sendResponse(result));
|
enqueue(() => addKeyword(request.keyword)).then((result) => sendResponse(result));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (request.type === "removeKeyword") {
|
if (request.type === "removeKeyword") {
|
||||||
removeKeyword(request.keyword).then(() => sendResponse({ ok: true }));
|
enqueue(() => removeKeyword(request.keyword)).then(() => sendResponse({ ok: true }));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (request.type === "removeFinding") {
|
if (request.type === "removeFinding") {
|
||||||
removeFinding(request.url).then(() => sendResponse({ ok: true }));
|
enqueue(() => removeFinding(request.findingId)).then(() => sendResponse({ ok: true }));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (request.type === "clearFindings") {
|
if (request.type === "clearFindings") {
|
||||||
clearFindings().then(() => sendResponse({ ok: true }));
|
enqueue(() => clearFindings()).then(() => sendResponse({ ok: true }));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (request.type === "exportFindings") {
|
if (request.type === "exportFindings") {
|
||||||
@@ -60,6 +134,8 @@ async function addKeyword(keyword) {
|
|||||||
const keywords = await getKeywords();
|
const keywords = await getKeywords();
|
||||||
const normalized = keyword.trim().toLowerCase();
|
const normalized = keyword.trim().toLowerCase();
|
||||||
if (!normalized) return { ok: false, error: "Keyword cannot be empty." };
|
if (!normalized) return { ok: false, error: "Keyword cannot be empty." };
|
||||||
|
if (normalized.length > MAX_KEYWORD_LENGTH) return { ok: false, error: `Keyword must be ${MAX_KEYWORD_LENGTH} characters or fewer.` };
|
||||||
|
if (keywords.length >= MAX_KEYWORDS) return { ok: false, error: `Maximum of ${MAX_KEYWORDS} keywords allowed.` };
|
||||||
if (keywords.includes(normalized)) return { ok: false, error: "Keyword already exists." };
|
if (keywords.includes(normalized)) return { ok: false, error: "Keyword already exists." };
|
||||||
keywords.push(normalized);
|
keywords.push(normalized);
|
||||||
await chrome.storage.local.set({ [KEYWORDS_KEY]: keywords });
|
await chrome.storage.local.set({ [KEYWORDS_KEY]: keywords });
|
||||||
@@ -82,7 +158,15 @@ async function saveFinding(finding) {
|
|||||||
(f) => f.url === finding.url && f.match === finding.match
|
(f) => f.url === finding.url && f.match === finding.match
|
||||||
);
|
);
|
||||||
if (isDuplicate) return;
|
if (isDuplicate) return;
|
||||||
|
|
||||||
|
finding.id = crypto.randomUUID();
|
||||||
findings.push(finding);
|
findings.push(finding);
|
||||||
|
|
||||||
|
// Evict oldest findings when cap is exceeded
|
||||||
|
if (findings.length > MAX_FINDINGS) {
|
||||||
|
findings.splice(0, findings.length - MAX_FINDINGS);
|
||||||
|
}
|
||||||
|
|
||||||
await chrome.storage.local.set({ [FINDINGS_KEY]: findings });
|
await chrome.storage.local.set({ [FINDINGS_KEY]: findings });
|
||||||
|
|
||||||
const badgeCount = findings.length;
|
const badgeCount = findings.length;
|
||||||
@@ -90,9 +174,9 @@ async function saveFinding(finding) {
|
|||||||
chrome.action.setBadgeBackgroundColor({ color: "#e74c3c" });
|
chrome.action.setBadgeBackgroundColor({ color: "#e74c3c" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeFinding(url) {
|
async function removeFinding(findingId) {
|
||||||
const findings = await getFindings();
|
const findings = await getFindings();
|
||||||
const updated = findings.filter((f) => f.url !== url);
|
const updated = findings.filter((f) => f.id !== findingId);
|
||||||
await chrome.storage.local.set({ [FINDINGS_KEY]: updated });
|
await chrome.storage.local.set({ [FINDINGS_KEY]: updated });
|
||||||
chrome.action.setBadgeText({ text: updated.length > 0 ? String(updated.length) : "" });
|
chrome.action.setBadgeText({ text: updated.length > 0 ? String(updated.length) : "" });
|
||||||
}
|
}
|
||||||
|
|||||||
+138
-5
@@ -87,14 +87,17 @@
|
|||||||
|
|
||||||
for (const kw of keywords) {
|
for (const kw of keywords) {
|
||||||
const escaped = kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const escaped = kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
// Require word boundary around keyword to avoid matching "hotkey", "monkey", "turkey" for "key"
|
||||||
const kwRegex = new RegExp(
|
const kwRegex = new RegExp(
|
||||||
`(?:${escaped})\\s*[:=]\\s*['"\`]([^'"\`\\n]{8,200})['"\`]`,
|
`(?:^|[^a-zA-Z])(?:${escaped})(?:[^a-zA-Z]|$)\\s*[:=]\\s*['"\`]([^'"\`\\n]{8,200})['"\`]`,
|
||||||
"gi"
|
"gi"
|
||||||
);
|
);
|
||||||
let m;
|
let m;
|
||||||
while ((m = kwRegex.exec(text)) !== null) {
|
while ((m = kwRegex.exec(text)) !== null) {
|
||||||
const val = m[1];
|
const val = m[1];
|
||||||
if (isFalsePositive(val)) continue;
|
if (isFalsePositive(val)) continue;
|
||||||
|
// Skip values that look like JS function/method names (camelCase identifiers)
|
||||||
|
if (/^[a-z][a-zA-Z0-9_]*$/.test(val) && val.length < 60) continue;
|
||||||
report({
|
report({
|
||||||
url: sourceUrl,
|
url: sourceUrl,
|
||||||
match: val.substring(0, 200),
|
match: val.substring(0, 200),
|
||||||
@@ -185,7 +188,16 @@
|
|||||||
const name = (input.name || input.id || "").toLowerCase();
|
const name = (input.name || input.id || "").toLowerCase();
|
||||||
const value = input.value;
|
const value = input.value;
|
||||||
if (!value || value.length < 8) continue;
|
if (!value || value.length < 8) continue;
|
||||||
const sensitive = ["token", "csrf", "api_key", "apikey", "secret", "auth", "session", "nonce", "key", "access_token"];
|
// Skip known framework CSRF tokens — these are ephemeral anti-CSRF nonces, not secrets
|
||||||
|
const csrfNames = ["authenticity_token", "csrf_token", "csrf", "_csrf", "__requestverificationtoken",
|
||||||
|
"csrfmiddlewaretoken", "react-codespace-csrf", "_token", "xsrf-token", "anticsrf"];
|
||||||
|
if (csrfNames.some((c) => name === c || name.startsWith(c))) continue;
|
||||||
|
// Skip common non-secret hidden fields
|
||||||
|
const benignNames = ["return_to", "redirect", "redirect_uri", "next", "ref", "referer",
|
||||||
|
"utm_source", "utm_medium", "utm_campaign", "notice_name", "host", "method",
|
||||||
|
"pinned_items_id_and_type[]", "repo_topics[]", "timestamp_secret"];
|
||||||
|
if (benignNames.some((b) => name === b || name.startsWith(b))) continue;
|
||||||
|
const sensitive = ["api_key", "apikey", "secret_key", "access_token", "private_key", "password"];
|
||||||
if (sensitive.some((s) => name.includes(s)) || isHighEntropy(value)) {
|
if (sensitive.some((s) => name.includes(s)) || isHighEntropy(value)) {
|
||||||
report({
|
report({
|
||||||
url: pageUrl, match: `${name}=${value.substring(0, 100)}`,
|
url: pageUrl, match: `${name}=${value.substring(0, 100)}`,
|
||||||
@@ -200,10 +212,20 @@
|
|||||||
|
|
||||||
function scanDataAttributes() {
|
function scanDataAttributes() {
|
||||||
const all = document.querySelectorAll("*");
|
const all = document.querySelectorAll("*");
|
||||||
|
// Attribute names that contain "key" but are not secrets
|
||||||
|
const ignoredAttrs = [
|
||||||
|
"data-hotkey", "data-hotkey-scope", "data-hotkey-within", // Keyboard shortcuts
|
||||||
|
"data-provider-key", // UI provider identifiers
|
||||||
|
"data-pjax-key", "data-turbo-key", // Framework routing keys
|
||||||
|
];
|
||||||
for (const el of all) {
|
for (const el of all) {
|
||||||
for (const attr of el.attributes) {
|
for (const attr of el.attributes) {
|
||||||
if (!/^data-.*(?:key|token|secret|auth|api|credential|password)/i.test(attr.name)) continue;
|
if (!/^data-.*(?:key|token|secret|auth|api|credential|password)/i.test(attr.name)) continue;
|
||||||
if (!attr.value || attr.value.length < 8) continue;
|
if (!attr.value || attr.value.length < 8) continue;
|
||||||
|
// Skip known non-secret data attributes
|
||||||
|
if (ignoredAttrs.includes(attr.name)) continue;
|
||||||
|
// Skip if the value looks like a keyboard shortcut (contains Mod+, Shift+, etc.)
|
||||||
|
if (/(?:Mod|Shift|Alt|Ctrl|Meta)\+/i.test(attr.value)) continue;
|
||||||
report({
|
report({
|
||||||
url: pageUrl, match: `${attr.name}="${attr.value.substring(0, 100)}"`,
|
url: pageUrl, match: `${attr.name}="${attr.value.substring(0, 100)}"`,
|
||||||
type: "data-attribute", patternName: "Sensitive Data Attribute",
|
type: "data-attribute", patternName: "Sensitive Data Attribute",
|
||||||
@@ -226,6 +248,12 @@
|
|||||||
|
|
||||||
function scanLinkHrefs() {
|
function scanLinkHrefs() {
|
||||||
const links = document.querySelectorAll("a[href], link[href]");
|
const links = document.querySelectorAll("a[href], link[href]");
|
||||||
|
// URL param names that look sensitive but aren't
|
||||||
|
const benignParams = ["author", "assignee", "reviewer", "creator", "user", "username",
|
||||||
|
"sort", "order", "page", "per_page", "tab", "type", "language", "q", "query",
|
||||||
|
"ref", "branch", "path", "since", "until", "direction", "state", "label",
|
||||||
|
"source", "plan", "return_to", "redirect", "onload", "render", "style",
|
||||||
|
"method", "host", "fromHostedPage", "countryBlackList"];
|
||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
try {
|
try {
|
||||||
const href = link.href;
|
const href = link.href;
|
||||||
@@ -233,8 +261,10 @@
|
|||||||
const url = new URL(href);
|
const url = new URL(href);
|
||||||
for (const [param, value] of url.searchParams) {
|
for (const [param, value] of url.searchParams) {
|
||||||
const p = param.toLowerCase();
|
const p = param.toLowerCase();
|
||||||
const sensitive = ["key", "api_key", "apikey", "token", "secret", "access_token", "auth", "password", "session_id"];
|
if (benignParams.includes(p)) continue;
|
||||||
if (sensitive.some((s) => p.includes(s)) && value.length >= 8) {
|
const sensitive = ["api_key", "apikey", "token", "secret", "access_token", "password", "session_id", "private_key"];
|
||||||
|
// Require exact match on the param name, not substring — "author" was matching "auth"
|
||||||
|
if (sensitive.some((s) => p === s || p.endsWith(`_${s}`) || p.startsWith(`${s}_`)) && value.length >= 8) {
|
||||||
report({
|
report({
|
||||||
url: href, match: `${param}=${value.substring(0, 100)}`,
|
url: href, match: `${param}=${value.substring(0, 100)}`,
|
||||||
type: "url-param", patternName: "Sensitive URL Parameter",
|
type: "url-param", patternName: "Sensitive URL Parameter",
|
||||||
@@ -251,6 +281,28 @@
|
|||||||
{ store: localStorage, label: "localStorage" },
|
{ store: localStorage, label: "localStorage" },
|
||||||
{ store: sessionStorage, label: "sessionStorage" },
|
{ store: sessionStorage, label: "sessionStorage" },
|
||||||
];
|
];
|
||||||
|
// Keys that are known non-sensitive framework/platform storage — never flag these
|
||||||
|
const ignoredKeyPrefixes = [
|
||||||
|
"ref-selector:", // GitHub branch selector cache
|
||||||
|
"jump_to:", // GitHub navigation cache
|
||||||
|
"soft-nav:", // GitHub SPA navigation state
|
||||||
|
"react-router-scroll", // React Router scroll positions
|
||||||
|
"COPILOT_SELECTED_MODEL", // GitHub Copilot UI preference
|
||||||
|
"rc::", // reCAPTCHA state
|
||||||
|
"debug:", // Debug flags
|
||||||
|
"ajs_", // Analytics.js state
|
||||||
|
"_ga", // Google Analytics
|
||||||
|
"intercom", // Intercom chat widget
|
||||||
|
"amplitude_", // Amplitude analytics
|
||||||
|
"mp_", // Mixpanel
|
||||||
|
"optimizely", // Optimizely experiments
|
||||||
|
];
|
||||||
|
// Specific exact keys that look sensitive but aren't
|
||||||
|
const ignoredExactKeys = [
|
||||||
|
"COPILOT_AUTH_TOKEN", // GitHub Copilot ephemeral session (browser-local, not extractable)
|
||||||
|
"COPILOT_AUTH_TOKEN:expiry",
|
||||||
|
"id", // Generic session IDs in iframes (e.g., Stripe m.stripe.network)
|
||||||
|
];
|
||||||
for (const { store, label } of stores) {
|
for (const { store, label } of stores) {
|
||||||
try {
|
try {
|
||||||
for (let i = 0; i < store.length; i++) {
|
for (let i = 0; i < store.length; i++) {
|
||||||
@@ -258,7 +310,14 @@
|
|||||||
const value = store.getItem(key);
|
const value = store.getItem(key);
|
||||||
if (!value || value.length < 12) continue;
|
if (!value || value.length < 12) continue;
|
||||||
const kl = key.toLowerCase();
|
const kl = key.toLowerCase();
|
||||||
const sensitive = ["token", "key", "secret", "auth", "session", "credential", "password", "jwt", "bearer"];
|
// Skip known benign keys
|
||||||
|
if (ignoredKeyPrefixes.some((p) => key.startsWith(p))) continue;
|
||||||
|
if (ignoredExactKeys.includes(key)) continue;
|
||||||
|
// Skip keys whose values are clearly JSON branch/ref data (GitHub caches)
|
||||||
|
if (value.startsWith('{"refs":') || value.startsWith('{"billing":')) continue;
|
||||||
|
const sensitive = ["token", "secret", "auth", "credential", "password", "jwt", "bearer", "private_key"];
|
||||||
|
// Require a stronger match — "key" alone is too broad (matches "hotkey", "monkey", etc.)
|
||||||
|
// Remove "key" and "session" from sensitive list to reduce noise
|
||||||
if (sensitive.some((s) => kl.includes(s)) || isHighEntropy(value.substring(0, 100))) {
|
if (sensitive.some((s) => kl.includes(s)) || isHighEntropy(value.substring(0, 100))) {
|
||||||
report({
|
report({
|
||||||
url: pageUrl, match: `${label}.${key}=${value.substring(0, 120)}`,
|
url: pageUrl, match: `${label}.${key}=${value.substring(0, 120)}`,
|
||||||
@@ -294,9 +353,13 @@
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kfNonce = document.documentElement.getAttribute("data-kf-verify") || "";
|
||||||
|
document.documentElement.removeAttribute("data-kf-verify");
|
||||||
|
|
||||||
window.addEventListener("__kf_finding__", (e) => {
|
window.addEventListener("__kf_finding__", (e) => {
|
||||||
const data = e.detail;
|
const data = e.detail;
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
if (data.__kfNonce !== kfNonce) return;
|
||||||
|
|
||||||
if (data.rawText) {
|
if (data.rawText) {
|
||||||
scanText(data.rawText, data.sourceUrl || pageUrl, data.type);
|
scanText(data.rawText, data.sourceUrl || pageUrl, data.type);
|
||||||
@@ -327,6 +390,76 @@
|
|||||||
scanCookies();
|
scanCookies();
|
||||||
await scanExternalScripts();
|
await scanExternalScripts();
|
||||||
|
|
||||||
|
// Observe DOM mutations for SPA support
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
for (const node of mutation.addedNodes) {
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
||||||
|
if (node.tagName === "SCRIPT") {
|
||||||
|
if (node.src) {
|
||||||
|
// New external script added
|
||||||
|
for (const kw of keywords) {
|
||||||
|
if (node.src.toLowerCase().includes(kw)) {
|
||||||
|
report({
|
||||||
|
url: node.src, match: node.src, type: "script-src",
|
||||||
|
patternName: `Script URL contains: ${kw}`,
|
||||||
|
severity: "medium", confidence: "medium", provider: "URL Scan",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (node.textContent) {
|
||||||
|
scanText(node.textContent, pageUrl, "inline-script");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Scan any new hidden inputs
|
||||||
|
const hiddenInputs = node.matches && node.matches('input[type="hidden"]')
|
||||||
|
? [node]
|
||||||
|
: (node.querySelectorAll ? Array.from(node.querySelectorAll('input[type="hidden"]')) : []);
|
||||||
|
for (const input of hiddenInputs) {
|
||||||
|
const name = (input.name || input.id || "").toLowerCase();
|
||||||
|
const value = input.value;
|
||||||
|
if (!value || value.length < 8) continue;
|
||||||
|
// Skip known CSRF tokens
|
||||||
|
const csrfNames = ["authenticity_token", "csrf_token", "csrf", "_csrf", "__requestverificationtoken",
|
||||||
|
"csrfmiddlewaretoken", "react-codespace-csrf", "_token", "xsrf-token", "anticsrf"];
|
||||||
|
if (csrfNames.some((c) => name === c || name.startsWith(c))) continue;
|
||||||
|
const benignNames = ["return_to", "redirect", "redirect_uri", "next", "ref",
|
||||||
|
"notice_name", "host", "method", "pinned_items_id_and_type[]", "repo_topics[]", "timestamp_secret"];
|
||||||
|
if (benignNames.some((b) => name === b || name.startsWith(b))) continue;
|
||||||
|
const sensitive = ["api_key", "apikey", "secret_key", "access_token", "private_key", "password"];
|
||||||
|
if (sensitive.some((s) => name.includes(s)) || isHighEntropy(value)) {
|
||||||
|
report({
|
||||||
|
url: pageUrl, match: `${name}=${value.substring(0, 100)}`,
|
||||||
|
type: "hidden-input", patternName: "Hidden Form Field",
|
||||||
|
severity: isHighEntropy(value) ? "high" : "medium",
|
||||||
|
confidence: sensitive.some((s) => name.includes(s)) ? "high" : "medium",
|
||||||
|
provider: "DOM Scan",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Scan data attributes on new elements
|
||||||
|
const elementsToCheck = node.querySelectorAll ? [node, ...node.querySelectorAll("*")] : [node];
|
||||||
|
const ignoredAttrsMut = ["data-hotkey", "data-hotkey-scope", "data-hotkey-within", "data-provider-key", "data-pjax-key", "data-turbo-key"];
|
||||||
|
for (const el of elementsToCheck) {
|
||||||
|
if (!el.attributes) continue;
|
||||||
|
for (const attr of el.attributes) {
|
||||||
|
if (!/^data-.*(?:key|token|secret|auth|api|credential|password)/i.test(attr.name)) continue;
|
||||||
|
if (!attr.value || attr.value.length < 8) continue;
|
||||||
|
if (ignoredAttrsMut.includes(attr.name)) continue;
|
||||||
|
if (/(?:Mod|Shift|Alt|Ctrl|Meta)\+/i.test(attr.value)) continue;
|
||||||
|
report({
|
||||||
|
url: pageUrl, match: `${attr.name}="${attr.value.substring(0, 100)}"`,
|
||||||
|
type: "data-attribute", patternName: "Sensitive Data Attribute",
|
||||||
|
severity: "medium", confidence: isHighEntropy(attr.value) ? "high" : "medium",
|
||||||
|
provider: "DOM Scan",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
|
||||||
|
|
||||||
if (seen.size > 0) {
|
if (seen.size > 0) {
|
||||||
console.log(`[KeyFinder] ${seen.size} potential secret(s) found on ${pageDomain}`);
|
console.log(`[KeyFinder] ${seen.size} potential secret(s) found on ${pageDomain}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const nonce = crypto.randomUUID();
|
||||||
|
|
||||||
|
// Store nonce where both MAIN world (interceptor) and ISOLATED world (content.js) can read it.
|
||||||
|
// The interceptor removes data-kf-nonce after reading; data-kf-verify stays for content.js.
|
||||||
|
const el = document.documentElement;
|
||||||
|
el.setAttribute("data-kf-nonce", nonce);
|
||||||
|
el.setAttribute("data-kf-verify", nonce);
|
||||||
|
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = chrome.runtime.getURL("js/interceptor.js");
|
||||||
|
(document.head || document.documentElement).appendChild(script);
|
||||||
|
script.onload = () => script.remove();
|
||||||
|
})();
|
||||||
+36
-17
@@ -2,8 +2,11 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const EVENT_NAME = "__kf_finding__";
|
const EVENT_NAME = "__kf_finding__";
|
||||||
|
const nonce = document.documentElement.getAttribute("data-kf-nonce") || "";
|
||||||
|
document.documentElement.removeAttribute("data-kf-nonce");
|
||||||
|
|
||||||
function emit(data) {
|
function emit(data) {
|
||||||
|
data.__kfNonce = nonce;
|
||||||
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: data }));
|
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: data }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,23 +39,39 @@
|
|||||||
"__ENV__", "__CONFIG__", "ENV", "CONFIG",
|
"__ENV__", "__CONFIG__", "ENV", "CONFIG",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const name of globalNames) {
|
const scannedGlobals = new Set();
|
||||||
try {
|
function scanGlobals() {
|
||||||
const val = window[name];
|
for (const name of globalNames) {
|
||||||
if (val === undefined || val === null) continue;
|
if (scannedGlobals.has(name)) continue;
|
||||||
const str = typeof val === "object" ? JSON.stringify(val) : String(val);
|
try {
|
||||||
if (str.length < 8 || str === "[object Object]") continue;
|
const val = window[name];
|
||||||
emit({
|
if (val === undefined || val === null) continue;
|
||||||
match: `window.${name}=${str.substring(0, 200)}`,
|
const str = typeof val === "object" ? JSON.stringify(val) : String(val);
|
||||||
type: "window-global",
|
if (str.length < 8 || str === "[object Object]") continue;
|
||||||
patternName: "Exposed Global Variable",
|
scannedGlobals.add(name);
|
||||||
severity: "high",
|
emit({
|
||||||
confidence: typeof val !== "object" && isHighEntropy(str.substring(0, 60)) ? "high" : "medium",
|
match: `window.${name}=${str.substring(0, 200)}`,
|
||||||
provider: "JS Global Scan",
|
type: "window-global",
|
||||||
isObject: typeof val === "object",
|
patternName: "Exposed Global Variable",
|
||||||
rawText: typeof val === "object" ? str.substring(0, 5000) : null,
|
severity: "high",
|
||||||
});
|
confidence: typeof val !== "object" && isHighEntropy(str.substring(0, 60)) ? "high" : "medium",
|
||||||
} catch {}
|
provider: "JS Global Scan",
|
||||||
|
isObject: typeof val === "object",
|
||||||
|
rawText: typeof val === "object" ? str.substring(0, 5000) : null,
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Run at document_start (likely empty), DOMContentLoaded, and load to catch
|
||||||
|
// globals set at any phase. Each name reports at most once via scannedGlobals.
|
||||||
|
scanGlobals();
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", scanGlobals, { once: true });
|
||||||
|
}
|
||||||
|
if (document.readyState !== "complete") {
|
||||||
|
window.addEventListener("load", scanGlobals, { once: true });
|
||||||
|
} else {
|
||||||
|
scanGlobals();
|
||||||
}
|
}
|
||||||
|
|
||||||
const origXhrOpen = XMLHttpRequest.prototype.open;
|
const origXhrOpen = XMLHttpRequest.prototype.open;
|
||||||
|
|||||||
+1
-1
@@ -76,7 +76,7 @@ const SECRET_PATTERNS = [
|
|||||||
{ name: "Shopify Private App Token", re: /\bshppa_[a-fA-F0-9]{32}\b/g, severity: "critical", confidence: "high", provider: "Shopify" },
|
{ name: "Shopify Private App Token", re: /\bshppa_[a-fA-F0-9]{32}\b/g, severity: "critical", confidence: "high", provider: "Shopify" },
|
||||||
{ name: "Shopify Shared Secret", re: /\bshpss_[a-fA-F0-9]{32}\b/g, severity: "critical", confidence: "high", provider: "Shopify" },
|
{ name: "Shopify Shared Secret", re: /\bshpss_[a-fA-F0-9]{32}\b/g, severity: "critical", confidence: "high", provider: "Shopify" },
|
||||||
|
|
||||||
{ name: "Sentry DSN", re: /https:\/\/[0-9a-f]{32}@(?:o[0-9]+\.)?(?:sentry\.io|[a-z0-9.-]+)\/[0-9]+/g, severity: "medium", confidence: "high", provider: "Sentry" },
|
{ name: "Sentry DSN", re: /https:\/\/[0-9a-f]{32}@(?:o[0-9]+\.)?(?:sentry\.io|[a-z0-9.-]+)\/[0-9]+/g, severity: "low", confidence: "high", provider: "Sentry" },
|
||||||
{ name: "Sentry Auth Token", re: /\bsntrys_[A-Za-z0-9_]{64,}\b/g, severity: "high", confidence: "high", provider: "Sentry" },
|
{ name: "Sentry Auth Token", re: /\bsntrys_[A-Za-z0-9_]{64,}\b/g, severity: "high", confidence: "high", provider: "Sentry" },
|
||||||
|
|
||||||
{ name: "New Relic API Key", re: /\bNRAK-[A-Z0-9]{27}\b/g, severity: "high", confidence: "high", provider: "New Relic" },
|
{ name: "New Relic API Key", re: /\bNRAK-[A-Z0-9]{27}\b/g, severity: "high", confidence: "high", provider: "New Relic" },
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
document.addEventListener("DOMContentLoaded", init);
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
const versionLabel = document.getElementById("versionLabel");
|
||||||
|
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
|
||||||
await renderKeywords();
|
await renderKeywords();
|
||||||
await renderStats();
|
await renderStats();
|
||||||
document.getElementById("keywordForm").addEventListener("submit", handleAddKeyword);
|
document.getElementById("keywordForm").addEventListener("submit", handleAddKeyword);
|
||||||
|
|||||||
+20
-11
@@ -3,6 +3,8 @@ let allFindings = [];
|
|||||||
document.addEventListener("DOMContentLoaded", init);
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
const versionLabel = document.getElementById("versionLabel");
|
||||||
|
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
|
||||||
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
|
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
|
||||||
allFindings = response.findings || [];
|
allFindings = response.findings || [];
|
||||||
|
|
||||||
@@ -173,8 +175,8 @@ function renderFindings() {
|
|||||||
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", url: f.url });
|
await chrome.runtime.sendMessage({ type: "removeFinding", findingId: f.id });
|
||||||
allFindings = allFindings.filter((x) => x !== f);
|
allFindings = allFindings.filter((x) => x.id !== f.id);
|
||||||
renderStats();
|
renderStats();
|
||||||
renderFindings();
|
renderFindings();
|
||||||
});
|
});
|
||||||
@@ -212,19 +214,26 @@ function exportJson() {
|
|||||||
downloadBlob(blob, `keyfinder-findings-${Date.now()}.json`);
|
downloadBlob(blob, `keyfinder-findings-${Date.now()}.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function csvSafe(value) {
|
||||||
|
let str = String(value || "");
|
||||||
|
if (/^[=+\-@\t\r\n]/.test(str)) str = "'" + str;
|
||||||
|
str = str.replace(/"/g, '""');
|
||||||
|
return `"${str}"`;
|
||||||
|
}
|
||||||
|
|
||||||
function exportCsv() {
|
function exportCsv() {
|
||||||
const filtered = getFiltered();
|
const filtered = getFiltered();
|
||||||
const headers = ["Severity", "Provider", "Pattern", "Match", "Type", "Domain", "URL", "Page URL", "Timestamp"];
|
const headers = ["Severity", "Provider", "Pattern", "Match", "Type", "Domain", "URL", "Page URL", "Timestamp"];
|
||||||
const rows = filtered.map((f) => [
|
const rows = filtered.map((f) => [
|
||||||
f.severity || "",
|
csvSafe(f.severity),
|
||||||
f.provider || "",
|
csvSafe(f.provider),
|
||||||
f.patternName || "",
|
csvSafe(f.patternName),
|
||||||
`"${(f.match || "").replace(/"/g, '""')}"`,
|
csvSafe(f.match),
|
||||||
f.type || "",
|
csvSafe(f.type),
|
||||||
f.domain || "",
|
csvSafe(f.domain),
|
||||||
f.url || "",
|
csvSafe(f.url),
|
||||||
f.pageUrl || "",
|
csvSafe(f.pageUrl),
|
||||||
f.timestamp ? new Date(f.timestamp).toISOString() : "",
|
csvSafe(f.timestamp ? new Date(f.timestamp).toISOString() : ""),
|
||||||
]);
|
]);
|
||||||
const csv = [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
|
const csv = [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
|
||||||
const blob = new Blob([csv], { type: "text/csv" });
|
const blob = new Blob([csv], { type: "text/csv" });
|
||||||
|
|||||||
+12
-4
@@ -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.",
|
"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.0.0",
|
"version": "2.1.1",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
@@ -34,14 +34,22 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matches": ["<all_urls>"],
|
"matches": ["<all_urls>"],
|
||||||
"js": ["js/interceptor.js"],
|
"js": ["js/interceptor-loader.js"],
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"world": "MAIN",
|
|
||||||
"all_frames": true
|
"all_frames": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"content_security_policy": {
|
||||||
|
"extension_pages": "script-src 'self'; object-src 'self'"
|
||||||
|
},
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["js/background.js"]
|
"scripts": ["js/background.js"]
|
||||||
},
|
},
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["js/interceptor.js"],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"permissions": ["activeTab", "storage"]
|
"permissions": ["activeTab", "storage"]
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-3
@@ -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.0.0",
|
"version": "2.1.1",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"action": {
|
"action": {
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
@@ -25,14 +25,22 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matches": ["<all_urls>"],
|
"matches": ["<all_urls>"],
|
||||||
"js": ["js/interceptor.js"],
|
"js": ["js/interceptor-loader.js"],
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"world": "MAIN",
|
|
||||||
"all_frames": true
|
"all_frames": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"content_security_policy": {
|
||||||
|
"extension_pages": "script-src 'self'; object-src 'self'"
|
||||||
|
},
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "js/background.js"
|
"service_worker": "js/background.js"
|
||||||
},
|
},
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["js/interceptor.js"],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"permissions": ["activeTab", "storage"]
|
"permissions": ["activeTab", "storage"]
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
<div class="header-brand">
|
<div class="header-brand">
|
||||||
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
|
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
|
||||||
<h1>KeyFinder</h1>
|
<h1>KeyFinder</h1>
|
||||||
<span class="version">v2.0</span>
|
<span class="version" id="versionLabel"></span>
|
||||||
</div>
|
</div>
|
||||||
<p class="header-tagline">Passive API key & secret discovery</p>
|
<p class="header-tagline">Passive API key & secret discovery</p>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
|
<img src="icons/icon48.png" alt="KeyFinder" class="header-icon">
|
||||||
<h1>KeyFinder <span class="version">v2.0</span></h1>
|
<h1>KeyFinder <span class="version" id="versionLabel"></span></h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ SHARED_FILES=(
|
|||||||
js/background.js
|
js/background.js
|
||||||
js/content.js
|
js/content.js
|
||||||
js/interceptor.js
|
js/interceptor.js
|
||||||
|
js/interceptor-loader.js
|
||||||
js/patterns.js
|
js/patterns.js
|
||||||
js/popup.js
|
js/popup.js
|
||||||
js/results.js
|
js/results.js
|
||||||
|
|||||||
Reference in New Issue
Block a user