PageSourceSearch

https://neuravant.ai/static/js/pages/compliance-scan.js?v=1

js neuravant.ai collected 2026-10-05 13:06:27 UTC 10,571 bytes, 209 lines download raw bytes

1/**
2 * Compliance Scan page — rendered EU/UK governance scan of a site's public
3 * surface (UK GDPR / DPA 2018, PECR, EU AI Act Art. 50, security headers,
4 * accessibility). Authenticated scans (behind login) require Monitor+.
5 *
6 * Backend: POST /api/saas/compliance-scan (async) → poll
7 *          GET  /api/saas/compliance-scan/{scan_id}.
8 */
9function _escHtml(s) {
10    return String(s == null ? '' : s).replace(/[&<>"']/g, c => (
11        { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
12}
13
14const _CSCAN_ICON = { pass: '✅', warn: '🟡', fail: '🔴', manual: '📋', info: 'ℹ️' };
15const _CSCAN_ORDER = { fail: 0, warn: 1, manual: 2, pass: 3, info: 4 };
16
17function _cscanGradeColour(grade) {
18    return ({ A: '#16c784', B: '#7bc86c', C: '#f7b500', D: '#ff8c42', F: '#ef4444' })[grade] || '#9fb0d6';
19}
20
21async function renderCompliancePage(container) {
22    container.innerHTML = `
23    <div class="page-header">
24      <div>
25        <h1 class="page-title">Compliance Scan</h1>
26        <p class="page-subtitle">Rendered EU/UK governance scan of a site's public surface —
27          UK GDPR / DPA 2018, PECR (cookies), EU AI Act Art. 50, security headers, accessibility.</p>
28      </div>
29    </div>
30
31    <div class="card" id="cscan-form-card">
32      <div class="form-group">
33        <label class="form-label">Site URL</label>
34        <input id="cscan-url" type="url" class="form-input" placeholder="https://example.com" autocomplete="off">
35      </div>
36      <div class="form-group" style="max-width:200px">
37        <label class="form-label">Pages to render (max)</label>
38        <input id="cscan-maxpages" type="number" class="form-input" value="8" min="1" max="25">
39      </div>
40
41      <details id="cscan-auth" style="margin:8px 0 16px">
42        <summary style="cursor:pointer;color:var(--accent,#4f7cff);font-weight:600">
43          🔐 Authenticated scan (behind login) — Monitor plan and above
44        </summary>
45        <p class="text-muted" style="font-size:.85rem;margin:8px 0">
46          Supply a login so the scan reaches behind-login high-risk flows a public scan can't see.
47          Credentials are used only for this scan and never stored in the report.</p>
48        <div class="form-group"><input id="cscan-login-url" class="form-input" placeholder="Login page URL (e.g. https://example.com/login)"></div>
49        <div class="form-group"><input id="cscan-login-user" class="form-input" placeholder="Username / email" autocomplete="off"></div>
50        <div class="form-group"><input id="cscan-login-pass" type="password" class="form-input" placeholder="Password" autocomplete="new-password"></div>
51      </details>
52
53      <button id="cscan-run" class="btn btn-primary">Run compliance scan →</button>
54    </div>
55
56    <div class="card" id="cscan-progress" style="display:none">
57      <div class="flex items-center gap-sm">
58        <div class="spinner" style="width:18px;height:18px;border:2px solid rgba(255,255,255,.25);border-top-color:#fff;border-radius:50%;display:inline-block;animation:cscanspin .8s linear infinite"></div>
59        <span id="cscan-progress-text">Rendering &amp; scanning the site…</span>
60      </div>
61      <p class="text-muted" style="font-size:.82rem;margin-top:8px">This renders pages in a real browser, so it takes ~30–90s.</p>
62    </div>
63    <style>@keyframes cscanspin{to{transform:rotate(360deg)}}</style>
64
65    <div id="cscan-results"></div>
66
67    <div class="card" id="cscan-history-card">
68      <h3 style="margin:0 0 12px">Recent scans</h3>
69      <div id="cscan-history" class="text-muted">Loading…</div>
70    </div>
71  `;
72
73    document.getElementById('cscan-run').addEventListener('click', _cscanRun);
74    _cscanLoadHistory();
75
76    function _showProgress(show) {
77        document.getElementById('cscan-progress').style.display = show ? '' : 'none';
78        document.getElementById('cscan-run').disabled = show;
79    }
80
81    async function _cscanRun() {
82        const url = document.getElementById('cscan-url').value.trim();
83        if (!url) { NAILUI.toast('Enter a site URL to scan', 'warning'); return; }
84        const maxPages = Math.max(1, Math.min(parseInt(document.getElementById('cscan-maxpages').value) || 8, 25));
85        const loginUrl = document.getElementById('cscan-login-url').value.trim();
86        const user = document.getElementById('cscan-login-user').value.trim();
87        const pass = document.getElementById('cscan-login-pass').value;
88
89        const body = { url, max_pages: maxPages };
90        if (loginUrl && user && pass) {
91            body.auth = { login: { url: loginUrl, username: user, password: pass } };
92        }
93
94        document.getElementById('cscan-results').innerHTML = '';
95        _showProgress(true);
96        try {
97            const resp = await NAILAPI.post('/api/saas/compliance-scan', body);
98            if (!resp || !resp.scan_id) {
99                throw new Error(resp && resp.message ? resp.message : 'Could not start scan');
100            }
101            _cscanPoll(resp.scan_id);
102        } catch (err) {
103            _showProgress(false);
104            const msg = /upgrade|paid|monitor/i.test(err.message || '')
105                ? 'This scan tier requires a paid plan (authenticated scans need Monitor+).'
106                : (err.message || 'Could not start scan');
107            NAILUI.toast(msg, 'error', 6000);
108        }
109    }
110
111    function _cscanPoll(scanId) {
112        let attempts = 0;
113        const maxAttempts = 60; // ~5 min at 5s
114        const iv = setInterval(async () => {
115            attempts++;
116            try {
117                const r = await NAILAPI.get('/api/saas/compliance-scan/' + encodeURIComponent(scanId));
118                document.getElementById('cscan-progress-text').textContent =
119                    `Rendering & scanning the site… (${r.status})`;
120                if (r.status === 'complete') {
121                    clearInterval(iv); _showProgress(false);
122                    _cscanRenderResult(r); _cscanLoadHistory();
123                } else if (r.status === 'failed') {
124                    clearInterval(iv); _showProgress(false);
125                    NAILUI.toast('Scan failed: ' + (r.error || 'unknown error'), 'error', 6000);
126                }
127                if (attempts >= maxAttempts) {
128                    clearInterval(iv); _showProgress(false);
129                    NAILUI.toast('Scan is taking longer than expected — check Recent scans shortly.', 'warning', 6000);
130                    _cscanLoadHistory();
131                }
132            } catch (err) {
133                if (attempts >= maxAttempts) { clearInterval(iv); _showProgress(false); }
134            }
135        }, 5000);
136    }
137
138    function _cscanRenderResult(r) {
139        const res = r.result || {};
140        const findings = res.findings || [];
141        const counts = res.counts || {};
142        const grade = r.grade || res.grade || '—';
143        const score = (r.score != null ? r.score : res.score) || 0;
144        const colour = _cscanGradeColour(grade);
145        findings.sort((a, b) => (_CSCAN_ORDER[a.status] - _CSCAN_ORDER[b.status]));
146
147        const rows = findings.map(f => `
148      <tr>
149        <td>${_CSCAN_ICON[f.status] || ''} ${_escHtml(f.status)}</td>
150        <td><strong>${_escHtml(f.title)}</strong><br><span class="text-muted" style="font-size:.8rem">${_escHtml(f.id)}</span></td>
151        <td>${_escHtml(f.law)}<br><span class="text-muted" style="font-size:.8rem">${_escHtml(f.reference)}</span></td>
152        <td>${_escHtml(f.severity)}</td>
153        <td style="font-size:.85rem">${_escHtml(f.evidence)}${f.recommendation ? `<br><em style="color:var(--accent,#4f7cff)">→ ${_escHtml(f.recommendation)}</em>` : ''}</td>
154      </tr>`).join('');
155
156        document.getElementById('cscan-results').innerHTML = `
157      <div class="card">
158        <div class="flex items-center justify-between" style="flex-wrap:wrap;gap:12px">
159          <div>
160            <h2 style="margin:0">Compliance — ${_escHtml(r.url || res.target || '')}</h2>
161            <p class="text-muted" style="margin:2px 0;font-size:.85rem">
162              Mode: ${_escHtml(r.mode || 'public')} · ${(res.notes || []).map(_escHtml).join(' ')}</p>
163          </div>
164          <div style="text-align:center">
165            <div style="font-size:2.4rem;font-weight:800;color:${colour};line-height:1">${score}%</div>
166            <div style="font-weight:700;color:${colour}">Grade ${grade}</div>
167          </div>
168        </div>
169        <div class="flex gap-sm" style="margin:10px 0 4px;font-size:.9rem">
170          <span>🔴 ${counts.fail || 0} fail</span><span>🟡 ${counts.warn || 0} warn</span>
171          <span>✅ ${counts.pass || 0} pass</span><span>📋 ${counts.manual || 0} manual</span>
172        </div>
173        <div style="overflow-x:auto">
174          <table class="table" style="width:100%;margin-top:10px">
175            <thead><tr><th>Status</th><th>Check</th><th>Law</th><th>Sev</th><th>Evidence / fix</th></tr></thead>
176            <tbody>${rows || '<tr><td colspan="5">No findings.</td></tr>'}</tbody>
177          </table>
178        </div>
179        <p class="text-muted" style="font-size:.78rem;margin-top:10px">
180          ${_escHtml(res.disclaimer || 'Evidence-based gap assessment of the public surface — not a legal determination.')}</p>
181      </div>`;
182    }
183
184    async function _cscanLoadHistory() {
185        const el = document.getElementById('cscan-history');
186        if (!el) return;
187        try {
188            const r = await NAILAPI.get('/api/saas/compliance-scans');
189            const scans = (r && r.scans) || [];
190            if (!scans.length) { el.innerHTML = '<span class="text-muted">No scans yet.</span>'; return; }
191            el.innerHTML = `
192        <div style="overflow-x:auto"><table class="table" style="width:100%">
193          <thead><tr><th>URL</th><th>Mode</th><th>Status</th><th>Grade</th><th>When</th></tr></thead>
194          <tbody>${scans.map(s => `
195            <tr>
196              <td>${_escHtml(s.url)}</td>
197              <td>${_escHtml(s.mode)}</td>
198              <td>${_escHtml(s.status)}</td>
199              <td>${s.grade ? `<strong style="color:${_cscanGradeColour(s.grade)}">${_escHtml(s.grade)}</strong> (${s.score != null ? s.score + '%' : ''})` : '—'}</td>
200              <td class="text-muted" style="font-size:.82rem">${_escHtml((s.created_at || '').replace('T', ' ').slice(0, 16))}</td>
201            </tr>`).join('')}</tbody>
202        </table></div>`;
203        } catch (err) {
204            el.innerHTML = '<span class="text-muted">Could not load history.</span>';
205        }
206    }
207}
208
209NAILRouter.registerPage('compliance-scan', renderCompliancePage);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.