Quelltext des aktuellen Standes
<?php $this->layout('layouts/admin', [
'page_title' => 'CVE-Einstellungen',
'current_admin_page' => 'cve-settings',
]); ?>
<?php $nvdKeySet = !empty($nvd_api_key_set); ?>
<div class="settings-page">
<section class="settings-card">
<div class="settings-card-header">
<span class="settings-card-icon">🛡</span>
<div>
<h2><?= $this->te('NVD-API-Zugang') ?></h2>
<p class="settings-card-desc"><?= $this->te('Schlüssel für den Abruf der Sicherheitsmeldungen aus der National Vulnerability Database.') ?></p>
</div>
</div>
<div class="settings-card-body">
<p style="line-height:1.55;"><?= $this->t('Die CVE-Daten stammen von der offiziellen <a href="https://nvd.nist.gov/" target="_blank" rel="noopener">National Vulnerability Database (NVD)</a>. Ohne API-Key erlaubt die NVD nur sehr wenige Anfragen (ca. 5 / 30 s) – die Aktualisierung ist dann langsam und kann am Rate-Limit scheitern. Ein <strong>kostenloser</strong> API-Key erhöht das Limit deutlich (ca. 50 / 30 s).') ?></p>
<p style="line-height:1.55;"><?= $this->te('Key anfordern:') ?>
<a href="https://nvd.nist.gov/developers/request-an-api-key" target="_blank" rel="noopener"><?= $this->te('nvd.nist.gov/developers/request-an-api-key') ?></a>.</p>
<form method="post" action="/admin/cve/settings" style="margin-top:1rem;">
<input type="hidden" name="_csrf" value="<?= $this->e($this->csrf()) ?>">
<div class="form-group">
<label for="nvd-key"><?= $this->te('NVD-API-Key') ?></label>
<input type="password" id="nvd-key" name="nvd_api_key" class="form-control" autocomplete="off"
placeholder="<?= $nvdKeySet ? $this->te('•••••••• (gesetzt)') : $this->te('z. B. 12345678-90ab-cdef-1234-567890abcdef') ?>">
<small class="form-hint">
<?= $this->te('Der Schlüssel wird verschlüsselt in der Datenbank gespeichert – keine Konfigurationsdatei nötig.') ?>
<?= $nvdKeySet ? $this->te('Leer lassen, um den hinterlegten Schlüssel beizubehalten.') : $this->te('Ohne Schlüssel gelten die engeren Abrufgrenzen der NVD.') ?>
</small>
<?php if ($nvdKeySet): ?>
<label class="form-hint" style="display:block;margin-top:.4rem;">
<input type="checkbox" name="remove_nvd_api_key" value="1">
<?= $this->te('Hinterlegten Schlüssel entfernen und ohne arbeiten') ?>
</label>
<?php endif; ?>
</div>
<hr style="margin:1.5rem 0;border:none;border-top:1px solid var(--gray-200);">
<h3 style="font-size:1rem;font-weight:700;margin:0 0 .35rem;"><?= $this->te('🇩🇪 BSI CERT-Bund Warnungen') ?></h3>
<p class="form-hint" style="margin-top:0;line-height:1.55;">
<?= $this->t('Zusätzlich zu NVD können die deutschsprachigen Sicherheitswarnungen des <a href="https://wid.cert-bund.de/" target="_blank" rel="noopener">BSI CERT-Bund (WID)</a> eingelesen und – über die referenzierten CVE-IDs mit deiner Watchlist verknüpft – direkt auf der CVE-Seite angezeigt werden. Watchlist-relevante Warnungen werden hervorgehoben; jede CVE mit deutscher Warnung erhält ein 🇩🇪-Badge.') ?>
</p>
<p class="form-hint" style="margin-top:.4rem;line-height:1.55;">
<?= $this->t('Bevorzugt wird die <strong>strukturierte JSON-Quelle</strong> abgerufen (liefert die referenzierten CVE-IDs und den Schweregrad mit). Ist sie nicht erreichbar, wird automatisch auf den <strong>RSS-Feed</strong> zurückgefallen. Der Server benötigt ausgehenden Zugriff auf <code>wid.cert-bund.de</code>.') ?>
</p>
<div class="form-group">
<label class="settings-toggle">
<input type="checkbox" name="bsi_enabled" value="1" <?= !empty($bsi_enabled) ? 'checked' : '' ?>>
<span class="toggle-text">
<strong><?= $this->te('BSI CERT-Bund Warnungen aktivieren') ?></strong>
<small><?= $this->te('Liest den CERT-Bund-Feed ein und zeigt die Warnungen verknüpft auf der CVE-Seite an.') ?></small>
</span>
</label>
</div>
<div class="form-group">
<label for="bsi-api"><?= $this->te('Strukturierte Quelle – CSAF (bevorzugt)') ?></label>
<input type="url" id="bsi-api" name="bsi_api_url" class="form-control" autocomplete="off"
value="<?= $this->e($bsi_api_url ?? '') ?>" placeholder="<?= $this->e($bsi_default_api ?? '') ?>">
<small class="form-hint"><?= $this->t('CSAF-Provider-Metadata des CERT-Bund (liefert über den ROLIE-Feed die einzelnen Advisory-Dokumente inkl. CVE-Bezügen). Leer lassen für den Standard: <code>{standard}</code>.', ['standard' => $this->e($bsi_default_api ?? '')]) ?></small>
</div>
<div class="form-group">
<label for="bsi-feed"><?= $this->te('RSS-/Atom-Feed (Fallback)') ?></label>
<input type="url" id="bsi-feed" name="bsi_feed_url" class="form-control" autocomplete="off"
value="<?= $this->e($bsi_feed_url ?? '') ?>" placeholder="<?= $this->e($bsi_default_feed ?? '') ?>">
<small class="form-hint"><?= $this->t('Leer lassen für den Standard-Feed: <code>{standard}</code>. Die Aktualisierung läuft zusammen mit dem CVE-Refresh.', ['standard' => $this->e($bsi_default_feed ?? '')]) ?></small>
</div>
<div class="form-group" style="margin-top:.5rem;">
<button type="button" class="btn btn-default btn-sm" id="bsiTestBtn" onclick="bsiTestSource()">
<?= $this->te('🔍 Quellen testen') ?>
</button>
<small class="form-hint"><?= $this->te('Prüft beide BSI-Quellen live: HTTP-Status, gefundene Einträge und referenzierte CVEs. Zuerst speichern, falls du die URL geändert hast.') ?></small>
<pre id="bsiTestOut" style="display:none;margin-top:.6rem;padding:.75rem;background:var(--gray-900);color:var(--gray-200);border-radius:.4rem;font-size:.74rem;line-height:1.5;max-height:340px;overflow:auto;white-space:pre-wrap;word-break:break-word;"></pre>
</div>
<button type="submit" class="btn btn-primary"><?= $this->te('Speichern') ?></button>
<a href="/cve" class="btn btn-default"><?= $this->te('Zur CVE-Übersicht') ?></a>
</form>
<script>
function bsiTestSource() {
var btn = document.getElementById('bsiTestBtn');
var out = document.getElementById('bsiTestOut');
btn.disabled = true; var orig = btn.innerHTML; btn.textContent = 'Teste…';
out.style.display = 'block'; out.textContent = 'Rufe BSI-Quellen ab…';
fetch('/admin/cve/bsi-test', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content,
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(r => r.json())
.then(d => {
var s = d.structured || {}, r = d.rss || {};
var lines = [];
lines.push('=== Strukturierte JSON-Quelle ===');
lines.push('URL: ' + (d.api_url || ''));
lines.push('HTTP: ' + s.http_code + (s.curl_error ? ' cURL-Fehler: ' + s.curl_error : ''));
lines.push('Body-Länge: ' + s.body_length);
lines.push('Entdeckte Dok.: ' + (s.discovered_docs || 0));
lines.push('Beispiel geparst: ' + s.parsed_count + ' Dok. CVEs (Beispiel): ' + s.cve_total);
if (s.parse_error) lines.push('Parse-Fehler: ' + s.parse_error);
lines.push('Hinweis: Der Test lädt nur wenige Beispiel-Dokumente; der Refresh holt alle.');
lines.push('Body-Auszug:');
lines.push((s.body_excerpt || '').slice(0, 1200));
lines.push('');
lines.push('=== RSS-Fallback ===');
lines.push('URL: ' + (d.feed_url || ''));
lines.push('HTTP: ' + r.http_code + (r.curl_error ? ' cURL-Fehler: ' + r.curl_error : ''));
lines.push('Geparste Items:' + r.parsed_count + ' CVEs gesamt: ' + r.cve_total);
if (s.sample) { lines.push(''); lines.push('Beispiel-Item (strukturiert):'); lines.push(JSON.stringify(s.sample, null, 2)); }
out.textContent = lines.join('\n');
})
.catch(() => { out.textContent = 'Test fehlgeschlagen (Netzwerk-/Serverfehler).'; })
.finally(() => { btn.disabled = false; btn.innerHTML = orig; });
}
</script>
</div>
</section>
</div>