feat(privacy): scan ingested sources for customer-identifying data

Detection-only scan (internal/privacy) attached to every AnalysisResult:
a customer-domain guess plus a findings list (category, file, line, match,
hint), ported from the KB grep playbook. Runs on archive uploads and the
serialized Redfish tree; gated by LOGPILE_PRIVACY_SCAN (default on).

Surfaced at GET /api/privacy-scan, in the "Customer data" UI panel, and as
privacy_report.json in the raw-export bundle. IP policy keeps RFC1918 and
example ranges out of findings; allowlist covers standards-body and vendor
infrastructure domains. No customer tokens in the repo. See ADL-066 and
bible-local/docs/privacy-scan.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-09-02 15:24:32 +03:00
co-authored by Claude Sonnet 5
parent 3311bafd8e
commit 4a4910f207
23 changed files with 1466 additions and 33 deletions
+45
View File
@@ -1001,3 +1001,48 @@ code {
color: #7a5200;
font-weight: 600;
}
.privacy-customer {
padding: 10px 12px;
border-bottom: 1px solid var(--border);
background: #fff8f0;
}
.privacy-customer.hidden {
display: none;
}
.privacy-customer-row {
margin-bottom: 6px;
}
.privacy-conf {
font-size: 0.8em;
text-transform: uppercase;
padding: 1px 6px;
border-radius: 3px;
background: var(--border);
}
.privacy-conf-high {
color: var(--crit-fg);
font-weight: 600;
}
.privacy-conf-medium {
color: #7a5200;
font-weight: 600;
}
.privacy-hits {
color: var(--muted);
font-size: 0.85em;
}
.privacy-evidence {
margin: 4px 0 0;
padding-left: 18px;
font-family: monospace;
font-size: 0.82em;
color: var(--muted);
}
+91
View File
@@ -1415,6 +1415,86 @@ async function loadData(vendor, filename) {
loadAuditViewer();
loadParseErrors();
loadPrivacyScan();
}
async function loadPrivacyScan() {
const section = document.getElementById('privacy-section');
const rows = document.getElementById('privacy-rows');
const title = document.getElementById('privacy-title');
const customer = document.getElementById('privacy-customer');
if (!section || !rows) return;
let data;
try {
const resp = await fetch('/api/privacy-scan');
if (!resp.ok) return;
data = await resp.json();
} catch (e) {
return;
}
if (!data || data.loaded === false) {
section.classList.add('hidden');
return;
}
const findings = Array.isArray(data.findings) ? data.findings : [];
const customers = Array.isArray(data.customers) ? data.customers : [];
if (findings.length === 0 && customers.length === 0) {
section.classList.add('hidden');
return;
}
const s = data.summary || {};
const parts = [];
if (s.high) parts.push(`${s.high} high`);
if (s.medium) parts.push(`${s.medium} medium`);
if (s.low) parts.push(`${s.low} low`);
const lead = customers.length > 0 ? `likely ${customers[0].domain}` : `${findings.length} finding${findings.length > 1 ? 's' : ''}`;
title.textContent = `Customer data — ${lead}${parts.length ? ' · ' + parts.join(', ') : ''}`;
if (customers.length > 0) {
customer.innerHTML = customers.map(c =>
`<div class="privacy-customer-row"><strong>${escapeHtml(c.domain)}</strong> ` +
`<span class="privacy-conf privacy-conf-${escapeHtml(c.confidence || '')}">${escapeHtml(c.confidence || '')}</span> ` +
`<span class="privacy-hits">${c.hits || 0} hit${(c.hits || 0) === 1 ? '' : 's'}</span>` +
((c.evidence && c.evidence.length)
? `<ul class="privacy-evidence">${c.evidence.map(e => `<li>${escapeHtml(e)}</li>`).join('')}</ul>`
: '') +
`</div>`
).join('');
customer.classList.remove('hidden');
} else {
customer.innerHTML = '';
customer.classList.add('hidden');
}
rows.innerHTML = '';
for (const f of findings) {
const loc = f.line ? `${f.path}:${f.line}` : (f.path || '');
const tr = document.createElement('tr');
tr.className = `parse-error-row parse-error-${f.severity === 'high' ? 'error' : (f.severity === 'medium' ? 'warning' : 'info')}`;
tr.innerHTML =
`<td>${escapeHtml(f.severity || '')}</td>` +
`<td>${escapeHtml(f.category || '')}</td>` +
`<td>${escapeHtml(loc)}</td>` +
`<td>${escapeHtml(f.match || '')}</td>` +
`<td>${escapeHtml(f.hint || '')}</td>`;
rows.appendChild(tr);
}
section.classList.remove('hidden');
}
let privacyCollapsed = false;
function togglePrivacy() {
const body = document.getElementById('privacy-body');
const toggle = document.getElementById('privacy-toggle');
if (!body) return;
privacyCollapsed = !privacyCollapsed;
body.style.display = privacyCollapsed ? 'none' : '';
toggle.textContent = privacyCollapsed ? '▼' : '▲';
}
async function loadParseErrors() {
@@ -1535,6 +1615,17 @@ async function clearData() {
if (parseErrBody) parseErrBody.style.display = '';
const parseErrToggle = document.getElementById('parse-errors-toggle');
if (parseErrToggle) parseErrToggle.textContent = '▲';
const privacySection = document.getElementById('privacy-section');
if (privacySection) privacySection.classList.add('hidden');
const privacyRows = document.getElementById('privacy-rows');
if (privacyRows) privacyRows.innerHTML = '';
const privacyCustomer = document.getElementById('privacy-customer');
if (privacyCustomer) privacyCustomer.innerHTML = '';
privacyCollapsed = false;
const privacyBody = document.getElementById('privacy-body');
if (privacyBody) privacyBody.style.display = '';
const privacyToggle = document.getElementById('privacy-toggle');
if (privacyToggle) privacyToggle.textContent = '▲';
} catch (err) {
console.error('Failed to clear data:', err);
}