Files
keyFinder/js/results.js
T
momenbasel 882e298d85 v2.2.0: ops-console UI redesign
- Popup + findings dashboard rebuilt as a dark ops console: radar-sweep
  brand mark, ARMED/PASSIVE status LEDs, scanline texture, mono chrome,
  amber (action) / teal (secret) semantic colors
- Popup: count-up stats, numbered watchlist, command-prompt input,
  typed terminal status line with live counts
- Dashboard: clickable severity stat filters, LATEST ticker, severity
  row edges, LED badges, click-to-copy match chips, relative times,
  skeleton rows, staggered first paint, radar empty state, / and Esc
  shortcuts, prefers-reduced-motion support
- Fix severity sort (critical sorted last) and ticker hidden override
- Add preview/ harness (stubbed chrome API) for headless UI screenshots
2026-07-18 12:41:58 +03:00

428 lines
14 KiB
JavaScript

let allFindings = [];
let firstRenderDone = false;
const REDUCE_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.addEventListener("DOMContentLoaded", init);
async function init() {
const versionLabel = document.getElementById("versionLabel");
if (versionLabel) versionLabel.textContent = "v" + chrome.runtime.getManifest().version;
renderSkeleton();
const response = await chrome.runtime.sendMessage({ type: "getFindings" });
allFindings = response.findings || [];
populateFilters();
renderStats();
renderFindings();
renderTicker();
firstRenderDone = true;
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);
const searchBox = document.getElementById("searchBox");
document.addEventListener("keydown", (e) => {
const tag = document.activeElement && document.activeElement.tagName;
const typing = tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA";
if (e.key === "/" && !typing) {
e.preventDefault();
searchBox.focus();
} else if (e.key === "Escape" && document.activeElement === searchBox) {
searchBox.value = "";
renderFindings();
searchBox.blur();
}
});
}
/* eased count-up for stat numerals */
function setNumber(el, target) {
if (!el) return;
const from = parseInt(el.textContent, 10);
const start = Number.isFinite(from) ? from : 0;
if (REDUCE_MOTION || start === target) {
el.textContent = target;
return;
}
const duration = 400;
const t0 = performance.now();
(function tick(t) {
const p = Math.min(1, (t - t0) / duration);
const eased = 1 - Math.pow(1 - p, 3);
el.textContent = Math.round(start + (target - start) * eased);
if (p < 1) requestAnimationFrame(tick);
})(t0);
}
function renderSkeleton() {
const tbody = document.getElementById("findingsBody");
tbody.classList.remove("fresh");
tbody.innerHTML = "";
const widths = ["skl-w2", "skl-w1", "skl-w3", "skl-w4", "skl-w2", "skl-w3"];
for (let i = 0; i < 6; i++) {
const tr = document.createElement("tr");
tr.className = "skl-row";
const td = document.createElement("td");
td.colSpan = 10;
const bar = document.createElement("div");
bar.className = "skl " + widths[i % widths.length];
td.appendChild(bar);
tr.appendChild(td);
tbody.appendChild(tr);
}
}
function getFiltered() {
const severity = document.getElementById("severityFilter").value;
const type = document.getElementById("typeFilter").value;
const provider = document.getElementById("providerFilter").value;
const search = document.getElementById("searchBox").value.toLowerCase();
return allFindings.filter((f) => {
if (severity !== "all" && f.severity !== severity) return false;
if (type !== "all" && f.type !== type) return false;
if (provider !== "all" && f.provider !== provider) return false;
if (search && !JSON.stringify(f).toLowerCase().includes(search)) return false;
return true;
});
}
function populateFilters() {
const types = [...new Set(allFindings.map((f) => f.type))].sort();
const providers = [...new Set(allFindings.map((f) => f.provider))].sort();
const typeSelect = document.getElementById("typeFilter");
for (const t of types) {
const opt = document.createElement("option");
opt.value = t;
opt.textContent = t;
typeSelect.appendChild(opt);
}
const providerSelect = document.getElementById("providerFilter");
for (const p of providers) {
const opt = document.createElement("option");
opt.value = p;
opt.textContent = p;
providerSelect.appendChild(opt);
}
}
function renderStats() {
const bar = document.getElementById("statsBar");
const critical = allFindings.filter((f) => f.severity === "critical").length;
const high = allFindings.filter((f) => f.severity === "high").length;
const medium = allFindings.filter((f) => f.severity === "medium").length;
const low = allFindings.filter((f) => f.severity === "low").length;
const domains = new Set(allFindings.map((f) => f.domain)).size;
bar.innerHTML = "";
const stats = [
{ label: "Total", value: allFindings.length, cls: "stat-total", filter: "all" },
{ label: "Critical", value: critical, cls: "stat-critical", filter: "critical" },
{ label: "High", value: high, cls: "stat-high", filter: "high" },
{ label: "Medium", value: medium, cls: "stat-medium", filter: "medium" },
{ label: "Low", value: low, cls: "stat-low", filter: "low" },
{ label: "Domains", value: domains, cls: "stat-domains", filter: null },
];
for (const s of stats) {
const el = document.createElement("div");
el.className = `stat-item ${s.cls}`;
const top = document.createElement("span");
top.className = "stat-top";
const led = document.createElement("span");
led.className = "stat-led";
const lbl = document.createElement("span");
lbl.className = "stat-lbl";
lbl.textContent = s.label;
top.appendChild(led);
top.appendChild(lbl);
const num = document.createElement("span");
num.className = "stat-num";
el.appendChild(top);
el.appendChild(num);
setNumber(num, s.value);
if (s.filter) {
el.dataset.filter = s.filter;
el.title = s.filter === "all" ? "Show all severities" : `Filter: ${s.label.toLowerCase()} only`;
el.addEventListener("click", () => {
document.getElementById("severityFilter").value = s.filter;
renderFindings();
});
}
bar.appendChild(el);
}
}
function syncStatActive() {
const current = document.getElementById("severityFilter").value;
document.querySelectorAll(".stat-item[data-filter]").forEach((el) => {
el.classList.toggle("active", el.dataset.filter === current);
});
}
function renderFindings() {
const filtered = getFiltered();
const tbody = document.getElementById("findingsBody");
const empty = document.getElementById("emptyState");
tbody.innerHTML = "";
tbody.classList.toggle("fresh", !firstRenderDone && !REDUCE_MOTION);
updateMeta(filtered.length);
syncStatActive();
if (filtered.length === 0) {
const title = document.getElementById("emptyTitle");
const copy = document.getElementById("emptyCopy");
if (allFindings.length === 0) {
title.textContent = "No findings yet";
copy.textContent = "KeyFinder passively scans every page you visit. Browse around — leaked keys, tokens and credentials will surface here.";
} else {
title.textContent = "No matches in view";
copy.textContent = "The current filters hide every finding. Adjust or clear them to bring results back.";
}
empty.hidden = false;
return;
}
empty.hidden = true;
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
filtered.sort((a, b) => (severityOrder[a.severity] ?? 5) - (severityOrder[b.severity] ?? 5));
filtered.forEach((f, i) => {
const sev = f.severity || "medium";
const tr = document.createElement("tr");
tr.className = "sev-" + sev;
if (i < 30) tr.style.setProperty("--i", i);
const tdNum = document.createElement("td");
tdNum.textContent = String(i + 1).padStart(2, "0");
tdNum.className = "td-num";
const tdSev = document.createElement("td");
const badge = document.createElement("span");
badge.className = `badge badge-${sev}`;
badge.textContent = sev.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 = "click to copy\n" + (f.match || "");
matchCode.addEventListener("click", () => {
navigator.clipboard.writeText(f.match || "");
flashCopied(copyBtn);
});
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");
tdSource.className = "td-source";
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, 30);
a.title = f.url;
tdSource.appendChild(a);
} else {
tdSource.textContent = f.url ? truncateUrl(f.url, 30) : "-";
}
const tdTime = document.createElement("td");
tdTime.textContent = f.timestamp ? formatTime(f.timestamp) : "-";
tdTime.className = "td-time";
if (f.timestamp) tdTime.title = new Date(f.timestamp).toLocaleString();
const tdActions = document.createElement("td");
tdActions.className = "td-actions";
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 || "");
flashCopied(copyBtn);
});
const delBtn = document.createElement("button");
delBtn.className = "btn-icon btn-icon-danger";
delBtn.textContent = "del";
delBtn.title = "Remove finding";
delBtn.addEventListener("click", async () => {
await chrome.runtime.sendMessage({ type: "removeFinding", findingId: f.id });
allFindings = allFindings.filter((x) => x.id !== f.id);
renderStats();
renderFindings();
renderTicker();
});
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 flashCopied(btn) {
if (!btn) return;
btn.textContent = "copied";
btn.classList.add("copied");
setTimeout(() => {
btn.textContent = "copy";
btn.classList.remove("copied");
}, 1200);
}
function updateMeta(shown) {
const metaLine = document.getElementById("metaLine");
if (metaLine) {
metaLine.innerHTML = "";
metaLine.append(
"showing ",
strong(shown),
" of ",
strong(allFindings.length)
);
}
const shellMeta = document.getElementById("shellMeta");
if (shellMeta) shellMeta.textContent = shown + (shown === 1 ? " row" : " rows");
}
function strong(n) {
const el = document.createElement("strong");
el.textContent = n;
return el;
}
function renderTicker() {
const ticker = document.getElementById("ticker");
const tickerText = document.getElementById("tickerText");
if (!ticker || !tickerText) return;
if (!allFindings.length) {
ticker.hidden = true;
return;
}
const latest = [...allFindings].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0];
tickerText.innerHTML = "";
const head = document.createElement("span");
head.textContent = `${latest.provider || "unknown"} · ${latest.patternName || "match"} @ ${latest.domain || "—"} `;
tickerText.appendChild(head);
if (latest.match) {
const code = document.createElement("code");
code.textContent = truncateUrl(latest.match, 28);
tickerText.appendChild(code);
}
ticker.hidden = false;
}
function truncateUrl(url, max) {
if (url.length <= max) return url;
return url.substring(0, max - 3) + "...";
}
function formatTime(ts) {
const d = new Date(ts);
const diff = Date.now() - d.getTime();
const m = Math.floor(diff / 60000);
if (m < 1) return "just now";
if (m < 60) return m + "m ago";
const h = Math.floor(m / 60);
if (h < 24) return h + "h ago";
const days = Math.floor(h / 24);
if (days < 7) return days + "d ago";
return d.toLocaleDateString();
}
function exportJson() {
const filtered = getFiltered();
const blob = new Blob([JSON.stringify(filtered, null, 2)], { type: "application/json" });
downloadBlob(blob, `keyfinder-findings-${Date.now()}.json`);
}
function csvSafe(value) {
let str = String(value || "");
if (/^[=+\-@\t\r\n]/.test(str)) str = "'" + str;
str = str.replace(/"/g, '""');
return `"${str}"`;
}
function exportCsv() {
const filtered = getFiltered();
const headers = ["Severity", "Provider", "Pattern", "Match", "Type", "Domain", "URL", "Page URL", "Timestamp"];
const rows = filtered.map((f) => [
csvSafe(f.severity),
csvSafe(f.provider),
csvSafe(f.patternName),
csvSafe(f.match),
csvSafe(f.type),
csvSafe(f.domain),
csvSafe(f.url),
csvSafe(f.pageUrl),
csvSafe(f.timestamp ? new Date(f.timestamp).toISOString() : ""),
]);
const csv = [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
const blob = new Blob([csv], { type: "text/csv" });
downloadBlob(blob, `keyfinder-findings-${Date.now()}.csv`);
}
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
async function clearAll() {
if (!confirm("Remove all findings?")) return;
await chrome.runtime.sendMessage({ type: "clearFindings" });
allFindings = [];
renderStats();
renderFindings();
renderTicker();
}