v2.0.0: Complete rewrite - Manifest V3, enterprise-grade secret detection

- Migrated to Chrome Manifest V3 with service worker architecture
- 80+ secret detection patterns covering AWS, GCP, Azure, GitHub, GitLab,
  Stripe, Slack, Discord, OpenAI, and 30+ other providers
- 10 scanning surfaces: inline scripts, external scripts, meta tags,
  hidden inputs, data attributes, HTML comments, URL params, web storage,
  cookies, and network response interception
- Shannon entropy analysis for detecting undocumented secret formats
- MAIN world interceptor for XHR/fetch response scanning and window globals
- Professional dark-theme UI with filtering, search, and CSV/JSON export
- Zero dependencies - removed jQuery, Bootstrap, font-awesome, popper
- Proper XSS-safe DOM rendering throughout
- Badge counter on extension icon showing finding count
- All frames scanning including iframes
This commit is contained in:
moamen
2026-04-07 18:22:42 +02:00
parent 8f635dea50
commit b73c2185b0
25 changed files with 1809 additions and 442 deletions
+99 -10
View File
@@ -1,14 +1,103 @@
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.method == "getStatus")
sendResponse({status: localStorage});
else
sendResponse({});
const KEYWORDS_KEY = "kf_keywords";
const FINDINGS_KEY = "kf_findings";
const DEFAULT_KEYWORDS = [
"key", "api_key", "apikey", "api-key", "secret", "token",
"access_token", "auth", "credential", "password",
"client_id", "client_secret"
];
chrome.runtime.onInstalled.addListener(async (details) => {
if (details.reason === "install") {
await chrome.storage.local.set({
[KEYWORDS_KEY]: DEFAULT_KEYWORDS,
[FINDINGS_KEY]: []
});
}
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === "finding") {
saveFinding(request.data).then(() => sendResponse({ ok: true }));
return true;
}
if (request.type === "getKeywords") {
getKeywords().then((keywords) => sendResponse({ keywords }));
return true;
}
if (request.type === "getFindings") {
getFindings().then((findings) => sendResponse({ findings }));
return true;
}
if (request.type === "addKeyword") {
addKeyword(request.keyword).then((result) => sendResponse(result));
return true;
}
if (request.type === "removeKeyword") {
removeKeyword(request.keyword).then(() => sendResponse({ ok: true }));
return true;
}
if (request.type === "removeFinding") {
removeFinding(request.url).then(() => sendResponse({ ok: true }));
return true;
}
if (request.type === "clearFindings") {
clearFindings().then(() => sendResponse({ ok: true }));
return true;
}
if (request.type === "exportFindings") {
getFindings().then((findings) => sendResponse({ findings }));
return true;
}
});
async function getKeywords() {
const result = await chrome.storage.local.get(KEYWORDS_KEY);
return result[KEYWORDS_KEY] || DEFAULT_KEYWORDS;
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.getter)
console.log(request.getter);
localStorage.setItem(request.getter,request.getter);
});
async function addKeyword(keyword) {
const keywords = await getKeywords();
const normalized = keyword.trim().toLowerCase();
if (!normalized) return { ok: false, error: "Keyword cannot be empty." };
if (keywords.includes(normalized)) return { ok: false, error: "Keyword already exists." };
keywords.push(normalized);
await chrome.storage.local.set({ [KEYWORDS_KEY]: keywords });
return { ok: true };
}
async function removeKeyword(keyword) {
const keywords = await getKeywords();
await chrome.storage.local.set({ [KEYWORDS_KEY]: keywords.filter((k) => k !== keyword) });
}
async function getFindings() {
const result = await chrome.storage.local.get(FINDINGS_KEY);
return result[FINDINGS_KEY] || [];
}
async function saveFinding(finding) {
const findings = await getFindings();
const isDuplicate = findings.some(
(f) => f.url === finding.url && f.match === finding.match
);
if (isDuplicate) return;
findings.push(finding);
await chrome.storage.local.set({ [FINDINGS_KEY]: findings });
const badgeCount = findings.length;
chrome.action.setBadgeText({ text: badgeCount > 0 ? String(badgeCount) : "" });
chrome.action.setBadgeBackgroundColor({ color: "#e74c3c" });
}
async function removeFinding(url) {
const findings = await getFindings();
const updated = findings.filter((f) => f.url !== url);
await chrome.storage.local.set({ [FINDINGS_KEY]: updated });
chrome.action.setBadgeText({ text: updated.length > 0 ? String(updated.length) : "" });
}
async function clearFindings() {
await chrome.storage.local.set({ [FINDINGS_KEY]: [] });
chrome.action.setBadgeText({ text: "" });
}
-7
View File
File diff suppressed because one or more lines are too long
+314 -40
View File
@@ -1,59 +1,333 @@
(async function () {
"use strict";
console.log("keyFinder🔑 is working!");
const pageUrl = location.href;
const pageDomain = location.hostname;
const seen = new Set();
let keywords = [];
try {
const response = await chrome.runtime.sendMessage({ type: "getKeywords" });
keywords = (response.keywords || []).map((k) => k.toLowerCase());
} catch {
return;
}
function shannonEntropy(str) {
const len = str.length;
if (len === 0) return 0;
const freq = {};
for (const ch of str) freq[ch] = (freq[ch] || 0) + 1;
let entropy = 0;
for (const ch in freq) {
const p = freq[ch] / len;
entropy -= p * Math.log2(p);
}
return entropy;
}
//custom searches
function isHighEntropy(str) {
if (str.length < 12) return false;
return shannonEntropy(str) > 3.5;
}
let js = document.getElementsByTagName('script');
function isFalsePositive(match) {
if (!match || match.length < 8) return true;
const lower = match.toLowerCase();
const fp = [
"true", "false", "null", "undefined", "function", "return",
"window", "document", "object", "string", "number", "boolean",
"prototype", "constructor", "adsbygoogle", "googletag",
"use strict", "text/javascript", "application/json",
"content-type", "text/html", "text/css", "image/png",
"image/jpeg", "charset=utf-8", "viewport", "width=device",
"http-equiv", "stylesheet", "text/plain",
];
for (const f of fp) {
if (lower === f) return true;
}
if (/^(0+|1+|a+|f+|x+)$/i.test(match)) return true;
if (/^[a-z]+$/i.test(match) && match.length < 20) return true;
if (/^(https?:\/\/)?[a-z0-9.-]+\.(js|css|html|png|jpg|gif|svg|woff|ttf|eot|ico)$/i.test(match)) return true;
return false;
}
function report(data) {
const key = `${data.type}:${data.match}:${data.url || ""}`;
if (seen.has(key)) return;
seen.add(key);
try {
chrome.runtime.sendMessage({
type: "finding",
data: { ...data, domain: pageDomain, pageUrl, timestamp: Date.now() },
});
} catch {}
}
function scanText(text, sourceUrl, sourceType) {
if (!text || text.length < 10) return;
chrome.runtime.sendMessage({method: "getStatus"}, function(response) {
var savedSearch = response.status;
for (var key in savedSearch) {
if (savedSearch.hasOwnProperty(key)) {
for(src in js){
var url = js[src].src;
let regex = new RegExp(key,'i');
try {
if(url.search(regex) !== -1) {
chrome.runtime.sendMessage({getter: url}, function(response) {
});
console.log(`KeyFinder: a potential API key found: ${url} it matched your search ${key}`);
}
} catch(err) {
//do nothing
}
}
for (const pattern of SECRET_PATTERNS) {
pattern.re.lastIndex = 0;
let m;
while ((m = pattern.re.exec(text)) !== null) {
const matched = m[1] || m[0];
if (isFalsePositive(matched)) continue;
report({
url: sourceUrl,
match: matched.substring(0, 200),
type: sourceType,
patternName: pattern.name,
severity: pattern.severity,
confidence: pattern.confidence,
provider: pattern.provider,
});
}
}
});
for (const kw of keywords) {
const escaped = kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const kwRegex = new RegExp(
`(?:${escaped})\\s*[:=]\\s*['"\`]([^'"\`\\n]{8,200})['"\`]`,
"gi"
);
let m;
while ((m = kwRegex.exec(text)) !== null) {
const val = m[1];
if (isFalsePositive(val)) continue;
report({
url: sourceUrl,
match: val.substring(0, 200),
type: sourceType,
patternName: `Keyword: ${kw}`,
severity: "medium",
confidence: isHighEntropy(val) ? "high" : "medium",
provider: "Keyword Match",
});
}
}
}
function scanScriptSrcUrls() {
const scripts = document.querySelectorAll("script[src]");
for (const script of scripts) {
const src = script.src;
if (!src) continue;
for (const kw of keywords) {
if (src.toLowerCase().includes(kw)) {
report({
url: src, match: src, type: "script-src",
patternName: `Script URL contains: ${kw}`,
severity: "medium", confidence: "medium", provider: "URL Scan",
});
}
}
try {
const url = new URL(src);
for (const [param, value] of url.searchParams) {
if (value.length >= 16 && isHighEntropy(value)) {
report({
url: src, match: `${param}=${value.substring(0, 100)}`,
type: "url-param", patternName: "High-Entropy URL Parameter",
severity: "medium", confidence: "medium", provider: "URL Scan",
});
}
}
} catch {}
}
}
function scanInlineScripts() {
const scripts = document.querySelectorAll("script:not([src])");
for (const script of scripts) {
scanText(script.textContent, pageUrl, "inline-script");
}
}
async function scanExternalScripts() {
const scripts = document.querySelectorAll("script[src]");
const fetched = new Set();
for (const script of scripts) {
try {
const src = script.src;
if (fetched.has(src)) continue;
if (new URL(src).origin !== location.origin) continue;
fetched.add(src);
const resp = await fetch(src, { credentials: "omit" });
if (!resp.ok) continue;
const text = await resp.text();
scanText(text, src, "external-script");
} catch {}
}
}
// for GoogleMaps API key
for (src in js) {
let url = js[src].src;
try {
if(url.search(/key/i) !== -1) {
console.log(`KeyFinder: a potential API key found: ${url}`)
function scanMetaTags() {
const metas = document.querySelectorAll("meta");
for (const meta of metas) {
const content = meta.getAttribute("content");
if (!content || content.length < 12) continue;
const name = (meta.getAttribute("name") || meta.getAttribute("property") || "").toLowerCase();
const sensitive = ["api-key", "api_key", "apikey", "token", "secret", "access-token", "csrf-token", "csrf_token"];
if (sensitive.some((s) => name.includes(s))) {
report({
url: pageUrl, match: `meta[${name}]=${content.substring(0, 100)}`,
type: "meta-tag", patternName: "Sensitive Meta Tag",
severity: "high", confidence: "high", provider: "DOM Scan",
});
}
scanText(`${name}=${content}`, pageUrl, "meta-tag");
}
}
//saving the results
chrome.runtime.sendMessage({getter: url}, function(response) {
function scanHiddenInputs() {
const inputs = document.querySelectorAll('input[type="hidden"]');
for (const input of inputs) {
const name = (input.name || input.id || "").toLowerCase();
const value = input.value;
if (!value || value.length < 8) continue;
const sensitive = ["token", "csrf", "api_key", "apikey", "secret", "auth", "session", "nonce", "key", "access_token"];
if (sensitive.some((s) => name.includes(s)) || isHighEntropy(value)) {
report({
url: pageUrl, match: `${name}=${value.substring(0, 100)}`,
type: "hidden-input", patternName: "Hidden Form Field",
severity: isHighEntropy(value) ? "high" : "medium",
confidence: sensitive.some((s) => name.includes(s)) ? "high" : "medium",
provider: "DOM Scan",
});
}
}
}
function scanDataAttributes() {
const all = document.querySelectorAll("*");
for (const el of all) {
for (const attr of el.attributes) {
if (!/^data-.*(?:key|token|secret|auth|api|credential|password)/i.test(attr.name)) continue;
if (!attr.value || attr.value.length < 8) continue;
report({
url: pageUrl, match: `${attr.name}="${attr.value.substring(0, 100)}"`,
type: "data-attribute", patternName: "Sensitive Data Attribute",
severity: "medium", confidence: isHighEntropy(attr.value) ? "high" : "medium",
provider: "DOM Scan",
});
}
}
}
function scanHtmlComments() {
const walker = document.createTreeWalker(document.documentElement, NodeFilter.SHOW_COMMENT, null);
while (walker.nextNode()) {
const text = walker.currentNode.textContent;
if (text && text.length >= 20) {
scanText(text, pageUrl, "html-comment");
}
}
}
function scanLinkHrefs() {
const links = document.querySelectorAll("a[href], link[href]");
for (const link of links) {
try {
const href = link.href;
if (!href) continue;
const url = new URL(href);
for (const [param, value] of url.searchParams) {
const p = param.toLowerCase();
const sensitive = ["key", "api_key", "apikey", "token", "secret", "access_token", "auth", "password", "session_id"];
if (sensitive.some((s) => p.includes(s)) && value.length >= 8) {
report({
url: href, match: `${param}=${value.substring(0, 100)}`,
type: "url-param", patternName: "Sensitive URL Parameter",
severity: "high", confidence: "high", provider: "URL Scan",
});
}
}
} catch {}
}
}
function scanWebStorage() {
const stores = [
{ store: localStorage, label: "localStorage" },
{ store: sessionStorage, label: "sessionStorage" },
];
for (const { store, label } of stores) {
try {
for (let i = 0; i < store.length; i++) {
const key = store.key(i);
const value = store.getItem(key);
if (!value || value.length < 12) continue;
const kl = key.toLowerCase();
const sensitive = ["token", "key", "secret", "auth", "session", "credential", "password", "jwt", "bearer"];
if (sensitive.some((s) => kl.includes(s)) || isHighEntropy(value.substring(0, 100))) {
report({
url: pageUrl, match: `${label}.${key}=${value.substring(0, 120)}`,
type: "web-storage", patternName: `${label} Secret`,
severity: "high",
confidence: sensitive.some((s) => kl.includes(s)) ? "high" : "medium",
provider: "Storage Scan",
});
}
scanText(`${key}=${value}`, pageUrl, "web-storage");
}
} catch {}
}
}
function scanCookies() {
try {
const cookies = document.cookie.split(";");
for (const cookie of cookies) {
const [name, ...rest] = cookie.split("=");
if (!name) continue;
const value = rest.join("=").trim();
const n = name.trim().toLowerCase();
const sensitive = ["token", "session", "auth", "jwt", "bearer", "api_key", "apikey", "secret", "credential"];
if (value && value.length >= 16 && sensitive.some((s) => n.includes(s))) {
report({
url: pageUrl, match: `cookie:${name.trim()}=${value.substring(0, 80)}`,
type: "cookie", patternName: "Sensitive Cookie",
severity: "medium", confidence: "medium", provider: "Cookie Scan",
});
}
}
} catch {}
}
window.addEventListener("__kf_finding__", (e) => {
const data = e.detail;
if (!data) return;
if (data.rawText) {
scanText(data.rawText, data.sourceUrl || pageUrl, data.type);
if (!data.match) return;
}
if (data.match) {
report({
url: data.sourceUrl || pageUrl,
match: data.match,
type: data.type,
patternName: data.patternName || data.type,
severity: data.severity || "medium",
confidence: data.confidence || "medium",
provider: data.provider || "Runtime Scan",
});
}
} catch (err) {
//console.log(err)
});
scanScriptSrcUrls();
scanInlineScripts();
scanMetaTags();
scanHiddenInputs();
scanDataAttributes();
scanHtmlComments();
scanLinkHrefs();
scanWebStorage();
scanCookies();
await scanExternalScripts();
if (seen.size > 0) {
console.log(`[KeyFinder] ${seen.size} potential secret(s) found on ${pageDomain}`);
}
}
})();
View File
+106
View File
@@ -0,0 +1,106 @@
(function () {
"use strict";
const EVENT_NAME = "__kf_finding__";
function emit(data) {
window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: data }));
}
function shannonEntropy(str) {
const len = str.length;
if (len === 0) return 0;
const freq = {};
for (const ch of str) freq[ch] = (freq[ch] || 0) + 1;
let entropy = 0;
for (const ch in freq) {
const p = freq[ch] / len;
entropy -= p * Math.log2(p);
}
return entropy;
}
function isHighEntropy(str) {
return str.length >= 12 && shannonEntropy(str) > 3.5;
}
const globalNames = [
"API_KEY", "api_key", "apiKey", "apikey",
"SECRET", "secret", "secretKey", "secret_key",
"TOKEN", "token", "accessToken", "access_token",
"AUTH_TOKEN", "authToken", "auth_token",
"STRIPE_KEY", "stripeKey", "stripe_key",
"FIREBASE_CONFIG", "firebaseConfig",
"AWS_ACCESS_KEY", "awsAccessKey",
"__NEXT_DATA__", "__NUXT__", "__APP_CONFIG__",
"__ENV__", "__CONFIG__", "ENV", "CONFIG",
];
for (const name of globalNames) {
try {
const val = window[name];
if (val === undefined || val === null) continue;
const str = typeof val === "object" ? JSON.stringify(val) : String(val);
if (str.length < 8 || str === "[object Object]") continue;
emit({
match: `window.${name}=${str.substring(0, 200)}`,
type: "window-global",
patternName: "Exposed Global Variable",
severity: "high",
confidence: typeof val !== "object" && isHighEntropy(str.substring(0, 60)) ? "high" : "medium",
provider: "JS Global Scan",
isObject: typeof val === "object",
rawText: typeof val === "object" ? str.substring(0, 5000) : null,
});
} catch {}
}
const origXhrOpen = XMLHttpRequest.prototype.open;
const origXhrSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) {
this._kfUrl = url;
return origXhrOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
this.addEventListener("load", function () {
try {
const ct = this.getResponseHeader("content-type") || "";
if (ct.includes("json") || ct.includes("javascript") || ct.includes("text")) {
const body = this.responseText;
if (body && body.length > 10 && body.length < 500000) {
emit({
type: "xhr-response",
sourceUrl: String(this._kfUrl || ""),
rawText: body,
});
}
}
} catch {}
});
return origXhrSend.apply(this, arguments);
};
const origFetch = window.fetch;
window.fetch = async function () {
const response = await origFetch.apply(this, arguments);
try {
const url = typeof arguments[0] === "string" ? arguments[0] : arguments[0]?.url || "";
const cloned = response.clone();
const ct = cloned.headers.get("content-type") || "";
if (ct.includes("json") || ct.includes("javascript") || ct.includes("text")) {
cloned.text().then((body) => {
if (body && body.length > 10 && body.length < 500000) {
emit({
type: "fetch-response",
sourceUrl: String(url || ""),
rawText: body,
});
}
}).catch(() => {});
}
} catch {}
return response;
};
})();
-2
View File
File diff suppressed because one or more lines are too long
+134
View File
@@ -0,0 +1,134 @@
const SECRET_PATTERNS = [
{ name: "AWS Access Key ID", re: /\b(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}\b/g, severity: "critical", confidence: "high", provider: "AWS" },
{ name: "AWS Secret Access Key", re: /(?:aws_secret_access_key|aws_secret|secret_access_key|AWS_SECRET)\s*[:=]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi, severity: "critical", confidence: "high", provider: "AWS" },
{ name: "AWS MWS Auth Token", re: /amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, severity: "critical", confidence: "high", provider: "AWS" },
{ name: "AWS Cognito Pool ID", re: /(?:us|eu|ap|sa|ca|me|af)-(?:east|west|south|north|central|southeast|northeast)-[0-9]:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, severity: "medium", confidence: "high", provider: "AWS" },
{ name: "AWS AppSync GraphQL Key", re: /da2-[a-z0-9]{26}/g, severity: "high", confidence: "high", provider: "AWS" },
{ name: "AWS Session Token", re: /(?:aws_session_token|AWS_SESSION_TOKEN)\s*[:=]\s*['"]?([A-Za-z0-9/+=]{100,})['"]?/gi, severity: "critical", confidence: "high", provider: "AWS" },
{ name: "Google API Key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g, severity: "high", confidence: "high", provider: "Google" },
{ name: "Google OAuth Access Token", re: /\bya29\.[0-9A-Za-z_-]+/g, severity: "critical", confidence: "high", provider: "Google" },
{ name: "Google OAuth Client ID", re: /[0-9]+-[a-z0-9_]{32}\.apps\.googleusercontent\.com/g, severity: "medium", confidence: "high", provider: "Google" },
{ name: "Google OAuth Client Secret", re: /(?:client_secret|google_secret)\s*[:=]\s*['"]?(GOCSPX-[A-Za-z0-9_-]{28})['"]?/gi, severity: "critical", confidence: "high", provider: "Google" },
{ name: "Google Cloud Service Account", re: /"type"\s*:\s*"service_account"/g, severity: "critical", confidence: "high", provider: "Google" },
{ name: "Firebase Database URL", re: /https:\/\/[a-z0-9-]+\.firebaseio\.com/g, severity: "medium", confidence: "high", provider: "Firebase" },
{ name: "Firebase Cloud Messaging Key", re: /\bAAAA[A-Za-z0-9_-]{7}:[A-Za-z0-9_-]{140}/g, severity: "high", confidence: "high", provider: "Firebase" },
{ name: "Azure Storage Account Key", re: /(?:AccountKey|azure_storage_key|AZURE_STORAGE_KEY)\s*[:=]\s*['"]?([A-Za-z0-9/+=]{88})['"]?/gi, severity: "critical", confidence: "high", provider: "Azure" },
{ name: "Azure Connection String", re: /DefaultEndpointsProtocol=https?;AccountName=[^;]+;AccountKey=[A-Za-z0-9/+=]{88}/g, severity: "critical", confidence: "high", provider: "Azure" },
{ name: "Azure SAS Token", re: /[?&]sig=[A-Za-z0-9%/+=]+&/g, severity: "high", confidence: "medium", provider: "Azure" },
{ name: "GitHub Personal Access Token", re: /\bghp_[A-Za-z0-9_]{36,}\b/g, severity: "critical", confidence: "high", provider: "GitHub" },
{ name: "GitHub OAuth Access Token", re: /\bgho_[A-Za-z0-9_]{36,}\b/g, severity: "critical", confidence: "high", provider: "GitHub" },
{ name: "GitHub User-to-Server Token", re: /\bghu_[A-Za-z0-9_]{36,}\b/g, severity: "critical", confidence: "high", provider: "GitHub" },
{ name: "GitHub Server-to-Server Token", re: /\bghs_[A-Za-z0-9_]{36,}\b/g, severity: "critical", confidence: "high", provider: "GitHub" },
{ name: "GitHub Refresh Token", re: /\bghr_[A-Za-z0-9_]{36,}\b/g, severity: "critical", confidence: "high", provider: "GitHub" },
{ name: "GitHub Fine-grained PAT", re: /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, severity: "critical", confidence: "high", provider: "GitHub" },
{ name: "GitLab Personal Access Token", re: /\bglpat-[A-Za-z0-9_-]{20,}\b/g, severity: "critical", confidence: "high", provider: "GitLab" },
{ name: "GitLab Pipeline Token", re: /\bglptt-[A-Za-z0-9_-]{20,}\b/g, severity: "high", confidence: "high", provider: "GitLab" },
{ name: "GitLab Runner Token", re: /\bGR1348941[A-Za-z0-9_-]{20}\b/g, severity: "high", confidence: "high", provider: "GitLab" },
{ name: "Stripe Secret Key", re: /\bsk_(live|test)_[0-9a-zA-Z]{24,}\b/g, severity: "critical", confidence: "high", provider: "Stripe" },
{ name: "Stripe Publishable Key", re: /\bpk_(live|test)_[0-9a-zA-Z]{24,}\b/g, severity: "low", confidence: "high", provider: "Stripe" },
{ name: "Stripe Restricted Key", re: /\brk_(live|test)_[0-9a-zA-Z]{24,}\b/g, severity: "critical", confidence: "high", provider: "Stripe" },
{ name: "Stripe Webhook Secret", re: /\bwhsec_[A-Za-z0-9]{32,}\b/g, severity: "high", confidence: "high", provider: "Stripe" },
{ name: "PayPal Braintree Access Token", re: /access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}/g, severity: "critical", confidence: "high", provider: "PayPal" },
{ name: "Square Access Token", re: /\bsq0atp-[0-9A-Za-z_-]{22}\b/g, severity: "critical", confidence: "high", provider: "Square" },
{ name: "Square OAuth Secret", re: /\bsq0csp-[0-9A-Za-z_-]{43}\b/g, severity: "critical", confidence: "high", provider: "Square" },
{ name: "Slack Bot Token", re: /\bxoxb-[0-9]{10,}-[0-9]{10,}-[A-Za-z0-9]{24,}\b/g, severity: "critical", confidence: "high", provider: "Slack" },
{ name: "Slack User Token", re: /\bxoxp-[0-9]{10,}-[0-9]{10,}-[A-Za-z0-9]{24,}\b/g, severity: "critical", confidence: "high", provider: "Slack" },
{ name: "Slack App Token", re: /\bxapp-[0-9]+-[A-Za-z0-9]+-[0-9]+-[A-Za-z0-9]+/g, severity: "high", confidence: "high", provider: "Slack" },
{ name: "Slack Webhook URL", re: /hooks\.slack\.com\/services\/T[A-Z0-9]{8,}\/B[A-Z0-9]{8,}\/[A-Za-z0-9]{24}/g, severity: "high", confidence: "high", provider: "Slack" },
{ name: "Discord Bot Token", re: /[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27,}/g, severity: "critical", confidence: "high", provider: "Discord" },
{ name: "Discord Webhook URL", re: /discord(?:app)?\.com\/api\/webhooks\/[0-9]+\/[A-Za-z0-9_-]+/g, severity: "high", confidence: "high", provider: "Discord" },
{ name: "Telegram Bot Token", re: /\b[0-9]{8,10}:[A-Za-z0-9_-]{35}\b/g, severity: "critical", confidence: "high", provider: "Telegram" },
{ name: "Twilio Account SID", re: /\bAC[0-9a-fA-F]{32}\b/g, severity: "medium", confidence: "high", provider: "Twilio" },
{ name: "Twilio API Key", re: /\bSK[0-9a-fA-F]{32}\b/g, severity: "high", confidence: "high", provider: "Twilio" },
{ name: "SendGrid API Key", re: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/g, severity: "critical", confidence: "high", provider: "SendGrid" },
{ name: "Mailchimp API Key", re: /\b[0-9a-f]{32}-us[0-9]{1,2}\b/g, severity: "high", confidence: "high", provider: "Mailchimp" },
{ name: "Mailgun API Key", re: /\bkey-[0-9a-zA-Z]{32}\b/g, severity: "high", confidence: "high", provider: "Mailgun" },
{ name: "NPM Access Token", re: /\bnpm_[A-Za-z0-9]{36}\b/g, severity: "critical", confidence: "high", provider: "NPM" },
{ name: "Docker Hub Token", re: /\bdckr_pat_[A-Za-z0-9_-]{27}\b/g, severity: "high", confidence: "high", provider: "Docker" },
{ name: "Vault Token", re: /\b(?:hvs|hvb|hvr)\.[A-Za-z0-9_-]{24,}\b/g, severity: "critical", confidence: "high", provider: "HashiCorp" },
{ name: "Terraform Cloud Token", re: /\b[A-Za-z0-9]{14}\.atlasv1\.[A-Za-z0-9_-]{60,}\b/g, severity: "critical", confidence: "high", provider: "HashiCorp" },
{ name: "MongoDB Connection String", re: /mongodb(?:\+srv)?:\/\/[^\s'"<>]+/g, severity: "critical", confidence: "high", provider: "MongoDB" },
{ name: "PostgreSQL Connection String", re: /postgres(?:ql)?:\/\/[^\s'"<>]+/g, severity: "critical", confidence: "high", provider: "PostgreSQL" },
{ name: "MySQL Connection String", re: /mysql:\/\/[^\s'"<>]+/g, severity: "critical", confidence: "high", provider: "MySQL" },
{ name: "Redis Connection String", re: /redis(?:s)?:\/\/[^\s'"<>]+/g, severity: "critical", confidence: "high", provider: "Redis" },
{ name: "Shopify Access Token", re: /\bshpat_[a-fA-F0-9]{32}\b/g, severity: "critical", confidence: "high", provider: "Shopify" },
{ name: "Shopify Custom App Token", re: /\bshpca_[a-fA-F0-9]{32}\b/g, severity: "critical", confidence: "high", provider: "Shopify" },
{ name: "Shopify Private App Token", re: /\bshppa_[a-fA-F0-9]{32}\b/g, severity: "critical", confidence: "high", provider: "Shopify" },
{ name: "Shopify Shared Secret", re: /\bshpss_[a-fA-F0-9]{32}\b/g, severity: "critical", confidence: "high", provider: "Shopify" },
{ name: "Sentry DSN", re: /https:\/\/[0-9a-f]{32}@(?:o[0-9]+\.)?(?:sentry\.io|[a-z0-9.-]+)\/[0-9]+/g, severity: "medium", confidence: "high", provider: "Sentry" },
{ name: "Sentry Auth Token", re: /\bsntrys_[A-Za-z0-9_]{64,}\b/g, severity: "high", confidence: "high", provider: "Sentry" },
{ name: "New Relic API Key", re: /\bNRAK-[A-Z0-9]{27}\b/g, severity: "high", confidence: "high", provider: "New Relic" },
{ name: "New Relic Browser Key", re: /\bNRJS-[a-f0-9]{19}\b/g, severity: "medium", confidence: "high", provider: "New Relic" },
{ name: "PlanetScale Token", re: /\bpscale_tkn_[A-Za-z0-9_-]{43}\b/g, severity: "critical", confidence: "high", provider: "PlanetScale" },
{ name: "PlanetScale Password", re: /\bpscale_pw_[A-Za-z0-9_-]{43}\b/g, severity: "critical", confidence: "high", provider: "PlanetScale" },
{ name: "Linear API Key", re: /\blin_api_[A-Za-z0-9]{40}\b/g, severity: "high", confidence: "high", provider: "Linear" },
{ name: "Notion Integration Token", re: /\bntn_[A-Za-z0-9]{40,}\b/g, severity: "high", confidence: "high", provider: "Notion" },
{ name: "Notion Secret", re: /\bsecret_[A-Za-z0-9]{43}\b/g, severity: "high", confidence: "medium", provider: "Notion" },
{ name: "OpenAI API Key", re: /\bsk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}\b/g, severity: "critical", confidence: "high", provider: "OpenAI" },
{ name: "OpenAI API Key (Project)", re: /\bsk-proj-[A-Za-z0-9_-]{40,}\b/g, severity: "critical", confidence: "high", provider: "OpenAI" },
{ name: "Anthropic API Key", re: /\bsk-ant-[A-Za-z0-9_-]{90,}\b/g, severity: "critical", confidence: "high", provider: "Anthropic" },
{ name: "HuggingFace Token", re: /\bhf_[A-Za-z0-9]{34}\b/g, severity: "high", confidence: "high", provider: "HuggingFace" },
{ name: "Replicate API Token", re: /\br8_[A-Za-z0-9]{36}\b/g, severity: "high", confidence: "high", provider: "Replicate" },
{ name: "Twitter Bearer Token", re: /\bAAAAAAAAAAAAAAAAAAAAA[A-Za-z0-9%]+/g, severity: "critical", confidence: "high", provider: "Twitter" },
{ name: "Facebook Access Token", re: /\bEAAC[a-zA-Z0-9]+/g, severity: "critical", confidence: "high", provider: "Facebook" },
{ name: "Instagram Access Token", re: /\bIGQV[A-Za-z0-9_-]+/g, severity: "high", confidence: "high", provider: "Instagram" },
{ name: "Cloudflare API Token", re: /(?:cloudflare_api_token|CF_API_TOKEN|CLOUDFLARE_API_TOKEN)\s*[:=]\s*['"]?([A-Za-z0-9_-]{40})['"]?/gi, severity: "high", confidence: "medium", provider: "Cloudflare" },
{ name: "DigitalOcean Token", re: /\bdop_v1_[a-f0-9]{64}\b/g, severity: "critical", confidence: "high", provider: "DigitalOcean" },
{ name: "DigitalOcean Spaces Key", re: /\bDO00[A-Z0-9]{36}\b/g, severity: "high", confidence: "high", provider: "DigitalOcean" },
{ name: "Doppler Token", re: /\bdp\.(?:ct|st|sa|scim)\.[A-Za-z0-9_-]{40,}\b/g, severity: "critical", confidence: "high", provider: "Doppler" },
{ name: "Pulumi Access Token", re: /\bpul-[a-f0-9]{40}\b/g, severity: "high", confidence: "high", provider: "Pulumi" },
{ name: "Grafana API Key", re: /\bglc_[A-Za-z0-9_+/]{32,}\b/g, severity: "high", confidence: "high", provider: "Grafana" },
{ name: "Grafana Service Account Token", re: /\bglsa_[A-Za-z0-9_]{32,}_[0-9a-f]{8}\b/g, severity: "high", confidence: "high", provider: "Grafana" },
{ name: "Mapbox Public Token", re: /\bpk\.[A-Za-z0-9_-]{60,}\.[A-Za-z0-9_-]{20,}\b/g, severity: "medium", confidence: "high", provider: "Mapbox" },
{ name: "Mapbox Secret Token", re: /\bsk\.[A-Za-z0-9_-]{60,}\.[A-Za-z0-9_-]{20,}\b/g, severity: "high", confidence: "high", provider: "Mapbox" },
{ name: "Datadog API Key", re: /(?:datadog_api_key|DD_API_KEY|DATADOG_API_KEY)\s*[:=]\s*['"]?([0-9a-f]{32})['"]?/gi, severity: "high", confidence: "high", provider: "Datadog" },
{ name: "Algolia API Key", re: /(?:algolia_api_key|ALGOLIA_API_KEY|algolia_admin_key)\s*[:=]\s*['"]?([A-Za-z0-9]{32})['"]?/gi, severity: "high", confidence: "medium", provider: "Algolia" },
{ name: "Vercel Access Token", re: /(?:vercel_token|VERCEL_TOKEN)\s*[:=]\s*['"]?([A-Za-z0-9]{24})['"]?/gi, severity: "high", confidence: "medium", provider: "Vercel" },
{ name: "JSON Web Token", re: /\beyJhbGci[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, severity: "high", confidence: "high", provider: "JWT" },
{ name: "RSA Private Key", re: /-----BEGIN RSA PRIVATE KEY-----/g, severity: "critical", confidence: "high", provider: "Crypto" },
{ name: "EC Private Key", re: /-----BEGIN EC PRIVATE KEY-----/g, severity: "critical", confidence: "high", provider: "Crypto" },
{ name: "OpenSSH Private Key", re: /-----BEGIN OPENSSH PRIVATE KEY-----/g, severity: "critical", confidence: "high", provider: "Crypto" },
{ name: "PGP Private Key Block", re: /-----BEGIN PGP PRIVATE KEY BLOCK-----/g, severity: "critical", confidence: "high", provider: "Crypto" },
{ name: "Authorization Bearer Token", re: /(?:Authorization|Bearer)\s*[:=]\s*['"]?Bearer\s+([A-Za-z0-9_.\-/+=]{20,})['"]?/gi, severity: "high", confidence: "medium", provider: "Generic" },
{ name: "Basic Auth Credentials", re: /(?:Authorization)\s*[:=]\s*['"]?Basic\s+([A-Za-z0-9+/=]{10,})['"]?/gi, severity: "high", confidence: "medium", provider: "Generic" },
{ name: "Generic API Key", re: /(?:api[_-]?key|apiKey|API_KEY)\s*[:=]\s*['"`]([A-Za-z0-9_\-/.]{16,120})['"`]/gi, severity: "medium", confidence: "medium", provider: "Generic" },
{ name: "Generic Secret", re: /(?:secret[_-]?key|secretKey|SECRET_KEY|app[_-]?secret|APP_SECRET)\s*[:=]\s*['"`]([A-Za-z0-9_\-/.]{16,120})['"`]/gi, severity: "high", confidence: "medium", provider: "Generic" },
{ name: "Generic Token", re: /(?:access[_-]?token|auth[_-]?token|AUTH_TOKEN|ACCESS_TOKEN)\s*[:=]\s*['"`]([A-Za-z0-9_\-/.]{16,120})['"`]/gi, severity: "high", confidence: "medium", provider: "Generic" },
{ name: "Generic Password", re: /(?:password|passwd|PASSWD|PASSWORD)\s*[:=]\s*['"`]([^\s'"`]{8,120})['"`]/gi, severity: "high", confidence: "low", provider: "Generic" },
{ name: "Credential URL", re: /(?:https?|ftp):\/\/[^\s:@'"]+:[^\s:@'"]+@[^\s'"]+/g, severity: "high", confidence: "medium", provider: "Generic" },
];
-5
View File
File diff suppressed because one or more lines are too long
+74 -40
View File
@@ -1,48 +1,82 @@
var save = document.getElementById('save');
document.addEventListener("DOMContentLoaded", init);
save.onclick = function() {
var keyword = document.getElementById("keyword").value;
if(keyword == "key") {
alert("already exist!");
}
if(keyword === ""){
alert("cannot be empty");
}
if(keyword == localStorage.getItem(keyword)){
alert("you can't add the same word twice!")
}
else
{
keyword.trim();
//setting key value to keyword for easy lopping
localStorage.setItem(keyword,keyword);
}
async function init() {
await renderKeywords();
await renderStats();
document.getElementById("keywordForm").addEventListener("submit", handleAddKeyword);
}
async function renderKeywords() {
const response = await chrome.runtime.sendMessage({ type: "getKeywords" });
const keywords = response.keywords || [];
const list = document.getElementById("keywordList");
list.innerHTML = "";
var i;
for(i=0; i < localStorage.length; i++) {
//regex to filter keywords from localStorage and showing URLS
if(!/(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i.test(localStorage.key(i))){
if(localStorage.key(i) !== 'undefined')
{
$('body').append(`<li>${localStorage.key(i)}<button value=${localStorage.key(i)} class="btn"><i class="fa fa-trash"></i></button></li>`)
}
document.getElementById("keywordCount").textContent = keywords.length;
if (keywords.length === 0) {
list.innerHTML = '<li class="empty-state">No keywords configured</li>';
return;
}
for (const kw of keywords) {
const li = document.createElement("li");
li.className = "keyword-item";
const label = document.createElement("span");
label.className = "keyword-label";
label.textContent = kw;
const removeBtn = document.createElement("button");
removeBtn.className = "keyword-remove";
removeBtn.textContent = "\u00D7";
removeBtn.title = `Remove "${kw}"`;
removeBtn.addEventListener("click", () => handleRemoveKeyword(kw));
li.appendChild(label);
li.appendChild(removeBtn);
list.appendChild(li);
}
}
async function renderStats() {
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
const findings = response.findings || [];
document.getElementById("findingCount").textContent = findings.length;
}
$('.btn').click(function() {
var selectedItem = $(this).val();
localStorage.removeItem(selectedItem);
location.reload();
alert(`${selectedItem} removed`);
})
async function handleAddKeyword(e) {
e.preventDefault();
const input = document.getElementById("keywordInput");
const errorMsg = document.getElementById("errorMsg");
const keyword = input.value.trim();
errorMsg.hidden = true;
if (!keyword) {
showError("Keyword cannot be empty.");
return;
}
const result = await chrome.runtime.sendMessage({ type: "addKeyword", keyword });
if (!result.ok) {
showError(result.error);
return;
}
input.value = "";
await renderKeywords();
}
async function handleRemoveKeyword(keyword) {
await chrome.runtime.sendMessage({ type: "removeKeyword", keyword });
await renderKeywords();
}
function showError(msg) {
const errorMsg = document.getElementById("errorMsg");
errorMsg.textContent = msg;
errorMsg.hidden = false;
setTimeout(() => { errorMsg.hidden = true; }, 3000);
}
+233 -71
View File
@@ -1,87 +1,249 @@
//Searching localStorage for URL
var i;
for(i=0; i < localStorage.length; i++) {
//regex to filter keywords from localStorage and showing URLS
if(/(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i.test(localStorage.key(i))){
$('.table').append(`
<tr>
<td scope="row"></td>
<td>${extractHostname(localStorage.key(i))}</td>
<td> <a href='${localStorage.key(i)}'>${localStorage.key(i)}</a> <button value=${localStorage.key(i)} class="btn"><i class="fa fa-trash"></i></button> </td>
<td>${searchKeywordOnURL(localStorage.key(i))}</td>
<tr>
<style>
let allFindings = [];
`)
document.addEventListener("DOMContentLoaded", init);
async function init() {
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
allFindings = response.findings || [];
populateFilters();
renderStats();
renderFindings();
document.getElementById("severityFilter").addEventListener("change", renderFindings);
document.getElementById("typeFilter").addEventListener("change", renderFindings);
document.getElementById("providerFilter").addEventListener("change", renderFindings);
document.getElementById("searchBox").addEventListener("input", renderFindings);
document.getElementById("exportJsonBtn").addEventListener("click", exportJson);
document.getElementById("exportCsvBtn").addEventListener("click", exportCsv);
document.getElementById("clearBtn").addEventListener("click", clearAll);
}
function getFiltered() {
const severity = document.getElementById("severityFilter").value;
const type = document.getElementById("typeFilter").value;
const provider = document.getElementById("providerFilter").value;
const search = document.getElementById("searchBox").value.toLowerCase();
return allFindings.filter((f) => {
if (severity !== "all" && f.severity !== severity) return false;
if (type !== "all" && f.type !== type) return false;
if (provider !== "all" && f.provider !== provider) return false;
if (search && !JSON.stringify(f).toLowerCase().includes(search)) return false;
return true;
});
}
function populateFilters() {
const types = [...new Set(allFindings.map((f) => f.type))].sort();
const providers = [...new Set(allFindings.map((f) => f.provider))].sort();
const typeSelect = document.getElementById("typeFilter");
for (const t of types) {
const opt = document.createElement("option");
opt.value = t;
opt.textContent = t;
typeSelect.appendChild(opt);
}
const providerSelect = document.getElementById("providerFilter");
for (const p of providers) {
const opt = document.createElement("option");
opt.value = p;
opt.textContent = p;
providerSelect.appendChild(opt);
}
}
function renderStats() {
const bar = document.getElementById("statsBar");
const critical = allFindings.filter((f) => f.severity === "critical").length;
const high = allFindings.filter((f) => f.severity === "high").length;
const medium = allFindings.filter((f) => f.severity === "medium").length;
const low = allFindings.filter((f) => f.severity === "low").length;
const domains = new Set(allFindings.map((f) => f.domain)).size;
function searchKeywordOnURL(key) {
let i;
var keyword;
for(i=0; i < localStorage.length; i++) {
//regex to filter keywords from localStorage and showing URLS
if(!/(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i.test(localStorage.key(i))){
if(key.indexOf(localStorage.key(i)) !== -1) {
return localStorage.key(i);
}
}
bar.innerHTML = "";
const stats = [
{ label: "Total", value: allFindings.length, cls: "stat-total" },
{ label: "Critical", value: critical, cls: "stat-critical" },
{ label: "High", value: high, cls: "stat-high" },
{ label: "Medium", value: medium, cls: "stat-medium" },
{ label: "Low", value: low, cls: "stat-low" },
{ label: "Domains", value: domains, cls: "stat-domains" },
];
for (const s of stats) {
const el = document.createElement("div");
el.className = `stat-item ${s.cls}`;
const num = document.createElement("span");
num.className = "stat-num";
num.textContent = s.value;
const lbl = document.createElement("span");
lbl.className = "stat-lbl";
lbl.textContent = s.label;
el.appendChild(num);
el.appendChild(lbl);
bar.appendChild(el);
}
}
function renderFindings() {
const filtered = getFiltered();
const tbody = document.getElementById("findingsBody");
const empty = document.getElementById("emptyState");
tbody.innerHTML = "";
//numbering results && self-inovking the function
(function(cl){
var table = document.querySelector('table.' + cl)
var trs = table.querySelectorAll('tr')
var counter = 1
if (filtered.length === 0) {
empty.hidden = false;
return;
}
empty.hidden = true;
Array.prototype.forEach.call(trs, function(x,i){
var firstChild = x.children[0]
if (firstChild.tagName === 'TD') {
var cell = document.createElement('td')
cell.textContent = counter ++
x.insertBefore(cell,firstChild)
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
filtered.sort((a, b) => (severityOrder[a.severity] || 5) - (severityOrder[b.severity] || 5));
filtered.forEach((f, i) => {
const tr = document.createElement("tr");
const tdNum = document.createElement("td");
tdNum.textContent = i + 1;
const tdSev = document.createElement("td");
const badge = document.createElement("span");
badge.className = `badge badge-${f.severity || "medium"}`;
badge.textContent = (f.severity || "medium").toUpperCase();
tdSev.appendChild(badge);
const tdProvider = document.createElement("td");
tdProvider.textContent = f.provider || "-";
tdProvider.className = "td-provider";
const tdPattern = document.createElement("td");
tdPattern.textContent = f.patternName || "-";
tdPattern.className = "td-pattern";
const tdMatch = document.createElement("td");
const matchCode = document.createElement("code");
matchCode.textContent = f.match || "-";
matchCode.className = "match-value";
matchCode.title = f.match || "";
tdMatch.appendChild(matchCode);
const tdType = document.createElement("td");
const typeBadge = document.createElement("span");
typeBadge.className = "type-badge";
typeBadge.textContent = f.type || "-";
tdType.appendChild(typeBadge);
const tdDomain = document.createElement("td");
tdDomain.textContent = f.domain || "-";
tdDomain.className = "td-domain";
const tdSource = document.createElement("td");
if (f.url && f.url.startsWith("http")) {
const a = document.createElement("a");
a.href = f.url;
a.target = "_blank";
a.rel = "noopener";
a.textContent = truncateUrl(f.url, 40);
a.title = f.url;
tdSource.appendChild(a);
} else {
firstChild.setAttribute('colspan',2)
}
})
})("table");
//delete
$('.btn').click(function() {
var selectedItem = $(this).val();
localStorage.removeItem(selectedItem);
location.reload();
alert(`${selectedItem} removed`);
})
//extract domain name from URL
function extractHostname(url) {
var hostname;
//find & remove protocol (http, ftp, etc.) and get hostname
if (url.indexOf("//") > -1) {
hostname = url.split('/')[2];
}
else {
hostname = url.split('/')[0];
tdSource.textContent = f.url ? truncateUrl(f.url, 40) : "-";
}
//find & remove port number
hostname = hostname.split(':')[0];
//find & remove "?"
hostname = hostname.split('?')[0];
const tdTime = document.createElement("td");
tdTime.textContent = f.timestamp ? formatTime(f.timestamp) : "-";
tdTime.className = "td-time";
return hostname;
const tdActions = document.createElement("td");
const copyBtn = document.createElement("button");
copyBtn.className = "btn-icon";
copyBtn.textContent = "Copy";
copyBtn.title = "Copy match value";
copyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(f.match || "");
copyBtn.textContent = "Done";
setTimeout(() => (copyBtn.textContent = "Copy"), 1500);
});
const delBtn = document.createElement("button");
delBtn.className = "btn-icon btn-icon-danger";
delBtn.textContent = "Del";
delBtn.title = "Remove finding";
delBtn.addEventListener("click", async () => {
await chrome.runtime.sendMessage({ type: "removeFinding", url: f.url });
allFindings = allFindings.filter((x) => x !== f);
renderStats();
renderFindings();
});
tdActions.appendChild(copyBtn);
tdActions.appendChild(delBtn);
tr.appendChild(tdNum);
tr.appendChild(tdSev);
tr.appendChild(tdProvider);
tr.appendChild(tdPattern);
tr.appendChild(tdMatch);
tr.appendChild(tdType);
tr.appendChild(tdDomain);
tr.appendChild(tdSource);
tr.appendChild(tdTime);
tr.appendChild(tdActions);
tbody.appendChild(tr);
});
}
function truncateUrl(url, max) {
if (url.length <= max) return url;
return url.substring(0, max - 3) + "...";
}
function formatTime(ts) {
const d = new Date(ts);
return d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}
function exportJson() {
const filtered = getFiltered();
const blob = new Blob([JSON.stringify(filtered, null, 2)], { type: "application/json" });
downloadBlob(blob, `keyfinder-findings-${Date.now()}.json`);
}
function exportCsv() {
const filtered = getFiltered();
const headers = ["Severity", "Provider", "Pattern", "Match", "Type", "Domain", "URL", "Page URL", "Timestamp"];
const rows = filtered.map((f) => [
f.severity || "",
f.provider || "",
f.patternName || "",
`"${(f.match || "").replace(/"/g, '""')}"`,
f.type || "",
f.domain || "",
f.url || "",
f.pageUrl || "",
f.timestamp ? new Date(f.timestamp).toISOString() : "",
]);
const csv = [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
const blob = new Blob([csv], { type: "text/csv" });
downloadBlob(blob, `keyfinder-findings-${Date.now()}.csv`);
}
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
async function clearAll() {
if (!confirm("Remove all findings?")) return;
await chrome.runtime.sendMessage({ type: "clearFindings" });
allFindings = [];
renderStats();
renderFindings();
}