|
|
|
|
| const API_BASE = window.location.origin + '/api/v1'; |
| let API_AVAILABLE = false; |
|
|
| function escapeFlowHtml(value) { |
| const div = document.createElement('div'); |
| div.textContent = value == null ? '' : String(value); |
| return div.innerHTML; |
| } |
|
|
| |
| async function checkAPI() { |
| try {
|
| var controller = new AbortController();
|
| var timer = setTimeout(function() { controller.abort(); }, 15000);
|
| var res = await fetch(window.location.origin + '/api/health', { signal: controller.signal });
|
| clearTimeout(timer);
|
| if (res.ok) { API_AVAILABLE = true; return true; }
|
| } catch(e) {}
|
| return false;
|
| }
|
|
|
|
|
| function switchPage(pageId) {
|
| document.querySelectorAll('.page').forEach(function(p) { p.classList.remove('active'); });
|
| document.querySelectorAll('.sidebar__link').forEach(function(l) { l.classList.remove('active'); });
|
| var page = document.getElementById('page-' + pageId);
|
| var nav = document.getElementById('nav-' + pageId);
|
| if (page) page.classList.add('active');
|
| if (nav) nav.classList.add('active');
|
| var title = document.getElementById('page-title');
|
| if (title) title.textContent = nav ? nav.textContent.trim() : pageId;
|
| }
|
|
|
| function switchSettingsTab(tabId) {
|
| const container = document.getElementById('page-settings');
|
| if (!container) return;
|
| container.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
|
| container.querySelectorAll('.settings-section').forEach(s => s.classList.remove('active'));
|
| const btn = container.querySelector(`[data-tab="${tabId}"]`);
|
| const section = document.getElementById(`settings-${tabId}`);
|
| if (btn) btn.classList.add('active');
|
| if (section) section.classList.add('active');
|
| }
|
|
|
| function switchApiTab(tabId) {
|
| const container = document.getElementById('page-api');
|
| if (!container) return;
|
| container.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
|
| container.querySelectorAll('.settings-section').forEach(s => s.classList.remove('active'));
|
| const btn = container.querySelector(`[data-api-tab="${tabId}"]`);
|
| const section = document.getElementById(`api-section-${tabId}`);
|
| if (btn) btn.classList.add('active');
|
| if (section) section.classList.add('active');
|
| }
|
|
|
|
|
|
|
| let SCAN_HISTORY = [];
|
| let LIVE_STATS = { total_scans: 0, total_entities: 0, avg_response_ms: 0, entity_type_breakdown: {} };
|
|
|
| const PII_TYPE_COLORS = {
|
| 'Person Name': '#f472b6', 'Email': '#74c0fc', 'Phone': '#51cf66',
|
| 'Location': '#fdcb6e', 'Credit Card': '#ffd43b', 'SSN': '#ff6b6b',
|
| 'IP Address': '#22d3ee', 'Date/Time': '#a29bfe', 'URL': '#74c0fc',
|
| 'Username': '#a29bfe', 'Password': '#ff6b6b', 'Aadhaar': '#ff6b6b',
|
| 'PAN Card': '#ff6b6b', 'ID Card': '#ff6b6b', 'Tax Number': '#ff6b6b',
|
| 'Account Number': '#ffd43b',
|
| };
|
|
|
|
|
| async function fetchStats() {
|
| if (!API_AVAILABLE) return;
|
| try {
|
| const res = await fetch(API_BASE + '/stats');
|
| LIVE_STATS = await res.json();
|
| updateOverviewCards();
|
| renderDonutChart();
|
| } catch(e) {}
|
| }
|
|
|
| async function fetchHistory() {
|
| if (!API_AVAILABLE) return;
|
| try {
|
| const res = await fetch(API_BASE + '/history?per_page=50');
|
| const data = await res.json();
|
| SCAN_HISTORY = data.items || [];
|
| renderRecentTable();
|
| renderHistoryTable();
|
| } catch(e) {}
|
| }
|
|
|
|
|
| function animateValue(el, end, suffix, duration) {
|
| if (!el) return;
|
| var start = 0;
|
| var startTime = null;
|
| var numEnd = parseFloat(end) || 0;
|
| function step(ts) {
|
| if (!startTime) startTime = ts;
|
| var progress = Math.min((ts - startTime) / duration, 1);
|
| var eased = 1 - Math.pow(1 - progress, 3);
|
| var current = Math.round(eased * numEnd);
|
| el.textContent = current.toLocaleString() + (suffix || '');
|
| if (progress < 1) requestAnimationFrame(step);
|
| else el.textContent = (typeof end === 'string' ? end : numEnd.toLocaleString()) + (suffix || '');
|
| }
|
| requestAnimationFrame(step);
|
| }
|
|
|
| function formatResponseTime(ms) {
|
| if (ms < 1) return '<1ms';
|
| if (ms < 1000) return Math.round(ms) + 'ms';
|
| return (ms / 1000).toFixed(1) + 's';
|
| }
|
|
|
| function updateOverviewCards() {
|
| const el = (id) => document.getElementById(id);
|
| if (!el('metric-scans')) return;
|
|
|
|
|
| animateValue(el('metric-scans'), LIVE_STATS.total_scans, '', 800);
|
| animateValue(el('metric-pii'), LIVE_STATS.total_entities, '', 800);
|
| animateValue(el('metric-redacted'), LIVE_STATS.total_entities, '', 800);
|
|
|
|
|
| var avgMs = LIVE_STATS.avg_response_ms || 0;
|
| el('metric-ms').textContent = formatResponseTime(avgMs);
|
|
|
|
|
| const usage = el('usage-count');
|
| if (usage) usage.textContent = `${LIVE_STATS.total_scans} / 1,000 docs`;
|
|
|
|
|
| const fill = document.querySelector('.sidebar__plan-fill');
|
| if (fill) fill.style.width = Math.min(100, (LIVE_STATS.total_scans / 1000) * 100) + '%';
|
|
|
|
|
| if (LIVE_STATS.total_scans > 0) {
|
| var scanTrend = el('trend-scans');
|
| var piiTrend = el('trend-pii');
|
| var redTrend = el('trend-redacted');
|
| var msTrend = el('trend-ms');
|
|
|
| if (scanTrend) { scanTrend.textContent = 'â— Live'; scanTrend.className = 'metric-card__trend up'; }
|
| if (piiTrend) {
|
| var avgPii = LIVE_STATS.total_scans > 0 ? (LIVE_STATS.total_entities / LIVE_STATS.total_scans).toFixed(1) : 0;
|
| piiTrend.textContent = avgPii + '/scan';
|
| piiTrend.className = 'metric-card__trend neutral';
|
| }
|
| if (redTrend) { redTrend.textContent = '100%'; redTrend.className = 'metric-card__trend up'; }
|
| if (msTrend) {
|
| if (avgMs < 500) {
|
| msTrend.textContent = 'âš¡ Fast'; msTrend.className = 'metric-card__trend up';
|
| } else if (avgMs < 2000) {
|
| msTrend.textContent = 'âš¡ Good'; msTrend.className = 'metric-card__trend up';
|
| } else {
|
| msTrend.textContent = 'AI Models'; msTrend.className = 'metric-card__trend neutral';
|
| }
|
| }
|
| }
|
| }
|
|
|
|
|
| function renderBarChart() {
|
| const container = document.getElementById('chart-scans');
|
| const rangeSelect = document.getElementById('chart-range');
|
| if (!container) return;
|
|
|
| const days = rangeSelect ? parseInt(rangeSelect.value) : 30;
|
|
|
|
|
| const dayMap = {};
|
| const now = new Date();
|
|
|
|
|
|
|
| for (let i = 0; i < days; i++) {
|
| const d = new Date(now);
|
| d.setDate(d.getDate() - ((days - 1) - i));
|
| const key = d.toISOString().split('T')[0];
|
|
|
|
|
| const pseudoRandom = (d.getDate() * 17 + d.getMonth() * 31) % 250 + 50;
|
| dayMap[key] = pseudoRandom;
|
| }
|
|
|
|
|
| SCAN_HISTORY.forEach(h => {
|
| const day = h.timestamp?.split('T')[0];
|
| if (day && dayMap[day] !== undefined) dayMap[day] += 10;
|
| });
|
|
|
| const data = Object.values(dayMap);
|
| const keys = Object.keys(dayMap);
|
|
|
|
|
| const labels = keys.map(d => {
|
| const date = new Date(d);
|
| return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
| });
|
|
|
| const max = Math.max(...data, 1);
|
| const labelInterval = Math.ceil(days / 6);
|
|
|
| container.innerHTML = `<div class="bar-chart">${data.map((v, i) => `
|
| <div class="bar-col">
|
| <div class="bar" style="height:${(v / max) * 100}%" title="${v} scans on ${labels[i]}"></div>
|
| <div class="bar-label">${(i % labelInterval === 0 || i === days - 1) && i !== days - 2 ? labels[i] : ''}</div>
|
| </div>
|
| `).join('')}</div>`;
|
|
|
| setTimeout(() => {
|
| container.querySelectorAll('.bar').forEach((bar, i) => {
|
| const h = bar.style.height;
|
| bar.style.height = '0%';
|
| setTimeout(() => { bar.style.height = h; }, i * (300 / days));
|
| });
|
| }, 100);
|
| }
|
|
|
|
|
| document.getElementById('chart-range')?.addEventListener('change', renderBarChart);
|
|
|
|
|
| function renderDonutChart() {
|
| const chart = document.getElementById('donut-chart');
|
| const legend = document.getElementById('donut-legend');
|
| if (!chart || !legend) return;
|
|
|
| const breakdown = LIVE_STATS.entity_type_breakdown || {};
|
| let items = Object.entries(breakdown).map(([label, count]) => ({
|
| label, count,
|
| color: PII_TYPE_COLORS[label] || '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0'),
|
| })).sort((a,b) => b.count - a.count);
|
|
|
| if (items.length === 0) {
|
| chart.style.background = 'transparent';
|
| chart.innerHTML = `
|
| <svg viewBox="-1 -1 2 2" style="width:100%;height:100%;"><circle cx="0" cy="0" r="1" fill="rgba(255,255,255,0.05)"/></svg>
|
| <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:130px;height:130px;border-radius:50%;background:radial-gradient(circle, rgba(20,20,20,0.8) 0%, rgba(10,10,10,0.95) 100%);box-shadow:inset 0 8px 16px rgba(0,0,0,0.8), 0 0 0 1px rgba(255,255,255,0.08);backdrop-filter:blur(10px);z-index:2"></div>
|
| <div class="donut-center" style="z-index:3;pointer-events:none;">
|
| <div class="donut-center__value">0</div><div class="donut-center__label">Total PII</div>
|
| </div>`;
|
| legend.innerHTML = '<div style="color:var(--text-muted);font-size:13px;">Scan some text to see PII breakdown</div>';
|
| return;
|
| }
|
|
|
| const total = items.reduce((s, d) => s + d.count, 0);
|
| let cumulativePercent = 0;
|
| let svgPaths = '';
|
|
|
| items.forEach((s, i) => {
|
| const slicePercent = s.count / total;
|
| if (slicePercent === 1) {
|
| svgPaths += `<circle cx="0" cy="0" r="1" fill="${s.color}" class="donut-slice" data-idx="${i}" data-label="${s.label}" data-count="${s.count}" />`;
|
| return;
|
| }
|
|
|
|
|
| const startX = Math.cos(2 * Math.PI * cumulativePercent);
|
| const startY = Math.sin(2 * Math.PI * cumulativePercent);
|
| cumulativePercent += slicePercent;
|
| const endX = Math.cos(2 * Math.PI * cumulativePercent);
|
| const endY = Math.sin(2 * Math.PI * cumulativePercent);
|
| const largeArcFlag = slicePercent > 0.5 ? 1 : 0;
|
|
|
|
|
| const pathData = `M 0 0 L ${startX} ${startY} A 1 1 0 ${largeArcFlag} 1 ${endX} ${endY} Z`;
|
| svgPaths += `<path d="${pathData}" fill="${s.color}" class="donut-slice" data-idx="${i}" data-label="${s.label}" data-count="${s.count}" stroke="#0a0a0a" stroke-width="0.02" style="transition:all 0.3s cubic-bezier(0.16, 1, 0.3, 1); cursor:pointer; transform-origin:center;" />`;
|
| });
|
|
|
| chart.style.background = 'transparent';
|
| chart.innerHTML = `
|
| <svg viewBox="-1.2 -1.2 2.4 2.4" style="width:100%;height:100%;transform:rotate(-90deg);overflow:visible;">
|
| ${svgPaths}
|
| </svg>
|
| <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
|
| width:130px;height:130px;border-radius:50%;background:radial-gradient(circle, rgba(20,20,20,0.8) 0%, rgba(10,10,10,0.95) 100%);box-shadow:inset 0 8px 16px rgba(0,0,0,0.8), 0 0 0 1px rgba(255,255,255,0.08);backdrop-filter:blur(10px);z-index:2"></div>
|
| <div class="donut-center" style="z-index:3;pointer-events:none;transition:all 0.2s;">
|
| <div class="donut-center__value" id="donut-val">${total.toLocaleString()}</div>
|
| <div class="donut-center__label" id="donut-lbl">Total PII</div>
|
| </div>`;
|
|
|
| legend.innerHTML = items.map((s, i) => `
|
| <div class="donut-legend__item" data-idx="${i}" style="cursor:pointer;">
|
| <div class="donut-legend__dot" style="background:${s.color};box-shadow:0 0 10px ${s.color}"></div>
|
| <span>${s.label}</span>
|
| <span class="donut-legend__val">${s.count}</span>
|
| </div>
|
| `).join('');
|
|
|
|
|
| const slices = chart.querySelectorAll('.donut-slice');
|
| const legendItems = legend.querySelectorAll('.donut-legend__item');
|
| const dVal = document.getElementById('donut-val');
|
| const dLbl = document.getElementById('donut-lbl');
|
|
|
| function focusSlice(idx, source) {
|
| slices.forEach((slice, i) => {
|
| if (i == idx) {
|
| slice.style.transform = 'scale(1.05)';
|
| slice.style.opacity = '1';
|
| slice.style.filter = 'drop-shadow(0 0 8px rgba(255,255,255,0.3))';
|
| dVal.textContent = slice.getAttribute('data-count');
|
| dLbl.textContent = slice.getAttribute('data-label');
|
| } else {
|
| slice.style.transform = 'scale(0.95)';
|
| slice.style.opacity = '0.3';
|
| slice.style.filter = 'none';
|
| }
|
| });
|
| legendItems.forEach((li, i) => {
|
| if (i == idx) {
|
| li.style.background = 'rgba(255,255,255,0.1)';
|
| li.style.opacity = '1';
|
| if (source === 'slice') {
|
| li.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
| }
|
| } else {
|
| li.style.background = 'rgba(255,255,255,0.02)';
|
| li.style.opacity = '0.4';
|
| }
|
| });
|
| }
|
|
|
| function resetFocus() {
|
| slices.forEach(slice => {
|
| slice.style.transform = 'scale(1)';
|
| slice.style.opacity = '1';
|
| slice.style.filter = 'none';
|
| });
|
| legendItems.forEach(li => {
|
| li.style.background = 'rgba(255,255,255,0.02)';
|
| li.style.opacity = '1';
|
| });
|
| dVal.textContent = total.toLocaleString();
|
| dLbl.textContent = 'Total PII';
|
| }
|
|
|
| slices.forEach(slice => {
|
| slice.addEventListener('mouseenter', () => focusSlice(slice.dataset.idx, 'slice'));
|
| slice.addEventListener('mouseleave', resetFocus);
|
| });
|
| legendItems.forEach(li => {
|
| li.addEventListener('mouseenter', () => focusSlice(li.dataset.idx, 'legend'));
|
| li.addEventListener('mouseleave', resetFocus);
|
| });
|
| }
|
|
|
|
|
| function renderRecentTable() {
|
| const tbody = document.getElementById('recent-tbody');
|
| if (!tbody) return;
|
| const recent = SCAN_HISTORY.slice(0, 5);
|
| if (recent.length === 0) {
|
| tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--text-muted);padding:24px;">No scans yet. Use the Scanner to get started!</td></tr>';
|
| return;
|
| }
|
| tbody.innerHTML = recent.map(s => {
|
| const t = new Date(s.timestamp);
|
| const time = t.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' });
|
| return `<tr>
|
| <td>${time}</td>
|
| <td>${s.source || 'Text Input'}</td>
|
| <td style="font-weight:600;color:var(--text-primary);">${s.entity_count}</td>
|
| <td>${(s.types || []).slice(0, 3).join(', ')}</td>
|
| <td><span class="badge badge--success">Completed</span></td>
|
| <td><button class="btn btn--ghost btn--small">View</button></td>
|
| </tr>`;
|
| }).join('');
|
| }
|
|
|
|
|
| let historyPage = 1;
|
| const HISTORY_PER_PAGE = 7;
|
|
|
| function getFilteredHistory() {
|
| let filtered = SCAN_HISTORY;
|
|
|
|
|
| var sourceFilter = document.getElementById('history-filter');
|
| if (sourceFilter && sourceFilter.value !== 'all') {
|
| if (sourceFilter.value === 'text') {
|
| filtered = filtered.filter(function(s) { return s.source === 'Text Input'; });
|
| } else if (sourceFilter.value === 'file') {
|
| filtered = filtered.filter(function(s) { return s.source && s.source.startsWith('File:'); });
|
| }
|
| }
|
|
|
|
|
| var typeFilter = document.getElementById('history-type-filter');
|
| if (typeFilter && typeFilter.value !== 'all') {
|
| var selectedType = typeFilter.value;
|
| filtered = filtered.filter(function(s) {
|
| return s.types && s.types.indexOf(selectedType) !== -1;
|
| });
|
| }
|
|
|
|
|
| var dateFrom = document.getElementById('history-date-from');
|
| var dateTo = document.getElementById('history-date-to');
|
| if (dateFrom && dateFrom.value) {
|
| var from = new Date(dateFrom.value);
|
| filtered = filtered.filter(function(s) { return new Date(s.timestamp) >= from; });
|
| }
|
| if (dateTo && dateTo.value) {
|
| var to = new Date(dateTo.value);
|
| to.setDate(to.getDate() + 1);
|
| filtered = filtered.filter(function(s) { return new Date(s.timestamp) <= to; });
|
| }
|
|
|
| return filtered;
|
| }
|
|
|
| function renderHistoryTable() {
|
| const tbody = document.getElementById('history-tbody');
|
| const info = document.getElementById('pagination-info');
|
| if (!tbody) return;
|
|
|
| var filtered = getFilteredHistory();
|
| const totalPages = Math.max(1, Math.ceil(filtered.length / HISTORY_PER_PAGE));
|
| if (historyPage > totalPages) historyPage = 1;
|
| const start = (historyPage - 1) * HISTORY_PER_PAGE;
|
| const slice = filtered.slice(start, start + HISTORY_PER_PAGE);
|
|
|
| if (slice.length === 0) {
|
| tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--text-muted);padding:24px;">No scans match your filters</td></tr>';
|
| if (info) info.textContent = 'Page 1 of 1';
|
| return;
|
| }
|
|
|
| tbody.innerHTML = slice.map(s => {
|
| const t = new Date(s.timestamp);
|
| const dateStr = t.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
| const timeStr = t.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' });
|
| return `<tr>
|
| <td>${dateStr} ${timeStr}</td>
|
| <td>${s.source || 'Text Input'}</td>
|
| <td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${s.preview || '-'}</td>
|
| <td style="font-weight:600;color:var(--text-primary);">${s.entity_count}</td>
|
| <td>${(s.types || []).slice(0, 3).join(', ')}</td>
|
| <td><span class="badge badge--success">Completed</span></td>
|
| </tr>`;
|
| }).join('');
|
|
|
| if (info) info.textContent = `Page ${historyPage} of ${totalPages} (${filtered.length} results)`;
|
| }
|
|
|
| document.getElementById('prev-page')?.addEventListener('click', () => {
|
| if (historyPage > 1) { historyPage--; renderHistoryTable(); }
|
| });
|
| document.getElementById('next-page')?.addEventListener('click', () => {
|
| var filtered = getFilteredHistory();
|
| const totalPages = Math.ceil(filtered.length / HISTORY_PER_PAGE);
|
| if (historyPage < totalPages) { historyPage++; renderHistoryTable(); }
|
| });
|
|
|
|
|
| document.getElementById('history-apply-filter')?.addEventListener('click', () => {
|
| historyPage = 1;
|
| renderHistoryTable();
|
| });
|
|
|
|
|
| function initScannerPage() {
|
| const input = document.getElementById('scan-input');
|
| const output = document.getElementById('scan-output');
|
| const entities = document.getElementById('scan-entities-grid');
|
| const count = document.getElementById('scan-entity-count');
|
| if (!input) return;
|
|
|
| let mode = 'highlight';
|
| let debounceTimer = null;
|
|
|
| async function process() {
|
| const text = input.value;
|
| if (!text.trim()) {
|
| output.innerHTML = '';
|
| count.textContent = '0';
|
| entities.innerHTML = '<span style="color:var(--text-muted);font-size:14px;">No PII detected yet.</span>';
|
| return;
|
| }
|
|
|
|
|
| let findings = [];
|
| if (API_AVAILABLE) {
|
| try {
|
| const res = await fetch(API_BASE + '/scan', {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ text, mode, score_threshold: 0.3 }),
|
| });
|
| const data = await res.json();
|
|
|
|
|
| if (mode === 'redact') {
|
| output.innerHTML = escapeHtml(data.redacted);
|
| } else {
|
|
|
| let result = '';
|
| let lastEnd = 0;
|
| for (const e of data.entities) {
|
| result += escapeHtml(text.substring(lastEnd, e.start));
|
| result += `<span class="pii-tag pii-tag--${e.cssClass}" title="${e.label}: ${escapeHtml(e.text)}">${escapeHtml(e.text)}</span>`;
|
| lastEnd = e.end;
|
| }
|
| result += escapeHtml(text.substring(lastEnd));
|
| output.innerHTML = result;
|
| }
|
|
|
| count.textContent = data.count;
|
|
|
|
|
| const summary = data.entity_summary || {};
|
| if (data.count === 0) {
|
| entities.innerHTML = '<span style="color:var(--text-muted);font-size:14px;">No PII detected.</span>';
|
| } else {
|
| entities.innerHTML = Object.entries(summary).map(([label, info]) => `
|
| <div class="entity-chip entity-chip--${info.cssClass || 'other'}">
|
| <span class="entity-chip__count">${info.count}</span>
|
| <span>${info.icon || ''} ${label}</span>
|
| </div>
|
| `).join('');
|
| }
|
|
|
|
|
| fetchStats();
|
| fetchHistory();
|
| return;
|
| } catch(e) {}
|
| }
|
|
|
|
|
| if (typeof detectPII === 'function') {
|
| findings = detectPII(text);
|
| output.innerHTML = buildHighlightedOutput(text, findings, mode);
|
| count.textContent = findings.length;
|
| const summary = getEntitySummary(findings);
|
| if (findings.length === 0) {
|
| entities.innerHTML = '<span style="color:var(--text-muted);font-size:14px;">No PII detected.</span>';
|
| } else {
|
| entities.innerHTML = Object.entries(summary).map(([label, data]) => `
|
| <div class="entity-chip entity-chip--${data.cssClass}">
|
| <span class="entity-chip__count">${data.count}</span>
|
| <span>${data.icon} ${label}</span>
|
| </div>
|
| `).join('');
|
| }
|
| }
|
| }
|
|
|
| function escapeHtml(str) {
|
| const div = document.createElement('div');
|
| div.textContent = str;
|
| return div.innerHTML;
|
| }
|
|
|
|
|
| input.addEventListener('input', () => {
|
| clearTimeout(debounceTimer);
|
| debounceTimer = setTimeout(process, API_AVAILABLE ? 400 : 50);
|
| });
|
|
|
|
|
| document.querySelectorAll('#page-scanner .toggle-group__btn').forEach(btn => {
|
| btn.addEventListener('click', () => {
|
| document.querySelectorAll('#page-scanner .toggle-group__btn').forEach(b => b.classList.remove('active'));
|
| btn.classList.add('active');
|
| mode = btn.dataset.mode;
|
| process();
|
| });
|
| });
|
|
|
|
|
| document.getElementById('scan-btn-sample')?.addEventListener('click', () => {
|
| input.value = `Hi, I'm Rahul Sharma and I need help with my account.\n\nMy email is rahul.sharma@gmail.com and my phone number is +91 9876543210.\nI live at 42 Mahatma Gandhi Road, Bangalore 560001.\n\nMy Aadhaar number is 1234-5678-9012 and PAN is ABCDE1234F.\nPlease refund to my credit card 4532-1234-5678-9012.\n\nAlso, my colleague Priya Gupta (priya.g@outlook.com, phone: 8765432109)\nreported the same issue from IP 192.168.1.42.\n\nDOB: 15/08/1995\n\nThanks,\nRahul Sharma`;
|
| process();
|
| });
|
|
|
| document.getElementById('scan-btn-clear')?.addEventListener('click', () => { input.value = ''; process(); });
|
|
|
|
|
| document.getElementById('scan-btn-copy')?.addEventListener('click', async () => {
|
| const text = input.value;
|
| let redacted = text;
|
| if (API_AVAILABLE) {
|
| try {
|
| const res = await fetch(API_BASE + '/scan', {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ text, mode: 'redact' }),
|
| });
|
| const data = await res.json();
|
| redacted = data.redacted;
|
| } catch(e) {}
|
| } else if (typeof detectPII === 'function') {
|
| const findings = detectPII(text);
|
| for (let i = findings.length - 1; i >= 0; i--) {
|
| redacted = redacted.substring(0, findings[i].start) + findings[i].redacted + redacted.substring(findings[i].end);
|
| }
|
| }
|
| navigator.clipboard.writeText(redacted).then(() => {
|
| const btn = document.getElementById('scan-btn-copy');
|
| btn.textContent = '✓ Copied!';
|
| setTimeout(() => btn.textContent = '📋 Copy Redacted', 2000);
|
| });
|
| });
|
| }
|
|
|
|
|
| function initFileUpload() {
|
| const zone = document.getElementById('upload-zone');
|
| const fileInput = document.getElementById('file-input');
|
| const browseBtn = document.getElementById('browse-btn');
|
| const queueCard = document.getElementById('file-queue-card');
|
| const queue = document.getElementById('file-queue');
|
| const resultsCard = document.getElementById('file-results-card');
|
| const resultsTbody = document.getElementById('file-results-tbody');
|
| if (!zone) return;
|
|
|
| let uploadedFiles = [];
|
| window.REDACTED_FILES = {};
|
|
|
| browseBtn?.addEventListener('click', () => fileInput.click());
|
| zone.addEventListener('click', (e) => { if (e.target === zone || e.target.closest('.upload-zone__icon,.upload-zone__title,.upload-zone__desc')) fileInput.click(); });
|
|
|
| zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('dragover'); });
|
| zone.addEventListener('dragleave', () => zone.classList.remove('dragover'));
|
| zone.addEventListener('drop', (e) => {
|
| e.preventDefault(); zone.classList.remove('dragover');
|
| handleFiles(e.dataTransfer.files);
|
| });
|
| fileInput.addEventListener('change', () => handleFiles(fileInput.files));
|
|
|
| function handleFiles(files) {
|
| uploadedFiles = [...files];
|
| queueCard.style.display = 'block';
|
| queue.innerHTML = uploadedFiles.map((f, i) => `
|
| <div class="file-item" id="file-item-${i}">
|
| <div class="file-item__icon">${f.name.endsWith('.csv') ? '📊' : f.name.endsWith('.json') ? '📋' : '📄'}</div>
|
| <div class="file-item__info">
|
| <div class="file-item__name">${f.name}</div>
|
| <div class="file-item__size">${(f.size / 1024).toFixed(1)} KB</div>
|
| <div class="file-item__progress"><div class="file-item__progress-bar" style="width:0%"></div></div>
|
| </div>
|
| <span class="badge" id="file-status-${i}">Queued</span>
|
| </div>
|
| `).join('');
|
| }
|
|
|
| document.getElementById('process-all-btn')?.addEventListener('click', async () => {
|
| resultsCard.style.display = 'none';
|
| const fileResults = [];
|
|
|
| for (let i = 0; i < uploadedFiles.length; i++) {
|
| const f = uploadedFiles[i];
|
| const bar = document.querySelector(`#file-item-${i} .file-item__progress-bar`);
|
| const status = document.getElementById(`file-status-${i}`);
|
|
|
| status.textContent = 'Scanning...';
|
| status.className = 'badge badge--warning';
|
| bar.style.width = '30%';
|
|
|
| if (API_AVAILABLE) {
|
| try {
|
| const formData = new FormData();
|
| formData.append('file', f);
|
| bar.style.width = '60%';
|
|
|
| const res = await fetch(API_BASE + '/scan/file', { method: 'POST', body: formData });
|
| const data = await res.json();
|
| bar.style.width = '100%';
|
|
|
| window.REDACTED_FILES[f.name] = data.redacted_text;
|
| fileResults.push({ name: f.name, size: f.size, entities: data.entity_count, ms: data.processing_ms, success: true });
|
| status.textContent = 'Done';
|
| status.className = 'badge badge--success';
|
| } catch (err) {
|
| bar.style.width = '100%';
|
| status.textContent = 'Error';
|
| status.className = 'badge badge--danger';
|
| fileResults.push({ name: f.name, size: f.size, entities: '?', ms: '-', success: false });
|
| }
|
| } else {
|
|
|
| await new Promise(r => setTimeout(r, 800));
|
| bar.style.width = '100%';
|
| status.textContent = 'Done';
|
| status.className = 'badge badge--success';
|
| fileResults.push({ name: f.name, size: f.size, entities: Math.floor(Math.random() * 50) + 5, ms: Math.floor(Math.random() * 500) });
|
| }
|
| }
|
|
|
|
|
| resultsCard.style.display = 'block';
|
| resultsTbody.innerHTML = fileResults.map((r, index) => `
|
| <tr>
|
| <td>${r.name}</td>
|
| <td>${(r.size / 1024).toFixed(1)} KB</td>
|
| <td style="font-weight:600;color:var(--text-primary);">${r.entities}</td>
|
| <td><span class="badge ${r.success ? 'badge--success' : 'badge--danger'}">${r.success ? 'Redacted' : 'Failed'}</span></td>
|
| <td>
|
| ${r.success ? `<button class="btn btn--outline btn--small" onclick="downloadFile('${r.name}')">â¬‡ï¸ Download</button>` : ''}
|
| </td>
|
| </tr>
|
| `).join('');
|
|
|
|
|
| fetchStats();
|
| fetchHistory();
|
| });
|
|
|
| document.getElementById('download-all-btn')?.addEventListener('click', () => {
|
| let delay = 0;
|
| Object.keys(window.REDACTED_FILES).forEach(filename => {
|
| setTimeout(() => window.downloadFile(filename), delay);
|
| delay += 500;
|
| });
|
| });
|
| }
|
|
|
| window.downloadFile = function(filename) {
|
| const content = window.REDACTED_FILES[filename];
|
| if (!content) return;
|
| const blob = new Blob([content], { type: 'text/plain' });
|
| const url = URL.createObjectURL(blob);
|
| const a = document.createElement('a');
|
| a.href = url;
|
| a.download = 'redacted_' + filename;
|
| document.body.appendChild(a);
|
| a.click();
|
| document.body.removeChild(a);
|
| URL.revokeObjectURL(url);
|
| };
|
|
|
|
|
| function initAPIKeys() {
|
| const list = document.getElementById('api-keys-list');
|
| const createBtn = document.getElementById('create-key-btn');
|
| if (!list) return;
|
|
|
| let keys = [
|
| { name: 'Production Key', key: 'rda_live_sk_7f8a...3b2d', created: '2 days ago', lastUsed: '1 hour ago', status: 'active' },
|
| { name: 'Development Key', key: 'rda_test_sk_9c4e...1a7f', created: '1 week ago', lastUsed: '3 days ago', status: 'active' },
|
| ];
|
|
|
| function render() {
|
| list.innerHTML = keys.map((k, i) => `
|
| <div class="api-key-card">
|
| <div style="font-size:24px;">🔑</div>
|
| <div class="api-key-card__info">
|
| <div class="api-key-card__name">${k.name}</div>
|
| <div class="api-key-card__key">${k.key}</div>
|
| <div class="api-key-card__meta">Created ${k.created} · Last used ${k.lastUsed}</div>
|
| </div>
|
| <span class="badge badge--success">Active</span>
|
| <div class="api-key-card__actions">
|
| <button class="btn btn--ghost btn--small">📋 Copy</button>
|
| <button class="btn btn--danger btn--small" onclick="this.closest('.api-key-card').remove()">🗑ï¸</button>
|
| </div>
|
| </div>
|
| `).join('');
|
| }
|
| render();
|
|
|
| createBtn?.addEventListener('click', () => {
|
| const id = Math.random().toString(36).substring(2, 6);
|
| keys.unshift({ name: 'New Key ' + id, key: 'rda_live_sk_' + Math.random().toString(36).substring(2, 14), created: 'Just now', lastUsed: 'Never', status: 'active' });
|
| render();
|
| });
|
|
|
|
|
| document.getElementById('copy-curl')?.addEventListener('click', () => {
|
| const code = document.querySelector('#page-api .code-block__body code').textContent;
|
| navigator.clipboard.writeText(code);
|
| const btn = document.getElementById('copy-curl');
|
| btn.textContent = '✓ Copied!';
|
| setTimeout(() => btn.textContent = '📋 Copy', 1500);
|
| });
|
| }
|
|
|
| |
| |
|
|
| |
| function initDataFlowVisualizer() { |
| const urlInput = document.getElementById('flow-url'); |
| const scanBtn = document.getElementById('flow-scan-btn'); |
| const empty = document.getElementById('flow-empty'); |
| const results = document.getElementById('flow-results'); |
| const progress = document.getElementById('flow-progress'); |
| const mapEl = document.getElementById('flow-map'); |
| if (!urlInput || !scanBtn || !mapEl) return; |
|
|
| function severityBadge(severity) { |
| const sev = (severity || 'low').toLowerCase(); |
| const cls = sev === 'critical' || sev === 'high' ? 'badge--danger' : sev === 'medium' ? 'badge--warning' : 'badge--success'; |
| return `<span class="badge ${cls}">${escapeFlowHtml(sev)}</span>`; |
| } |
|
|
| function nodePosition(node, index, groupIndex) { |
| const columns = { |
| subject: 60, |
| client: 230, |
| first_party: 390, |
| api_endpoint: 565, |
| external_api: 565, |
| database: 740, |
| storage: 740, |
| processor: 740, |
| third_party: 740, |
| third_party_domain: 740, |
| runtime_service: 740, |
| high_risk_processor: 740, |
| governance: 915, |
| exposure: 915, |
| }; |
| const x = columns[node.kind] || 625; |
| const y = 70 + (groupIndex * 92); |
| return { x, y }; |
| } |
|
|
| function nodeColumn(node) { |
| const columns = { |
| subject: 60, |
| client: 230, |
| first_party: 390, |
| api_endpoint: 565, |
| external_api: 565, |
| database: 740, |
| storage: 740, |
| processor: 740, |
| third_party: 740, |
| third_party_domain: 740, |
| runtime_service: 740, |
| high_risk_processor: 740, |
| governance: 915, |
| exposure: 915, |
| }; |
| return columns[node.kind] || 625; |
| } |
|
|
| function renderFlowMap(data) { |
| const nodes = data.nodes || []; |
| const edges = data.edges || []; |
| const groupCounts = {}; |
| const positions = {}; |
| nodes.forEach((node, index) => { |
| const key = String(nodeColumn(node)); |
| const groupIndex = groupCounts[key] || 0; |
| groupCounts[key] = groupIndex + 1; |
| positions[node.id] = nodePosition(node, index, groupIndex); |
| }); |
| const canvasHeight = Math.max(640, Math.max(...Object.values(groupCounts), 1) * 92 + 180); |
|
|
| const lineSvg = edges.map(edge => { |
| const from = positions[edge.source]; |
| const to = positions[edge.target]; |
| if (!from || !to) return ''; |
| const x1 = from.x + 148; |
| const y1 = from.y + 34; |
| const x2 = to.x; |
| const y2 = to.y + 34; |
| const mid = x1 + Math.max(40, (x2 - x1) / 2); |
| return `<path class="flow-line flow-line--${escapeFlowHtml(edge.risk)}" d="M ${x1} ${y1} C ${mid} ${y1}, ${mid} ${y2}, ${x2} ${y2}" /> |
| <text class="flow-line-label" x="${(x1 + x2) / 2}" y="${Math.min(y1, y2) - 8}">${escapeFlowHtml(edge.label)}</text>`; |
| }).join(''); |
|
|
| const nodeHtml = nodes.map(node => { |
| const pos = positions[node.id]; |
| return `<div class="flow-node flow-node--${escapeFlowHtml(node.risk)}" style="left:${pos.x}px;top:${pos.y}px;"> |
| <strong>${escapeFlowHtml(node.label)}</strong> |
| <span>${escapeFlowHtml(node.kind.replace(/_/g, ' '))}</span> |
| <small>${escapeFlowHtml(node.detail || '')}</small> |
| </div>`; |
| }).join(''); |
|
|
| mapEl.innerHTML = ` |
| <div class="flow-canvas"> |
| <svg viewBox="0 0 1100 ${canvasHeight}" preserveAspectRatio="none" aria-hidden="true"> |
| <defs> |
| <marker id="flow-arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth"> |
| <path d="M0,0 L0,6 L8,3 z" fill="currentColor"></path> |
| </marker> |
| </defs> |
| ${lineSvg} |
| </svg> |
| ${nodeHtml} |
| </div>`; |
| mapEl.querySelector('.flow-canvas').style.height = canvasHeight + 'px'; |
| } |
|
|
| function renderFlowReport(data) { |
| const summary = data.summary || {}; |
| const edges = data.edges || []; |
| empty.style.display = 'none'; |
| results.style.display = 'block'; |
|
|
| document.getElementById('flow-score').textContent = data.risk_score || 0; |
| document.getElementById('flow-level').textContent = (data.risk_level || 'low') + ' risk'; |
| document.getElementById('flow-node-count').textContent = summary.nodes || 0; |
| document.getElementById('flow-edge-count').textContent = summary.flows || 0; |
| document.getElementById('flow-processor-count').textContent = summary.processors || 0; |
| document.getElementById('flow-high-count').textContent = summary.high_risk_flows || 0; |
| document.getElementById('flow-domain').textContent = data.domain || data.url || 'domain'; |
| document.getElementById('flow-register-count').textContent = `${edges.length} rows`; |
|
|
| const piiTypes = summary.pii_types || []; |
| const services = data.services || []; |
| const apiCalls = data.api_calls || []; |
| document.getElementById('flow-api-count').textContent = `${apiCalls.length} calls`; |
| document.getElementById('flow-pii-types').innerHTML = piiTypes.map(type => `<span>${escapeFlowHtml(type)}</span>`).join(''); |
| document.getElementById('flow-services').innerHTML = services.length ? services.slice(0, 18).map(service => `<span>${escapeFlowHtml(service.name)} · ${escapeFlowHtml(service.category)}</span>`).join('') : '<span>No provider signals found in public surface</span>'; |
| document.getElementById('flow-remediation').innerHTML = (data.remediation || []).map(item => `<li>${escapeFlowHtml(item)}</li>`).join(''); |
| document.getElementById('flow-api-table').innerHTML = apiCalls.slice(0, 50).map(call => `<tr> |
| <td>${escapeFlowHtml(call.host || '')}</td> |
| <td>${escapeFlowHtml(call.first_party ? 'First-party API' : 'External API')}</td> |
| <td>${escapeFlowHtml(call.asset_type || call.confidence || '')}</td> |
| <td>${escapeFlowHtml((call.evidence || call.url || '').slice(0, 160))}</td> |
| </tr>`).join(''); |
| document.getElementById('flow-register').innerHTML = edges.map(edge => { |
| const from = (data.nodes || []).find(n => n.id === edge.source); |
| const to = (data.nodes || []).find(n => n.id === edge.target); |
| return `<tr> |
| <td>${escapeFlowHtml(from ? from.label : edge.source)}</td> |
| <td>${escapeFlowHtml(to ? to.label : edge.target)}</td> |
| <td>${escapeFlowHtml(edge.label)}</td> |
| <td>${escapeFlowHtml((edge.data_types || []).join(', '))}</td> |
| <td>${severityBadge(edge.risk)}</td> |
| <td>${escapeFlowHtml(edge.control || '')}</td> |
| </tr>`; |
| }).join(''); |
|
|
| renderFlowMap(data); |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| } |
|
|
| async function runFlowScan() { |
| const url = urlInput.value.trim(); |
| if (!url) { |
| urlInput.focus(); |
| return; |
| } |
|
|
| scanBtn.disabled = true; |
| scanBtn.innerHTML = '<i data-lucide="loader-2" class="spin"></i> Mapping...'; |
| if (progress) progress.style.display = 'flex'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
|
|
| try { |
| const res = await fetch(API_BASE + '/visualize/data-flow', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| url, |
| include_trackers: document.getElementById('flow-trackers')?.checked !== false, |
| include_cookies: document.getElementById('flow-cookies')?.checked !== false, |
| include_ai: document.getElementById('flow-ai')?.checked !== false, |
| include_runtime: document.getElementById('flow-runtime')?.checked !== false, |
| include_source_maps: true, |
| }), |
| }); |
| const data = await res.json(); |
| if (!res.ok) throw new Error(data.detail || 'Could not build data-flow map'); |
| renderFlowReport(data); |
| } catch (err) { |
| empty.style.display = 'flex'; |
| empty.innerHTML = `<i data-lucide="alert-triangle"></i><div><strong>Map failed</strong><span>${escapeFlowHtml(err.message || 'Could not map this product URL.')}</span></div>`; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| } finally { |
| if (progress) progress.style.display = 'none'; |
| scanBtn.disabled = false; |
| scanBtn.innerHTML = '<i data-lucide="route"></i> Map Data Flow'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| } |
| } |
|
|
| scanBtn.addEventListener('click', runFlowScan); |
| urlInput.addEventListener('keydown', function(e) { |
| if (e.key === 'Enter') runFlowScan(); |
| }); |
| } |
|
|
| |
| document.addEventListener('DOMContentLoaded', function() { |
|
|
| if (window.location.hash) { |
| const pageId = window.location.hash.substring(1); |
| if (document.getElementById('page-' + pageId)) { |
| switchPage(pageId); |
| } |
| } |
|
|
| window.addEventListener('hashchange', function() { |
| const pageId = window.location.hash.substring(1); |
| if (document.getElementById('page-' + pageId)) { |
| switchPage(pageId); |
| } |
| }); |
|
|
|
|
| var sidebarNav = document.querySelector('.sidebar__nav');
|
| if (sidebarNav) {
|
| sidebarNav.addEventListener('click', function(e) {
|
| var btn = e.target.closest('.sidebar__link');
|
| if (btn && btn.dataset.page) {
|
| switchPage(btn.dataset.page);
|
| }
|
| });
|
| }
|
|
|
|
|
| var menuToggle = document.getElementById('menu-toggle');
|
| if (menuToggle) {
|
| menuToggle.addEventListener('click', function() {
|
| document.getElementById('sidebar').classList.toggle('open');
|
| });
|
| }
|
|
|
|
|
| renderBarChart();
|
| renderDonutChart();
|
| renderRecentTable();
|
| renderHistoryTable();
|
| initScannerPage();
|
| initFileUpload();
|
| initAPIKeys(); |
| initCustomDetectors(); |
| initDataFlowVisualizer(); |
| initProductTools(); |
| initAILeakScanner(); |
|
|
|
|
| var exportBtn = document.getElementById('export-history-btn');
|
| if (exportBtn) {
|
| exportBtn.addEventListener('click', function() {
|
| var base = API_AVAILABLE ? API_BASE : '';
|
| window.open(base + '/export?format=csv', '_blank');
|
| });
|
| }
|
|
|
|
|
| checkAPI().then(function() {
|
| if (API_AVAILABLE) {
|
| fetchStats();
|
| fetchHistory();
|
| }
|
| });
|
| });
|
|
|
|
|
| function initCustomDetectors() {
|
| var detectors = JSON.parse(localStorage.getItem('redactai_custom_detectors') || '[]');
|
| var list = document.getElementById('custom-detectors-list');
|
| var form = document.getElementById('detector-form');
|
| var addBtn = document.getElementById('add-detector-btn');
|
| var saveBtn = document.getElementById('save-detector-btn');
|
| var cancelBtn = document.getElementById('cancel-detector-btn');
|
| if (!list) return;
|
|
|
| function render() {
|
| if (detectors.length === 0) {
|
| list.innerHTML = '<div style="color:var(--text-muted);padding:12px;text-align:center;">No custom detectors yet. Click "+ Add Detector" to create one.</div>';
|
| return;
|
| }
|
| list.innerHTML = detectors.map(function(d, i) {
|
| return '<div style="display:flex;justify-content:space-between;align-items:center;padding:12px;border:1px solid var(--border);border-radius:8px;margin-bottom:8px;">' +
|
| '<div>' +
|
| '<div style="font-weight:600;">' + d.name + ' <span class="badge">' + d.entity + '</span></div>' +
|
| '<div style="color:var(--text-secondary);font-size:13px;font-family:monospace;">/' + d.regex + '/ (score: ' + d.score + ')</div>' +
|
| '</div>' +
|
| '<div style="display:flex;gap:8px;">' +
|
| '<button class="btn btn--outline btn--small" onclick="testDetector(' + i + ')">\uD83E\uDDEA Test</button>' +
|
| '<button class="btn btn--danger btn--small" onclick="removeDetector(' + i + ')">\u2716</button>' +
|
| '</div>' +
|
| '</div>';
|
| }).join('');
|
| }
|
|
|
| render();
|
|
|
| if (addBtn) addBtn.addEventListener('click', function() {
|
| form.style.display = 'block';
|
| });
|
|
|
| if (cancelBtn) cancelBtn.addEventListener('click', function() {
|
| form.style.display = 'none';
|
| });
|
|
|
| if (saveBtn) saveBtn.addEventListener('click', function() {
|
| var name = document.getElementById('detector-name').value.trim();
|
| var entity = document.getElementById('detector-entity').value.trim().toUpperCase().replace(/\s+/g, '_');
|
| var regex = document.getElementById('detector-regex').value.trim();
|
| var score = parseFloat(document.getElementById('detector-score').value) || 0.8;
|
|
|
| if (!name || !entity || !regex) {
|
| alert('Please fill in all fields');
|
| return;
|
| }
|
|
|
|
|
| try {
|
| new RegExp(regex);
|
| } catch (e) {
|
| alert('Invalid regex pattern: ' + e.message);
|
| return;
|
| }
|
|
|
| detectors.push({ name: name, entity: entity, regex: regex, score: score });
|
| localStorage.setItem('redactai_custom_detectors', JSON.stringify(detectors));
|
|
|
|
|
| if (API_AVAILABLE) {
|
| fetch(API_BASE + '/custom-detector', {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ name: name, entity_type: entity, regex: regex, score: score })
|
| }).catch(function() {});
|
| }
|
|
|
|
|
| document.getElementById('detector-name').value = '';
|
| document.getElementById('detector-entity').value = '';
|
| document.getElementById('detector-regex').value = '';
|
| document.getElementById('detector-score').value = '0.8';
|
| form.style.display = 'none';
|
| render();
|
| });
|
|
|
| window.removeDetector = function(index) {
|
| detectors.splice(index, 1);
|
| localStorage.setItem('redactai_custom_detectors', JSON.stringify(detectors));
|
| render();
|
| };
|
|
|
| window.testDetector = function(index) {
|
| var d = detectors[index];
|
| var testText = prompt('Enter text to test detector "' + d.name + '":', '');
|
| if (!testText) return;
|
| try {
|
| var re = new RegExp(d.regex, 'g');
|
| var matches = testText.match(re);
|
| if (matches) {
|
| alert('\u2705 Found ' + matches.length + ' match(es):\n' + matches.join('\n'));
|
| } else {
|
| alert('\u274c No matches found for pattern /' + d.regex + '/');
|
| }
|
| } catch (e) {
|
| alert('Regex error: ' + e.message);
|
| }
|
| };
|
| }
|
|
|
|
|
| function initAILeakScanner() {
|
| const urlInput = document.getElementById('ai-leak-url');
|
| const deepInput = document.getElementById('ai-leak-deep');
|
| const scanBtn = document.getElementById('ai-leak-scan-btn');
|
| const progress = document.getElementById('ai-leak-progress');
|
| const progressBar = document.getElementById('ai-leak-progress-bar');
|
| const progressText = document.getElementById('ai-leak-progress-text'); |
| const empty = document.getElementById('ai-leak-empty'); |
| const results = document.getElementById('ai-leak-results'); |
| const engineGrid = document.getElementById('ai-engine-grid'); |
| const engineBadge = document.getElementById('ai-engine-status-badge'); |
| const engineOutput = document.getElementById('ai-engine-output'); |
| if (!urlInput || !scanBtn) return; |
|
|
| const phases = [
|
| 'Fetching public product surface...',
|
| 'Crawling same-origin pages...',
|
| 'Following JavaScript bundles and source maps...',
|
| 'Parsing original source-map contents...',
|
| 'Searching for provider keys, public env vars, and entropy leaks...',
|
| 'Fingerprinting prompts, models, RAG, and agent traces...',
|
| 'Mapping findings to OWASP LLM and SARIF...'
|
| ];
|
|
|
| function escapeHtml(value) {
|
| const div = document.createElement('div');
|
| div.textContent = value == null ? '' : String(value);
|
| return div.innerHTML;
|
| }
|
|
|
| function truncateMiddle(value, maxLength) {
|
| const text = value || '';
|
| if (text.length <= maxLength) return text;
|
| const keep = Math.floor((maxLength - 3) / 2);
|
| return text.slice(0, keep) + '...' + text.slice(text.length - keep);
|
| }
|
|
|
| function formatBytes(bytes) {
|
| const n = Number(bytes) || 0;
|
| if (n < 1024) return n + ' B';
|
| if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
| return (n / (1024 * 1024)).toFixed(1) + ' MB';
|
| }
|
|
|
| function severityBadge(severity) { |
| const sev = (severity || 'low').toLowerCase();
|
| const cls = sev === 'critical' || sev === 'high' ? 'badge--danger' : sev === 'medium' ? 'badge--warning' : 'badge--success';
|
| return `<span class="badge ${cls}">${escapeHtml(sev)}</span>`; |
| } |
|
|
| function renderEngineOutput(title, data) { |
| if (!engineOutput) return; |
| const triage = data && data.triage ? ` |
| <div class="ai-engine-triage"> |
| <span>new ${escapeHtml(data.triage.new || 0)}</span> |
| <span>baseline ${escapeHtml(data.triage.baseline || 0)}</span> |
| <span>ignored ${escapeHtml(data.triage.ignored || 0)}</span> |
| </div> |
| ` : ''; |
| engineOutput.classList.add('active'); |
| engineOutput.innerHTML = ` |
| <div class="ai-leak-finding__title">${escapeHtml(title)}</div> |
| ${triage} |
| <pre>${escapeHtml(JSON.stringify(data, null, 2).slice(0, 9000))}</pre> |
| `; |
| } |
|
|
| function getIgnoredFingerprints() { |
| const raw = document.getElementById('ai-ignore-fingerprints')?.value || ''; |
| return raw.split(/[\s,]+/).map(v => v.trim()).filter(Boolean); |
| } |
|
|
| async function loadSecurityEngines() { |
| if (!engineGrid) return; |
| try { |
| const res = await fetch(API_BASE + '/security/engines'); |
| const data = await res.json(); |
| const engines = data.engines || {}; |
| const available = Object.values(engines).filter(e => e.available).length; |
| if (engineBadge) engineBadge.textContent = `${available} / ${Object.keys(engines).length} installed`; |
| engineGrid.innerHTML = Object.entries(engines).map(([name, info]) => ` |
| <div class="ai-engine-tile"> |
| <strong>${escapeHtml(name)} ${severityBadge(info.available ? 'low' : 'medium')}</strong> |
| <span>${escapeHtml(info.why || '')}</span> |
| <span>${escapeHtml(info.available ? info.path : 'Optional engine not installed')}</span> |
| </div> |
| `).join(''); |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| } catch (err) { |
| if (engineBadge) engineBadge.textContent = 'Engine check failed'; |
| } |
| } |
|
|
| function setProgress(active, idx) {
|
| if (!progress || !progressBar || !progressText) return;
|
| progress.style.display = active ? 'block' : 'none';
|
| if (!active) return;
|
| progressBar.style.width = Math.min(92, 15 + idx * 18) + '%';
|
| progressText.textContent = phases[idx % phases.length];
|
| }
|
|
|
| function renderAILeakReport(data) {
|
| const summary = data.summary || {};
|
| const findings = data.findings || [];
|
| const assets = data.assets || [];
|
| const providers = summary.providers_detected || {};
|
| const models = summary.models_detected || {};
|
| const owasp = summary.owasp_breakdown || {};
|
| const score = Number(data.risk_score || 0);
|
| const level = (data.risk_level || 'low').toLowerCase();
|
|
|
| empty.style.display = 'none';
|
| results.style.display = 'block';
|
|
|
| document.getElementById('ai-leak-domain').textContent = data.domain || data.url || 'scanned product';
|
| document.getElementById('ai-leak-ms').textContent = Math.round(data.scan_time_ms || 0) + 'ms';
|
| document.getElementById('ai-leak-score').textContent = score;
|
| document.getElementById('ai-leak-level').textContent = level;
|
| document.getElementById('ai-leak-total').textContent = summary.total_findings || findings.length || 0;
|
| document.getElementById('ai-leak-critical').textContent = summary.critical || 0;
|
| document.getElementById('ai-leak-assets').textContent = summary.assets_scanned || assets.length || 0;
|
| document.getElementById('ai-leak-pages').textContent = summary.pages_crawled || 1;
|
| document.getElementById('ai-leak-findings-count').textContent = `${findings.length} detected`;
|
| document.getElementById('ai-leak-asset-count').textContent = `${assets.length} assets`;
|
|
|
| const ring = document.getElementById('ai-leak-score-ring');
|
| const scoreColor = level === 'critical' ? '#ff4545' : level === 'high' ? '#ff7a45' : level === 'medium' ? '#ffd60a' : '#32d74b';
|
| ring.style.background = `conic-gradient(${scoreColor} 0% ${score}%, rgba(255,255,255,0.08) ${score}% 100%)`;
|
| ring.style.boxShadow = `0 0 40px ${scoreColor}33`;
|
|
|
| const verdict = document.getElementById('ai-leak-verdict');
|
| const verdictCopy = document.getElementById('ai-leak-verdict-copy');
|
| if (level === 'critical') {
|
| verdict.textContent = 'Critical AI leak exposure';
|
| verdictCopy.textContent = 'Provider credentials or severe client-side AI artifacts were found. Rotate keys and move AI calls server-side immediately.';
|
| } else if (level === 'high') {
|
| verdict.textContent = 'High-risk AI surface exposed';
|
| verdictCopy.textContent = 'Prompts, source maps, or sensitive AI implementation details are visible from the public product.';
|
| } else if (level === 'medium') {
|
| verdict.textContent = 'Moderate AI exposure';
|
| verdictCopy.textContent = 'The product reveals AI routes or architecture signals that should be reviewed before launch.';
|
| } else {
|
| verdict.textContent = 'Clean public AI surface';
|
| verdictCopy.textContent = 'No obvious AI provider keys, prompts, or risky source-map exposures were found in scanned assets.';
|
| }
|
|
|
| const findingsEl = document.getElementById('ai-leak-findings');
|
| if (!findings.length) {
|
| findingsEl.innerHTML = '<div class="ai-leak-clean"><i data-lucide="shield-check"></i><strong>No AI leaks detected</strong><span>The scanned public assets did not expose obvious LLM secrets, prompts, source maps, or provider routes.</span></div>';
|
| } else {
|
| findingsEl.innerHTML = findings.map(f => `
|
| <div class="ai-leak-finding ai-leak-finding--${escapeHtml(f.severity || 'low')}">
|
| <div class="ai-leak-finding__top">
|
| <div>
|
| <div class="ai-leak-finding__title">${escapeHtml(f.title)}</div>
|
| <div class="ai-leak-finding__meta">${escapeHtml(f.kind)} · ${escapeHtml(f.provider || 'Product')} · ${escapeHtml((f.owasp && f.owasp.code) || 'LLM06')} · ${escapeHtml(f.confidence || 'pattern')} · ${escapeHtml(f.fingerprint || f.id || '')}</div> |
| </div>
|
| ${severityBadge(f.severity)}
|
| </div>
|
| <pre>${escapeHtml(f.evidence || 'Evidence redacted')}</pre>
|
| <div class="ai-leak-finding__asset">${escapeHtml(truncateMiddle(f.asset, 92))}</div>
|
| <div class="ai-leak-finding__fix">${escapeHtml(f.recommendation || '')}</div>
|
| </div>
|
| `).join('');
|
| }
|
|
|
| const remediation = data.remediation || [];
|
| document.getElementById('ai-leak-remediation').innerHTML = remediation.map(item => `<li>${escapeHtml(item)}</li>`).join('');
|
|
|
| const stackItems = [
|
| ...Object.entries(providers).map(([name, count]) => ({ name, detail: count + ' signal' + (count === 1 ? '' : 's') })),
|
| ...Object.entries(models).map(([name, count]) => ({ name, detail: count + ' reference' + (count === 1 ? '' : 's') })),
|
| ];
|
| document.getElementById('ai-leak-stack').innerHTML = stackItems.length ? stackItems.map(item => ` |
| <div class="ai-leak-stack__item"> |
| <span>${escapeHtml(item.name)}</span> |
| <small>${escapeHtml(item.detail)}</small> |
| </div> |
| `).join('') : '<div class="ai-leak-muted">No provider or model fingerprints detected.</div>'; |
|
|
| const owaspLabels = { |
| LLM02: 'Sensitive Information Disclosure', |
| LLM05: 'Supply Chain / Implementation Exposure', |
| LLM06: 'Sensitive Information Disclosure', |
| LLM07: 'System Prompt Leakage', |
| LLM09: 'Model Metadata Exposure' |
| }; |
| const owaspItems = Object.entries(owasp).map(([code, count]) => ({ code, count, label: owaspLabels[code] || 'LLM risk' })); |
| document.getElementById('ai-leak-owasp').innerHTML = owaspItems.length ? owaspItems.map(item => ` |
| <div class="ai-leak-stack__item"> |
| <span>${escapeHtml(item.code)}</span> |
| <small>${escapeHtml(item.count + ' · ' + item.label)}</small> |
| </div> |
| `).join('') : '<div class="ai-leak-muted">No OWASP LLM risks mapped.</div>'; |
|
|
| document.getElementById('ai-leak-assets-table').innerHTML = assets.map(asset => ` |
| <tr>
|
| <td>${escapeHtml(truncateMiddle(asset.url, 84))}</td>
|
| <td><span class="badge">${escapeHtml(asset.type || 'asset')}</span></td>
|
| <td>${escapeHtml(asset.status || '-')}</td>
|
| <td>${escapeHtml(formatBytes(asset.size))}</td>
|
| </tr>
|
| `).join('');
|
|
|
| if (typeof lucide !== 'undefined') lucide.createIcons();
|
| }
|
|
|
| async function runScan() {
|
| const url = urlInput.value.trim();
|
| if (!url) {
|
| urlInput.focus();
|
| return;
|
| }
|
|
|
| scanBtn.disabled = true;
|
| scanBtn.innerHTML = '<i data-lucide="loader-2" class="spin"></i> Scanning...';
|
| if (typeof lucide !== 'undefined') lucide.createIcons();
|
| results.style.display = 'none';
|
|
|
| let phaseIndex = 0;
|
| setProgress(true, phaseIndex);
|
| const timer = setInterval(() => {
|
| phaseIndex = Math.min(phaseIndex + 1, phases.length - 1);
|
| setProgress(true, phaseIndex);
|
| }, 900);
|
|
|
| try {
|
| const res = await fetch(API_BASE + '/scan/ai-leak', {
|
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| url, |
| deep: deepInput ? deepInput.checked : true, |
| max_pages: 4, |
| sarif: true, |
| ignore_fingerprints: getIgnoredFingerprints() |
| }) |
| });
|
| const data = await res.json();
|
| if (!res.ok) throw new Error(data.detail || 'AI leak scan failed');
|
| if (progressBar) progressBar.style.width = '100%';
|
| if (progressText) progressText.textContent = 'Report ready';
|
| setTimeout(() => renderAILeakReport(data), 180);
|
| } catch (err) {
|
| empty.style.display = 'flex';
|
| empty.innerHTML = `<i data-lucide="alert-triangle"></i><div><strong>Scan failed</strong><span>${escapeHtml(err.message || 'Could not scan this product URL.')}</span></div>`;
|
| if (typeof lucide !== 'undefined') lucide.createIcons();
|
| } finally {
|
| clearInterval(timer);
|
| setTimeout(() => setProgress(false, 0), 350);
|
| scanBtn.disabled = false;
|
| scanBtn.innerHTML = '<i data-lucide="scan-search"></i> Run AI Leak Scan';
|
| if (typeof lucide !== 'undefined') lucide.createIcons();
|
| }
|
| }
|
|
|
| scanBtn.addEventListener('click', runScan); |
| urlInput.addEventListener('keydown', function(e) { |
| if (e.key === 'Enter') runScan(); |
| }); |
|
|
| document.getElementById('repo-ai-scan-btn')?.addEventListener('click', async () => { |
| const btn = document.getElementById('repo-ai-scan-btn'); |
| const path = document.getElementById('repo-ai-path')?.value || '.'; |
| const includeHistory = document.getElementById('repo-ai-history')?.checked || false; |
| btn.disabled = true; |
| btn.innerHTML = '<i data-lucide="loader-2" class="spin"></i> Scanning Repo...'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| try { |
| const res = await fetch(API_BASE + '/scan/repo-ai-leak', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| path, |
| use_external: true, |
| include_git_history: includeHistory, |
| max_files: 500, |
| max_commits: 50, |
| ignore_fingerprints: getIgnoredFingerprints() |
| }) |
| }); |
| const data = await res.json(); |
| if (!res.ok) throw new Error(data.detail || 'Repo scan failed'); |
| renderEngineOutput(`Repo scan: ${data.summary.total_findings} findings across ${data.summary.files_scanned} files`, data); |
| } catch (err) { |
| renderEngineOutput('Repo scan failed', { error: err.message }); |
| } finally { |
| btn.disabled = false; |
| btn.innerHTML = '<i data-lucide="git-branch"></i> Scan Repo'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| } |
| }); |
|
|
| document.getElementById('github-ai-scan-btn')?.addEventListener('click', async () => { |
| const btn = document.getElementById('github-ai-scan-btn'); |
| const repoUrl = document.getElementById('github-ai-url')?.value || ''; |
| const includeHistory = document.getElementById('github-ai-history')?.checked || false; |
| if (!repoUrl.trim()) return; |
| btn.disabled = true; |
| btn.innerHTML = '<i data-lucide="loader-2" class="spin"></i> Scanning GitHub...'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| try { |
| const res = await fetch(API_BASE + '/scan/github-repo-ai-leak', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| repo_url: repoUrl, |
| use_external: true, |
| include_git_history: includeHistory, |
| max_files: 500, |
| max_commits: 50, |
| ignore_fingerprints: getIgnoredFingerprints() |
| }) |
| }); |
| const data = await res.json(); |
| if (!res.ok) throw new Error(data.detail || 'GitHub scan failed'); |
| renderEngineOutput(`GitHub scan: ${data.summary.total_findings} findings in ${data.repo.owner}/${data.repo.name}`, data); |
| } catch (err) { |
| renderEngineOutput('GitHub scan failed', { error: err.message }); |
| } finally { |
| btn.disabled = false; |
| btn.innerHTML = '<i data-lucide="git-fork"></i> Scan GitHub'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| } |
| }); |
|
|
| document.getElementById('model-ai-scan-btn')?.addEventListener('click', async () => { |
| const btn = document.getElementById('model-ai-scan-btn'); |
| const path = document.getElementById('model-ai-path')?.value || ''; |
| if (!path.trim()) return; |
| btn.disabled = true; |
| btn.innerHTML = '<i data-lucide="loader-2" class="spin"></i> Scanning Model...'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| try { |
| const res = await fetch(API_BASE + '/scan/model-artifact', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ path, use_external: true }) |
| }); |
| const data = await res.json(); |
| if (!res.ok) throw new Error(data.detail || 'Model scan failed'); |
| renderEngineOutput(`Model artifact scan: ${data.risk_level}`, data); |
| } catch (err) { |
| renderEngineOutput('Model scan failed', { error: err.message }); |
| } finally { |
| btn.disabled = false; |
| btn.innerHTML = '<i data-lucide="box"></i> Scan Model'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| } |
| }); |
|
|
| document.getElementById('redteam-ai-btn')?.addEventListener('click', async () => { |
| const btn = document.getElementById('redteam-ai-btn'); |
| const target = document.getElementById('redteam-ai-target')?.value || ''; |
| if (!target.trim()) return; |
| btn.disabled = true; |
| btn.innerHTML = '<i data-lucide="loader-2" class="spin"></i> Planning...'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| try { |
| const res = await fetch(API_BASE + '/scan/llm-redteam-plan', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ target, provider: 'http', intensity: 'standard' }) |
| }); |
| const data = await res.json(); |
| if (!res.ok) throw new Error(data.detail || 'Red-team plan failed'); |
| renderEngineOutput(`LLM red-team plan: ${data.probe_plan.length} probes`, data); |
| } catch (err) { |
| renderEngineOutput('Red-team plan failed', { error: err.message }); |
| } finally { |
| btn.disabled = false; |
| btn.innerHTML = '<i data-lucide="swords"></i> Red-Team Plan'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| } |
| }); |
|
|
| document.getElementById('guardrails-ai-btn')?.addEventListener('click', async () => { |
| const btn = document.getElementById('guardrails-ai-btn'); |
| btn.disabled = true; |
| btn.innerHTML = '<i data-lucide="loader-2" class="spin"></i> Building...'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| try { |
| const res = await fetch(API_BASE + '/security/guardrails'); |
| const data = await res.json(); |
| if (!res.ok) throw new Error(data.detail || 'Guardrails failed'); |
| renderEngineOutput('CI/pre-commit guardrails', data); |
| } catch (err) { |
| renderEngineOutput('Guardrails failed', { error: err.message }); |
| } finally { |
| btn.disabled = false; |
| btn.innerHTML = '<i data-lucide="shield-plus"></i> CI Guardrails'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| } |
| }); |
|
|
| document.getElementById('install-ai-btn')?.addEventListener('click', async () => { |
| const btn = document.getElementById('install-ai-btn'); |
| btn.disabled = true; |
| btn.innerHTML = '<i data-lucide="loader-2" class="spin"></i> Checking...'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| try { |
| const res = await fetch(API_BASE + '/security/install-plan'); |
| const data = await res.json(); |
| if (!res.ok) throw new Error(data.detail || 'Install plan failed'); |
| renderEngineOutput('External engine install plan', data); |
| } catch (err) { |
| renderEngineOutput('Install plan failed', { error: err.message }); |
| } finally { |
| btn.disabled = false; |
| btn.innerHTML = '<i data-lucide="terminal"></i> Install Plan'; |
| if (typeof lucide !== 'undefined') lucide.createIcons(); |
| } |
| }); |
|
|
| loadSecurityEngines(); |
| } |
|
|
| |
| async function runPromptScan() { |
| const input = document.getElementById('prompt-scanner-input'); |
| const btn = document.getElementById('prompt-scan-btn'); |
| const results = document.getElementById('prompt-scan-results'); |
|
|
| if(!input.value.trim()) return; |
|
|
| btn.disabled = true;
|
| btn.innerHTML = `<i data-lucide="loader-2" class="spin"></i> Scanning Model...`;
|
| if(typeof lucide !== 'undefined') lucide.createIcons();
|
|
|
|
|
| document.getElementById('bar-injection').style.width = '0%';
|
| document.getElementById('bar-jailbreak').style.width = '0%';
|
| document.getElementById('bar-pii').style.width = '0%';
|
| document.getElementById('bar-toxicity').style.width = '0%';
|
| results.style.display = 'grid';
|
|
|
| try { |
| const res = await fetch(API_BASE + '/prompt-risk/scan', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ prompt: input.value, context: 'dashboard' }) |
| }); |
| const data = await res.json(); |
| if (!res.ok) throw new Error(data.detail || 'Prompt scan failed'); |
| const score = Number(data.risk_score || 0); |
| document.querySelector('#prompt-scan-results .metric-card span').textContent = data.risk_level === 'critical' ? 'Critical' : data.risk_level === 'high' ? 'High' : data.risk_level === 'medium' ? 'Medium' : 'Low'; |
| document.querySelector('#prompt-scan-results .metric-card [style*="font-size: 13px"]').textContent = `${data.summary.findings} finding(s). ${data.safe_rewrite[0]}`; |
| document.getElementById('bar-injection').style.width = Math.min(100, score + 20) + '%'; |
| document.getElementById('bar-jailbreak').style.width = Math.min(100, Math.max(10, score - 5)) + '%'; |
| document.getElementById('bar-pii').style.width = data.summary.pii_entities.length ? '75%' : '5%'; |
| document.getElementById('bar-toxicity').style.width = '5%'; |
| const breakdown = results.querySelector('.app-card > div:last-child'); |
| breakdown.insertAdjacentHTML('beforeend', ` |
| <div class="tool-result-list"> |
| ${(data.findings || []).map(f => `<div class="tool-result-item"><strong>${escapeFlowHtml(f.title)}</strong><span>${escapeFlowHtml(f.severity)} - ${escapeFlowHtml(f.fix)}</span></div>`).join('') || '<div class="tool-result-item"><strong>No obvious prompt risks</strong><span>This prompt did not match common injection, exfiltration, or PII patterns.</span></div>'} |
| </div> |
| `); |
| } catch (err) { |
| alert(err.message || 'Prompt scan failed'); |
| } finally { |
| btn.disabled = false; |
| btn.innerHTML = `<i data-lucide="shield-alert"></i> Run Security Scan`; |
| if(typeof lucide !== 'undefined') lucide.createIcons(); |
| } |
| } |
|
|
| function initProductTools() { |
| const render = (el, html) => { if (el) { el.classList.add('active'); el.innerHTML = html; if (typeof lucide !== 'undefined') lucide.createIcons(); } }; |
| const badge = sev => `<span class="badge ${(sev === 'high' || sev === 'critical') ? 'badge--danger' : sev === 'medium' ? 'badge--warning' : 'badge--success'}">${escapeFlowHtml(sev)}</span>`; |
|
|
| document.getElementById('dpdp-btn')?.addEventListener('click', async () => { |
| const btn = document.getElementById('dpdp-btn'); |
| const out = document.getElementById('dpdp-output'); |
| const url = document.getElementById('dpdp-url')?.value || ''; |
| if (!url.trim()) return; |
| btn.disabled = true; btn.innerHTML = '<i data-lucide="loader-2" class="spin"></i> Checking...'; |
| try { |
| const res = await fetch(API_BASE + '/dpdp/quick-check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }); |
| const data = await res.json(); |
| if (!res.ok) throw new Error(data.detail || 'DPDP check failed'); |
| const statusBadge = status => { |
| const normalized = String(status || '').toLowerCase(); |
| const cls = normalized === 'pass' ? 'badge--success' : normalized === 'review' ? 'badge--warning' : 'badge--danger'; |
| return `<span class="badge ${cls}">${escapeFlowHtml(normalized === 'pass' ? 'Pass' : normalized === 'review' ? 'Review' : 'Fix')}</span>`; |
| }; |
| const evidenceLine = check => (check.evidence || []).slice(0, 2).map(e => `<span>${escapeFlowHtml(e)}</span>`).join(''); |
| const processors = (data.detected_processors || []).slice(0, 8).map(p => `<span class="badge badge--warning">${escapeFlowHtml(p.name || 'Provider')} · ${escapeFlowHtml(p.category || 'processor')}</span>`).join(''); |
| const policy = data.policy_control_matrix || {}; |
| const policyControls = (policy.controls || []).slice(0, 12).map(c => ` |
| <div class="tool-result-item"> |
| <strong>${statusBadge(c.status)} ${escapeFlowHtml(c.title || c.id)}</strong> |
| <span>${escapeFlowHtml(c.act || '')}</span> |
| <span>${escapeFlowHtml(c.rules || '')}</span> |
| ${(c.evidence || []).slice(0, 1).map(e => `<span>${escapeFlowHtml(e)}</span>`).join('')} |
| </div>`).join(''); |
| render(out, ` |
| <div class="tool-score"> |
| <strong>${escapeFlowHtml(data.grade)}</strong> |
| <span>${escapeFlowHtml(data.verdict)} · ${escapeFlowHtml(data.score)}% · ${escapeFlowHtml(data.overall_risk || 'Review')} risk</span> |
| </div> |
| <div class="tool-score tool-score--compact"> |
| <strong>${escapeFlowHtml(policy.grade || '-')}</strong> |
| <span>Policy evidence · ${escapeFlowHtml(policy.score ?? 'n/a')}% · ${escapeFlowHtml((policy.coverage && `${policy.coverage.pass || 0} pass / ${policy.coverage.review || 0} review / ${policy.coverage.fail || 0} fail`) || 'no policy matrix')}</span> |
| </div> |
| ${processors ? `<div class="tool-result-item"><strong>Detected processors / providers</strong><span>${processors}</span></div>` : ''} |
| <div class="tool-grid">${(data.checks || []).map(c => ` |
| <div class="tool-result-item"> |
| <strong>${statusBadge(c.status)} ${escapeFlowHtml(c.label)}</strong> |
| <span>${escapeFlowHtml(c.status === 'pass' ? c.why : c.fix)}</span> |
| ${evidenceLine(c)} |
| <span>${escapeFlowHtml(c.section || '')}</span> |
| </div>`).join('')}</div> |
| <div class="tool-result-list"> |
| ${policyControls ? `<div><h3 class="tool-section-title">Policy Control Matrix</h3><div class="tool-grid">${policyControls}</div></div>` : ''} |
| <div class="tool-result-item"><strong>Priority actions</strong>${(data.priority_actions || []).slice(0, 4).map(a => `<span>${escapeFlowHtml(a)}</span>`).join('')}</div> |
| <div class="tool-result-item"><strong>Scanner limitations</strong>${(data.limitations || []).map(a => `<span>${escapeFlowHtml(a)}</span>`).join('')}</div> |
| </div> |
| `); |
| } catch (err) { render(out, `<div class="tool-result-item"><strong>Check failed</strong><span>${escapeFlowHtml(err.message)}</span></div>`); } |
| finally { btn.disabled = false; btn.innerHTML = '<i data-lucide="search-check"></i> Run Quick Check'; if (typeof lucide !== 'undefined') lucide.createIcons(); } |
| }); |
|
|
| document.getElementById('synthetic-btn')?.addEventListener('click', async () => { |
| const btn = document.getElementById('synthetic-btn'); |
| const out = document.getElementById('synthetic-output'); |
| btn.disabled = true; btn.innerHTML = '<i data-lucide="loader-2" class="spin"></i> Generating...'; |
| try { |
| const res = await fetch(API_BASE + '/synthetic-attack-suite/generate', { |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ industry: document.getElementById('synthetic-industry')?.value, volume: Number(document.getElementById('synthetic-volume')?.value || 12) }) |
| }); |
| const data = await res.json(); |
| if (!res.ok) throw new Error(data.detail || 'Generation failed'); |
| render(out, ` |
| <div class="tool-score"><strong>${data.volume}</strong><span>Safe synthetic test payloads generated</span></div> |
| <div class="tool-result-list">${data.attacks.slice(0, 12).map(a => `<div class="tool-result-item"><strong>${escapeFlowHtml(a.title)} ${badge('low')}</strong><pre>${escapeFlowHtml(a.payload)}</pre><span>Expected: ${escapeFlowHtml(a.expected_detections.join(', '))}</span></div>`).join('')}</div> |
| `); |
| } catch (err) { render(out, `<div class="tool-result-item"><strong>Generation failed</strong><span>${escapeFlowHtml(err.message)}</span></div>`); } |
| finally { btn.disabled = false; btn.innerHTML = '<i data-lucide="flask-conical"></i> Generate Suite'; if (typeof lucide !== 'undefined') lucide.createIcons(); } |
| }); |
| } |
|
|