Samstag, 20 Dezember 2025

Diese Woche am beliebtesten

Vertiefendes Material

DMARC XML to Human

DMARC-Berichte liefern wichtige Informationen über die Authentizität und Sicherheit von E-Mails. Das Problem: Diese Reports werden standardmäßig als komplexe XML-Dateien bereitgestellt, die für Menschen kaum lesbar sind. Genau hier setzt unser Tool an: Der DMARC XML to Human Converter übersetzt rohe DMARC-Feedback-Dateien in eine klar strukturierte, leicht verständliche Darstellung.

Funktionen:

  • Drag & Drop Upload: Einfach DMARC XML-Dateien, ZIP-Archive oder GZIP-Dateien hochladen.
  • Automatische Entpackung & Parsing: Das Tool erkennt das Dateiformat, entpackt es bei Bedarf und liest die enthaltenen XML-Daten.
  • Menschenlesbare Darstellung: Wichtige Informationen wie Absender-Domain, Richtlinien, Auswertungen und Authentifizierungsergebnisse (SPF, DKIM) werden übersichtlich angezeigt.
  • Übersicht der Quellen: Zeigt IP-Adressen, Absenderdomains und deren Bewertung nach den DMARC-Regeln.
  • Exportfunktionen: Möglichkeit, die Ergebnisse als JSON herunterzuladen oder zu kopieren.
  • Lokale Verarbeitung: Alle Daten werden ausschließlich im Browser verarbeitet – es erfolgt keine Weitergabe an Dritte.
  • Optionaler Server-Upload: Für Unternehmen besteht die Möglichkeit, Reports per AJAX automatisch an einen definierten Server zu übertragen.

Vorteile:

  • Kein mühsames Durchsuchen von XML-Dateien mehr.
  • Schnelles Erkennen von Problemen bei SPF- oder DKIM-Prüfungen.
  • Ideal für Administratoren, Sicherheitsverantwortliche und Unternehmen, die DMARC-Monitoring einsetzen.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Dreamcodes - DMARC XML to Human Converter</title>

<!-- Externe Bibliotheken per CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js" integrity="sha512-+sX8QfZyZQ7Q7r2b/3t1p3kJ6s6pJ3h8h2eQqGv5k7b8XbFf0oZkY9y0L6n5f2qHqv8h8+Yq5sHkq8Y1p6mMw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js" integrity="sha512-4lFj5qON7bXn14G1+Zl0E0wP9Q+o5j2b6g2Q3qZ2p7sY8c3uK6q9r1e7o4p8u9v1w2Y3z5m7n8p9q2z1o7k3g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<style>
  :root{
    --bg:#f6f7fb;
    --card:#ffffff;
    --accent:#1766a6;
    --muted:#6b7280;
    --success:#0f9d58;
  }
  body{
    margin:0;
    font-family:Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
    background:var(--bg);
    color:#0f172a;
    -webkit-font-smoothing:antialiased;
  }
  header{
    background:linear-gradient(90deg,#0e5fa4, #0b72b9);
    color:white;
    padding:28px 20px;
    display:flex;
    align-items:center;
    gap:16px;
  }
  header h1{ margin:0; font-size:20px; font-weight:600; }
  header p{ margin:0; opacity:0.9; font-size:13px; }
  .container{ max-width:1100px; margin:28px auto; padding:0 20px; }
  .drop{
    background:linear-gradient(180deg, rgba(255,255,255,0.8), rgba(255,255,255,0.6));
    border:2px dashed rgba(23,102,166,0.15);
    border-radius:10px;
    padding:28px;
    display:flex;
    gap:18px;
    align-items:center;
    justify-content:space-between;
    box-shadow: 0 6px 18px rgba(16,24,40,0.04);
  }
  .drop-left{ display:flex; gap:18px; align-items:center; }
  .drop .info{ max-width:720px; }
  .drop h2{ margin:0; font-size:18px; color:var(--accent); }
  .drop p{ margin:6px 0 0; color:var(--muted); font-size:13px; line-height:1.4; }
  .actions{ display:flex; gap:10px; align-items:center; }
  .btn{
    background:var(--accent);
    color:white;
    border:none;
    padding:10px 14px;
    border-radius:8px;
    cursor:pointer;
    font-weight:600;
  }
  .btn.secondary{
    background:#fff;
    color:var(--accent);
    border:1px solid rgba(23,102,166,0.12);
  }
  input[type=file]{ display:none; }
  .list{ margin-top:18px; display:grid; gap:12px; }
  .card{
    background:var(--card);
    border-radius:10px;
    padding:16px;
    box-shadow: 0 6px 18px rgba(16,24,40,0.04);
  }
  .meta{ display:flex; gap:12px; flex-wrap:wrap; font-size:13px; color:var(--muted); }
  .key{ color:var(--accent); font-weight:700; margin-right:6px; }
  .section-title{ font-size:15px; margin:12px 0 8px; color:#0f172a; }
  table{ width:100%; border-collapse:collapse; font-size:13px; }
  th, td{ text-align:left; padding:8px; border-bottom:1px solid #f1f5f9; }
  th{ color:var(--muted); font-weight:600; font-size:12px; }
  pre{ background:#0b1720; color:#e6eef6; padding:12px; border-radius:8px; overflow:auto; font-size:13px; }
  .small{ font-size:12px; color:var(--muted); }
  .controls{ display:flex; gap:10px; margin-top:12px; flex-wrap:wrap; }
  .badge{ background:#eef6fb; color:var(--accent); padding:6px 8px; border-radius:999px; font-weight:700; font-size:12px; }
  .footer-note{ font-size:13px; color:var(--muted); margin-top:12px; }
  .row{ display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
  .export-btn{ background:var(--success); border:none; color:white; padding:8px 12px; border-radius:8px; cursor:pointer; }
  .danger{ background:#ef4444; color:white; padding:8px 12px; border-radius:8px; border:none; cursor:pointer; }
  .toggle{ display:inline-flex; gap:6px; align-items:center; background:#fff; padding:6px 8px; border-radius:8px; border:1px solid #e6eef6; }
  .empty{ color:var(--muted); padding:24px; text-align:center; border-radius:10px; background:linear-gradient(180deg, #fff, #fbfdff); border:1px dashed #edf2f7; }
</style>
</head>
<body>
<header>
  <div style="display:flex;flex-direction:column;">
    <h1>Dreamcodes - DMARC XML to Human Converter</h1>
    <p>DMARC Reports lesbar machen, Fehler verstehen und Maßnahmen ableiten</p>
  </div>
</header>

<div class="container">
  <div class="drop" id="dropzone">
    <div class="drop-left">
      <div style="width:64px;height:64px;border-radius:12px;background:#eaf6ff;display:flex;align-items:center;justify-content:center">
        <svg width="34" height="34" viewBox="0 0 24 24" fill="none" aria-hidden><path d="M12 2v13" stroke="#0369a1" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M5 10l7-7 7 7" stroke="#0369a1" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
      </div>
      <div class="info">
        <h2>Dreamcodes - Drag und Drop DMARC XML Dateien hierher</h2>
        <p>Unterstützte Formate, unkomprimiertes XML, gzip oder zip. Du kannst beliebig viele Dateien hochladen. Die Dateien werden lokal im Browser verarbeitet.</p>
      </div>
    </div>

    <div class="actions">
      <label for="fileInput" class="btn" title="Dateien wählen">Dateien wählen</label>
      <input id="fileInput" type="file" multiple />
      <button id="clearAll" class="btn secondary">Alles entfernen</button>
      <div style="font-size:12px;color:#fff;background:rgba(255,255,255,0.06);padding:6px 8px;border-radius:8px" class="small">Lokal im Browser</div>
    </div>
  </div>

  <div id="output" class="list" style="margin-top:16px;">
    <div class="card empty" id="emptyState">
      Ziehe DMARC XML Dateien hierher oder wähle Dateien aus, um sie lesbar aufzubereiten
    </div>
  </div>

  <div style="margin-top:18px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
    <div class="toggle" title="Optionen">
      <input type="checkbox" id="showRaw" /> <label for="showRaw" style="font-size:13px;color:#0f172a">Rohdaten anzeigen</label>
    </div>
    <button id="exportAll" class="export-btn">Alle als JSON exportieren</button>
    <button id="uploadServer" class="btn secondary">Optionalen Upload per AJAX</button>
  </div>
  <p class="footer-note">
  Hinweis: Die Verarbeitung geschieht ausschließlich im Browser. 
  Die Dateien werden nicht ohne dein Zutun an einen Server gesendet. 
  Mehr Infos findest du bei 
  <a href="http://www.dreamcodes.net" target="_blank" rel="noopener noreferrer">Dreamcodes</a>.
</p>
</div>
<script>
const $ = id => document.getElementById(id);
const fileInput = $('fileInput');
const dropzone = $('dropzone');
const output = $('output');
const empty = $('emptyState');
const clearAllBtn = $('clearAll');
const exportAllBtn = $('exportAll');
const uploadServerBtn = $('uploadServer');
const showRaw = $('showRaw');
let processedReports = []; // Array mit Objekten { filename, xmlString, parsed }
function setEmptyState(show){
  empty.style.display = show ? 'block' : 'none';
}
function createCard(fileName, parsed, rawXml){
  const card = document.createElement('div');
  card.className='card';
  const header = document.createElement('div');
  header.className='row';
  const title = document.createElement('div');
  title.innerHTML = `<div style="font-weight:700">${escapeHtml(fileName)}</div><div class="small">DMARC Report Übersicht</div>`;
  header.appendChild(title);
  const controls = document.createElement('div');
  controls.style.marginLeft='auto';
  controls.className='row';
  const exportBtn = document.createElement('button');
  exportBtn.className='export-btn';
  exportBtn.textContent='JSON herunterladen';
  exportBtn.onclick = () => downloadJSON(parsed, fileName + '.json');
  const copyBtn = document.createElement('button');
  copyBtn.className='btn secondary';
  copyBtn.textContent='In Zwischenablage kopieren';
  copyBtn.onclick = async () => {
    await navigator.clipboard.writeText(JSON.stringify(parsed, null, 2));
    alert('JSON in die Zwischenablage kopiert');
  };
  const toggleRawBtn = document.createElement('button');
  toggleRawBtn.className='btn secondary';
  toggleRawBtn.textContent='Roh anzeigen';
  toggleRawBtn.onclick = () => {
    const pre = card.querySelector('pre');
    pre.style.display = pre.style.display === 'none' ? 'block' : 'none';
  };
  controls.appendChild(exportBtn);
  controls.appendChild(copyBtn);
  controls.appendChild(toggleRawBtn);
  header.appendChild(controls);
  card.appendChild(header);
  const meta = document.createElement('div');
  meta.className='meta';
  const m = parsed.report_metadata || {};
  meta.innerHTML = `
    <div><span class="key">Organisation</span> ${escapeHtml(m.org_name || 'unbekannt')}</div>
    <div><span class="key">Report ID</span> ${escapeHtml(m.report_id || 'n/a')}</div>
    <div><span class="key">Datum von</span> ${formatEpoch(m.begin)} bis ${formatEpoch(m.end)}</div>
  `;
  card.appendChild(meta);
  if(parsed.policy_published){
    const s = document.createElement('div');
    s.innerHTML = `<div class="section-title">Richtlinie</div>
      <div class="small">Domain: <strong>${escapeHtml(parsed.policy_published.domain || '')}</strong>, Policy: <strong>${escapeHtml(parsed.policy_published.p || '')}</strong>, Subdomain Policy: <strong>${escapeHtml(parsed.policy_published.sp||'n/a')}</strong>, Tengrade: <strong>${escapeHtml(parsed.policy_published.pct||'100')}</strong></div>`;
    card.appendChild(s);
  }
  if(parsed.records && parsed.records.length){
    const tableTitle = document.createElement('div');
    tableTitle.className='section-title';
    tableTitle.textContent = `Beobachtete Quellen ${parsed.records.length} Einträge`;
    card.appendChild(tableTitle);
    const table = document.createElement('table');
    const thead = document.createElement('thead');
    thead.innerHTML = '<tr><th>IP</th><th>Count</th><th>Resultat</th><th>Header From</th><th>SPF</th><th>DKIM</th></tr>';
    table.appendChild(thead);
    const tbody = document.createElement('tbody');
    parsed.records.forEach(r => {
      const tr = document.createElement('tr');
      const ip = r.row && r.row.source_ip ? r.row.source_ip : '';
      const count = r.row && r.row.count ? r.row.count : '';
      const res = r.row && r.row.policy_evaluated ? `${r.row.policy_evaluated.disposition || ''} / dkim:${r.row.policy_evaluated.dkim || ''} / spf:${r.row.policy_evaluated.spf || ''}` : '';
      const headerFrom = r.identifiers && r.identifiers.header_from ? r.identifiers.header_from : '';
      const spf = r.auth_results && r.auth_results.spf ? summarizeAuth(r.auth_results.spf) : '';
      const dkim = r.auth_results && r.auth_results.dkim ? summarizeAuth(r.auth_results.dkim) : '';
      tr.innerHTML = `<td>${escapeHtml(ip)}</td><td>${escapeHtml(String(count))}</td><td>${escapeHtml(res)}</td><td>${escapeHtml(headerFrom)}</td><td>${escapeHtml(spf)}</td><td>${escapeHtml(dkim)}</td>`;
      tbody.appendChild(tr);
    });
    table.appendChild(tbody);
    card.appendChild(table);
  } else {
    const noRec = document.createElement('div');
    noRec.className='small';
    noRec.textContent = 'Keine Einträge im Report gefunden';
    card.appendChild(noRec);
  }
  const pre = document.createElement('pre');
  pre.style.display = showRaw && showRaw.checked ? 'block' : 'none';
  pre.textContent = rawXml || '';
  card.appendChild(pre);

  return card;
}
function escapeHtml(input){
  if(!input && input !== 0) return '';
  return String(input).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('"','&quot;');
}
function formatEpoch(epoch){
  if(!epoch) return 'n/a';
  try {
    const ms = Number(epoch) * 1000;
    if(isNaN(ms)) return 'n/a';
    const d = new Date(ms);
    return d.toISOString().replace('T',' ').replace('.000Z',' UTC');
  } catch(e){ return 'n/a'; }
}
function summarizeAuth(arr){
  if(!arr) return '';
  if(Array.isArray(arr)){
    return arr.map(a => `${a.domain||''}:${a.result||''}`).join(', ');
  } else if(typeof arr === 'object'){
    return `${arr.domain||''}:${arr.result||''}`;
  }
  return String(arr);
}
function downloadJSON(obj, filename){
  const blob = new Blob([JSON.stringify(obj, null, 2)], {type:'application/json'});
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href=url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
}
function parseDmarcXml(xmlString){
  const parser = new DOMParser();
  const doc = parser.parseFromString(xmlString, 'application/xml');
  const parseError = doc.querySelector('parsererror');
  if(parseError) {
    // Falls kein gültiges XML
    throw new Error('Ungültiges XML');
  }

  const getText = (parent, tag) => {
    const el = parent.querySelector(tag);
    return el ? el.textContent.trim() : null;
  };

  const root = doc.documentElement;
  // Report metadata
  const report_metadata = {};
  const rm = root.querySelector('report_metadata');
  if(rm){
    report_metadata.org_name = getText(rm, 'org_name');
    report_metadata.report_id = getText(rm, 'report_id');
    const dateRange = rm.querySelector('date_range');
    if(dateRange){
      report_metadata.begin = getText(dateRange, 'begin');
      report_metadata.end = getText(dateRange, 'end');
    }
  }

  // Policy published
  const policy_published = {};
  const pp = root.querySelector('policy_published');
  if(pp){
    policy_published.domain = getText(pp, 'domain');
    policy_published.adkim = getText(pp, 'adkim');
    policy_published.aspf = getText(pp, 'aspf');
    policy_published.p = getText(pp, 'p');
    policy_published.sp = getText(pp, 'sp');
    policy_published.pct = getText(pp, 'pct');
  }

  // Records
  const records = [];
  const recEls = root.querySelectorAll('record');
  recEls.forEach(rEl => {
    const row = {};
    const rowEl = rEl.querySelector('row');
    if(rowEl){
      row.source_ip = getText(rowEl, 'source_ip');
      row.count = getText(rowEl, 'count');
      const pol = rowEl.querySelector('policy_evaluated');
      if(pol){
        row.policy_evaluated = {
          disposition: getText(pol, 'disposition'),
          dkim: getText(pol, 'dkim'),
          spf: getText(pol, 'spf')
        };
      }
    }
    const identifiers = {};
    const idEl = rEl.querySelector('identifiers');
    if(idEl){
      identifiers.header_from = getText(idEl, 'header_from');
      identifiers.envelope_to = getText(idEl, 'envelope_to');
      identifiers.envelope_from = getText(idEl, 'envelope_from');
    }
    const auth_results = {};
    const arEl = rEl.querySelector('auth_results');
    if(arEl){
      // dkim may be multiple
      const dkimEls = arEl.querySelectorAll('dkim');
      if(dkimEls.length){
        auth_results.dkim = Array.from(dkimEls).map(dk=>({
          domain: getText(dk, 'domain'),
          selector: getText(dk, 'selector'),
          result: getText(dk, 'result')
        }));
      }
      const spfEls = arEl.querySelectorAll('spf');
      if(spfEls.length){
        auth_results.spf = Array.from(spfEls).map(spf=>({
          domain: getText(spf, 'domain'),
          scope: getText(spf, 'scope'),
          result: getText(spf, 'result')
        }));
      }
    }

    records.push({row, identifiers, auth_results});
  });

  return {
    report_metadata,
    policy_published,
    records
  };
}
async function handleFile(file){
  const name = file.name || 'unknown';
  const arrBuf = await file.arrayBuffer();
  const bytes = new Uint8Array(arrBuf);
  const isGzip = bytes[0] === 0x1f && bytes[1] === 0x8b;
  const isZip = bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04;
  if(isGzip){
    try {
      const decompressed = pako.ungzip(bytes, { to: 'string' });
      processXmlString(name.replace(/\.gz$/i, ''), decompressed);
    } catch(e){
      console.error('GZIP Fehler', e);
      alert('Fehler beim Entpacken von gzip Datei ' + name);
    }
  } else if(isZip){
    try {
      const jszip = await JSZip.loadAsync(bytes);
      let found = 0;
      await Promise.all(
        Object.keys(jszip.files).map(async key => {
          const f = jszip.files[key];
          if(!f.dir && /(\.xml)$/i.test(f.name)){
            found++;
            const content = await f.async('string');
            processXmlString(f.name, content);
          } else if(!f.dir && /\.(xml|xml\.txt)$/i.test(f.name)){
            found++;
            const content = await f.async('string');
            processXmlString(f.name, content);
          } else if(!f.dir && /\.(gz)$/i.test(f.name)){
            // handle nested gz in zip
            const data = await f.async('uint8array');
            try {
              const dec = pako.ungzip(data, { to: 'string' });
              processXmlString(f.name.replace(/\.gz$/i,''), dec);
              found++;
            } catch(e){}
          }
        })
      );
      if(found === 0){
        alert('Kein XML in der ZIP Datei gefunden: ' + name);
      }
    } catch(e){
      console.error('ZIP Fehler', e);
      alert('Fehler beim Auslesen der ZIP Datei ' + name);
    }
  } else {
    // Assume plain XML text
    const text = new TextDecoder().decode(bytes);
    // If file extension .xml or content starts with <?xml or <feedback
    const trimmed = text.trim();
    if(trimmed.startsWith('<') && trimmed.length > 20){
      processXmlString(name, text);
    } else {
      alert('Unbekanntes Format: ' + name);
    }
  }
}
function processXmlString(filename, xmlString){
  try {
    const parsed = parseDmarcXml(xmlString);
    processedReports.push({ filename, xmlString, parsed });
    renderAll();
  } catch(e){
    console.error('Parse Fehler', e);
    alert('Fehler beim Parsen der Datei ' + filename + '. Datei ist möglicherweise kein gültiger DMARC XML Bericht.');
  }
}
function renderAll(){
  // clear
  output.innerHTML = '';
  if(processedReports.length === 0){
    setEmptyState(true);
    output.appendChild(empty);
    return;
  }
  setEmptyState(false);
  processedReports.forEach(r => {
    const card = createCard(r.filename, r.parsed, r.xmlString);
    output.appendChild(card);
  });
}
// Drag and drop events
['dragenter','dragover'].forEach(evt => {
  dropzone.addEventListener(evt, e => {
    e.preventDefault();
    dropzone.style.borderColor = 'rgba(23,102,166,0.4)';
  });
});
['dragleave','drop'].forEach(evt => {
  dropzone.addEventListener(evt, e => {
    e.preventDefault();
    dropzone.style.borderColor = 'rgba(23,102,166,0.15)';
  });
});
dropzone.addEventListener('drop', async e => {
  const dt = e.dataTransfer;
  const files = Array.from(dt.files || []);
  if(files.length === 0) return;
  for(const f of files) await handleFile(f);
});

// File input
fileInput.addEventListener('change', async e => {
  const files = Array.from(e.target.files || []);
  for(const f of files) await handleFile(f);
  fileInput.value = '';
});

// Clear all
clearAllBtn.addEventListener('click', () => {
  if(!confirm('Alle verarbeiteten Reports entfernen?')) return;
  processedReports = [];
  renderAll();
});

// Export all JSON
exportAllBtn.addEventListener('click', () => {
  if(processedReports.length === 0){ alert('Keine Reports zum Exportieren'); return; }
  const payload = processedReports.map(p => ({ filename: p.filename, parsed: p.parsed }));
  downloadJSON(payload, 'dmarc_reports.json');
});

// Optionaler AJAX Upload
uploadServerBtn.addEventListener('click', async () => {
  if(processedReports.length === 0){ alert('Keine Reports vorhanden'); return; }
  const url = prompt('Ziel URL für Upload eingeben, z. B. https://example.com/api/dmarc', '');
  if(!url) return;
  try {
    // Beispiel: POST JSON
    const payload = processedReports.map(p => ({ filename: p.filename, parsed: p.parsed }));
    const resp = await fetch(url, {
      method:'POST',
      headers:{ 'Content-Type':'application/json' },
      body: JSON.stringify({ reports: payload })
    });
    if(!resp.ok) throw new Error('Upload fehlgeschlagen ' + resp.status);
    alert('Upload erfolgreich');
  } catch(err){
    console.error(err);
    alert('Fehler beim Upload: ' + err.message);
  }
});

// init
setEmptyState(true);
renderAll();

</script>
</body>
</html>
Dreamcodes Redaktion
Dreamcodes Redaktion
Jeder auf Dreamcodes bereitgestellte Codeschnipsel sowie jede Tutorial Anleitung basiert auf geprüften Best Practices und fundierter Praxiserfahrung. Ziel ist es, ein belastbares technisches Fundament bereitzustellen und keine unausgereiften oder experimentellen Lösungen zu veröffentlichen. Die konkrete Nutzung, Integration, Anpassung und Absicherung der Inhalte obliegt jedoch dem Anwender. Vor dem produktiven Einsatz sind sämtliche Inhalte eigenverantwortlich zu prüfen, zu testen und gegebenenfalls abzusichern. Dreamcodes stellt die technische Grundlage zur Verfügung, die finale Umsetzung und Verantwortung verbleibt beim Nutzer.
Vorheriges Tutorial
Nächstes Tutorial

Vielleicht einen Blick WERT?