feat(sanitize): in-place, length-preserving log de-identification
Adds internal/sanitize: rewrites the customer-identifying spans that internal/privacy detects (domain/FQDN/e-mail/AD/public-IP/timezone) with same-length neutral fillers, in place, without changing the file format. - Fillers keep byte length: "sigma.sbrf.ru" -> "xxxxx.xxxx.xx", IP -> "00.000.000.00", "Europe/Moscow" -> "Etc/Universal" (same-length valid neutral IANA zone), offset "180" -> "000". Timestamps are not recomputed. - Lossless recursive archive walk (tar/.sds/gz/tgz/zip): entry names, modes, and all embedded timestamps preserved; untouched zip entries copied raw; member payload length unchanged so tar headers stay byte-identical; only the .gz/.zip compression layer is rebuilt. 0 redactions -> byte-identical output. - privacy.FindSpans is the one matcher shared by detection and redaction; fillers are recognised by isRedactionFiller so a re-scan / second pass is a no-op. New privacy FPs fixed along the way: syslog selectors (local7.info), "MEVersion" firmware quads, *.conf_bak vendor templates, bundled viewer domains. - Binary members (FRU.bin, localtime, redis-dump.rdb, SOL captures) and unreadable nested archives are reported in Result.SkippedBinary, never edited. - Surfaces: POST /api/sanitize (+ GET /api/sanitize/download), the "Обезличить и скачать копию" button in the Customer-data panel, and logpile -sanitize <file> (restores mtime/atime). Verified: re-parsing a sanitized Dell TSR / xFusion / Inspur onekeylog / H3C .sds yields the identical hardware inventory; re-scan is clean. ADL-067, bible-local/docs/log-sanitization.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e74e01ad05
commit
a63bb17438
@@ -1046,3 +1046,53 @@ code {
|
||||
font-size: 0.82em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.privacy-sanitize {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.privacy-sanitize.hidden,
|
||||
.privacy-sanitize-preview.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#privacy-sanitize-btn,
|
||||
.privacy-sanitize-dl {
|
||||
font-size: 0.85em;
|
||||
padding: 5px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.privacy-sanitize-preview {
|
||||
margin-top: 10px;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
.privacy-sanitize-preview table {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.privacy-sanitize-dl {
|
||||
margin-top: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.privacy-sanitize-warn {
|
||||
margin-top: 8px;
|
||||
padding: 6px 8px;
|
||||
background: #fff8f0;
|
||||
border: 1px solid #f0e0c0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.privacy-sanitize-warn ul {
|
||||
margin: 4px 0 0;
|
||||
padding-left: 18px;
|
||||
font-family: monospace;
|
||||
font-size: 0.82em;
|
||||
}
|
||||
|
||||
.privacy-sanitize-err {
|
||||
color: var(--crit-fg);
|
||||
}
|
||||
|
||||
@@ -1497,9 +1497,57 @@ async function loadPrivacyScan() {
|
||||
if (findingsBox) findingsBox.style.display = privacyCollapsed ? 'none' : '';
|
||||
}
|
||||
|
||||
const sanBox = document.getElementById('privacy-sanitize');
|
||||
if (sanBox) {
|
||||
sanBox.classList.toggle('hidden', !data.sanitizable);
|
||||
const prev = document.getElementById('privacy-sanitize-preview');
|
||||
if (prev) { prev.classList.add('hidden'); prev.innerHTML = ''; }
|
||||
const btn = document.getElementById('privacy-sanitize-btn');
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Обезличить и скачать копию'; }
|
||||
}
|
||||
|
||||
section.classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function sanitizePreview() {
|
||||
const btn = document.getElementById('privacy-sanitize-btn');
|
||||
const prev = document.getElementById('privacy-sanitize-preview');
|
||||
if (!btn || !prev) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Обработка…';
|
||||
let data;
|
||||
try {
|
||||
const resp = await fetch('/api/sanitize', { method: 'POST' });
|
||||
data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data && data.error ? data.error : 'sanitize failed');
|
||||
} catch (e) {
|
||||
prev.innerHTML = `<div class="privacy-sanitize-err">${escapeHtml(String(e.message || e))}</div>`;
|
||||
prev.classList.remove('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Обезличить и скачать копию';
|
||||
return;
|
||||
}
|
||||
|
||||
const changes = Array.isArray(data.changes) ? data.changes : [];
|
||||
const skipped = Array.isArray(data.skipped_binary) ? data.skipped_binary : [];
|
||||
let html = `<p><strong>${data.total_replaced}</strong> замен в <strong>${changes.length}</strong> файл(ах). ` +
|
||||
`Формат, имена и даты записей сохранены; правки той же длины.</p>`;
|
||||
if (changes.length) {
|
||||
html += '<table class="parse-errors-table"><thead><tr><th>Категория</th><th>Кол-во</th><th>Файл</th></tr></thead><tbody>' +
|
||||
changes.map(c => `<tr><td>${escapeHtml(c.category)}</td><td>${c.count}</td><td>${escapeHtml(c.path)}</td></tr>`).join('') +
|
||||
'</tbody></table>';
|
||||
}
|
||||
if (skipped.length) {
|
||||
html += `<div class="privacy-sanitize-warn"><strong>Не удалось отредактировать (обезличьте вручную):</strong><ul>` +
|
||||
skipped.map(s => `<li>${escapeHtml(s)}</li>`).join('') + '</ul></div>';
|
||||
}
|
||||
html += `<button type="button" class="privacy-sanitize-dl" onclick="window.location='/api/sanitize/download'">Скачать обезличенную копию</button>`;
|
||||
prev.innerHTML = html;
|
||||
prev.classList.remove('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Пересобрать';
|
||||
}
|
||||
|
||||
// The header + customer summary stay visible; only the findings table collapses.
|
||||
let privacyCollapsed = true;
|
||||
function togglePrivacy() {
|
||||
@@ -1640,6 +1688,10 @@ async function clearData() {
|
||||
if (privacyFindings) privacyFindings.style.display = 'none';
|
||||
const privacyToggle = document.getElementById('privacy-toggle');
|
||||
if (privacyToggle) { privacyToggle.textContent = '▼'; privacyToggle.style.visibility = ''; }
|
||||
const privacySan = document.getElementById('privacy-sanitize');
|
||||
if (privacySan) privacySan.classList.add('hidden');
|
||||
const privacySanPrev = document.getElementById('privacy-sanitize-preview');
|
||||
if (privacySanPrev) { privacySanPrev.innerHTML = ''; privacySanPrev.classList.add('hidden'); }
|
||||
} catch (err) {
|
||||
console.error('Failed to clear data:', err);
|
||||
}
|
||||
|
||||
@@ -166,6 +166,10 @@
|
||||
</div>
|
||||
<div id="privacy-body" class="parse-errors-body">
|
||||
<div id="privacy-customer" class="privacy-customer"></div>
|
||||
<div id="privacy-sanitize" class="privacy-sanitize hidden">
|
||||
<button id="privacy-sanitize-btn" type="button" onclick="sanitizePreview()">Обезличить и скачать копию</button>
|
||||
<div id="privacy-sanitize-preview" class="privacy-sanitize-preview hidden"></div>
|
||||
</div>
|
||||
<div id="privacy-findings" class="privacy-findings" style="display:none">
|
||||
<table class="parse-errors-table">
|
||||
<thead>
|
||||
|
||||
Reference in New Issue
Block a user