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:
co-authored by
Claude Sonnet 5
parent
3311bafd8e
commit
4a4910f207
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -190,6 +190,27 @@
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section id="privacy-section" class="parse-errors-section hidden">
|
||||
<div class="parse-errors-header" onclick="togglePrivacy()">
|
||||
<span id="privacy-title">Customer data</span>
|
||||
<span id="privacy-toggle" class="parse-errors-toggle">▲</span>
|
||||
</div>
|
||||
<div id="privacy-body" class="parse-errors-body">
|
||||
<div id="privacy-customer" class="privacy-customer"></div>
|
||||
<table class="parse-errors-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Severity</th>
|
||||
<th>Category</th>
|
||||
<th>Location</th>
|
||||
<th>Match</th>
|
||||
<th>Hint</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="privacy-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user