darkbert-ner-fixed / index.html
Gm82's picture
Fix renderResults entity type extraction bug
ee654bc verified
Raw History Blame Contribute Delete
22.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VERISBERTA · DarkBERT NER</title>
<style>
:root {
--bg: #080c14;
--surface: #0d1420;
--surface2: #111b2e;
--border: #1e2d4a;
--accent: #00d4ff;
--accent2: #ff3a6e;
--accent3: #a259ff;
--text: #c8d8f0;
--text-dim: #4a6080;
--actor: #ff3a6e;
--actor-bg: rgba(255,58,110,0.12);
--action: #00d4ff;
--action-bg: rgba(0,212,255,0.10);
--assets: #a259ff;
--assets-bg: rgba(162,89,255,0.12);
--o: #4a6080;
--glow: 0 0 20px rgba(0,212,255,0.15);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Segoe UI', system-ui, sans-serif;
min-height: 100vh;
overflow-x: hidden;
}
/* ── GRID BACKGROUND ── */
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(0,212,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,212,255,0.03) 1px, transparent 1px);
background-size: 48px 48px;
pointer-events: none;
z-index: 0;
}
/* ── FLOATING NODES ── */
.node {
position: fixed;
border-radius: 50%;
filter: blur(60px);
pointer-events: none;
z-index: 0;
animation: drift 18s ease-in-out infinite;
}
.node-1 { width:300px; height:300px; background:rgba(0,212,255,0.07); top:-60px; left:10%; }
.node-2 { width:200px; height:200px; background:rgba(255,58,110,0.07); bottom:10%; right:5%; animation-delay:-7s; }
.node-3 { width:250px; height:250px; background:rgba(162,89,255,0.06); top:40%; left:70%; animation-delay:-13s; }
@keyframes drift {
0%,100% { transform: translateY(0) translateX(0); }
33% { transform: translateY(-30px) translateX(20px); }
66% { transform: translateY(20px) translateX(-15px); }
}
/* ── LAYOUT ── */
.wrapper {
position: relative;
z-index: 1;
max-width: 980px;
margin: 0 auto;
padding: 40px 24px 80px;
}
/* ── HEADER ── */
header {
text-align: center;
margin-bottom: 48px;
}
.badge-row {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 18px;
flex-wrap: wrap;
}
.badge {
font-size: 11px;
font-weight: 600;
letter-spacing: .08em;
text-transform: uppercase;
padding: 3px 10px;
border-radius: 20px;
border: 1px solid currentColor;
}
.badge-dark { color: #888; border-color: #333; }
.badge-cyber { color: var(--accent); border-color: rgba(0,212,255,0.3); }
.badge-ner { color: var(--accent2); border-color: rgba(255,58,110,0.3); }
h1 {
font-size: clamp(28px, 5vw, 46px);
font-weight: 800;
letter-spacing: -.02em;
line-height: 1.1;
background: linear-gradient(135deg, #fff 30%, var(--accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.sub {
margin-top: 12px;
color: var(--text-dim);
font-size: 14px;
max-width: 500px;
margin-left: auto;
margin-right: auto;
line-height: 1.6;
}
.model-link {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 14px;
font-size: 12px;
color: var(--accent);
text-decoration: none;
border: 1px solid rgba(0,212,255,0.2);
border-radius: 20px;
padding: 4px 12px;
transition: all .2s;
}
.model-link:hover { background: rgba(0,212,255,0.1); border-color: var(--accent); }
.fixed-badge {
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 8px;
font-size: 12px;
color: #22c55e;
text-decoration: none;
border: 1px solid rgba(34,197,94,0.3);
border-radius: 20px;
padding: 4px 12px;
}
/* ── PLATFORM CARD ── */
.platform {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 28px;
margin-bottom: 24px;
box-shadow: var(--glow), 0 2px 40px rgba(0,0,0,0.4);
position: relative;
overflow: hidden;
}
.platform::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
opacity: .4;
}
.platform-label {
font-size: 10px;
font-weight: 700;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--text-dim);
margin-bottom: 14px;
}
/* ── TOKEN INPUT ── */
.token-row {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.token-row label { font-size: 13px; color: var(--text-dim); white-space: nowrap; }
#hf-token {
flex: 1;
min-width: 200px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 12px;
color: var(--text);
font-size: 13px;
font-family: monospace;
outline: none;
transition: border-color .2s;
}
#hf-token:focus { border-color: var(--accent); }
.token-hint { font-size: 11px; color: var(--text-dim); margin-top: 6px; }
.token-hint a { color: var(--accent); text-decoration: none; }
/* ── TEXTAREA + CONTROLS ── */
.input-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
flex-wrap: wrap;
gap: 8px;
}
textarea {
width: 100%;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 16px;
color: var(--text);
font-size: 14px;
line-height: 1.6;
resize: vertical;
min-height: 120px;
outline: none;
font-family: inherit;
transition: border-color .2s;
}
textarea:focus { border-color: var(--accent); }
.samples-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 10px;
}
.sample-btn {
font-size: 11px;
padding: 4px 10px;
border-radius: 20px;
border: 1px solid var(--border);
background: transparent;
color: var(--text-dim);
cursor: pointer;
transition: all .2s;
}
.sample-btn:hover { border-color: var(--accent); color: var(--accent); }
.run-btn {
display: flex;
align-items: center;
gap: 8px;
background: linear-gradient(135deg, #00a8cc, #0070ff);
color: #fff;
font-weight: 700;
font-size: 14px;
border: none;
border-radius: 10px;
padding: 10px 24px;
cursor: pointer;
transition: opacity .2s, transform .15s;
margin-top: 14px;
}
.run-btn:hover { opacity: .9; transform: translateY(-1px); }
.run-btn:active { transform: translateY(0); }
.run-btn:disabled { opacity: .4; cursor: not-allowed; transform: none; }
.spinner {
width: 14px; height: 14px;
border: 2px solid rgba(255,255,255,.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin .7s linear infinite;
display: none;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── OUTPUT ── */
#output-section { display: none; }
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px;
text-align: center;
}
.stat-label { font-size: 10px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; margin-bottom: 4px; }
.stat-value { font-size: 28px; font-weight: 800; }
.stat-actor .stat-label { color: var(--actor); } .stat-actor .stat-value { color: var(--actor); }
.stat-action .stat-label { color: var(--action); } .stat-action .stat-value { color: var(--action); }
.stat-assets .stat-label { color: var(--assets); } .stat-assets .stat-value { color: var(--assets); }
.stat-total .stat-label { color: var(--text-dim); } .stat-total .stat-value { color: var(--text); }
#rendered-text {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 18px 20px;
font-size: 15px;
line-height: 2;
word-break: break-word;
}
/* ── ENTITY HIGHLIGHT CHIPS ── */
.ent {
display: inline;
padding: 2px 0;
border-radius: 4px;
position: relative;
cursor: default;
}
.ent-label {
display: inline-block;
font-size: 10px;
font-weight: 800;
letter-spacing: .08em;
border-radius: 3px;
padding: 0px 5px;
margin-left: 3px;
vertical-align: middle;
position: relative;
top: -1px;
}
.ent-ACTOR { background: var(--actor-bg); color: var(--actor); border-bottom: 2px solid var(--actor); }
.ent-ACTION { background: var(--action-bg); color: var(--action); border-bottom: 2px solid var(--action); }
.ent-ASSETS { background: var(--assets-bg); color: var(--assets); border-bottom: 2px solid var(--assets); }
.ent-label-ACTOR { background: var(--actor); color: #fff; }
.ent-label-ACTION { background: var(--action); color: #000; }
.ent-label-ASSETS { background: var(--assets); color: #fff; }
/* tooltip */
.ent:hover::after {
content: attr(data-score);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #1a2840;
border: 1px solid var(--border);
border-radius: 6px;
padding: 3px 8px;
font-size: 11px;
color: var(--text);
white-space: nowrap;
pointer-events: none;
z-index: 10;
}
/* ── LEGEND ── */
.legend {
display: flex;
gap: 18px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 7px;
font-size: 13px;
}
.legend-dot {
width: 10px; height: 10px;
border-radius: 50%;
}
/* ── JSON ZONE ── */
#json-toggle {
background: transparent;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-dim);
font-size: 12px;
padding: 6px 14px;
cursor: pointer;
transition: all .2s;
margin-top: 14px;
}
#json-toggle:hover { border-color: var(--accent3); color: var(--accent3); }
#json-pane {
display: none;
margin-top: 12px;
background: #060b12;
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
font-family: monospace;
font-size: 12px;
color: #7ec8e3;
max-height: 280px;
overflow-y: auto;
line-height: 1.5;
white-space: pre-wrap;
}
/* ── ERROR ── */
#error-box {
display: none;
background: rgba(255,58,110,0.1);
border: 1px solid rgba(255,58,110,0.3);
border-radius: 10px;
padding: 14px 18px;
color: var(--actor);
font-size: 13px;
margin-top: 14px;
}
/* ── FOOTER ── */
footer {
text-align: center;
margin-top: 60px;
font-size: 12px;
color: var(--text-dim);
}
footer a { color: var(--accent); text-decoration: none; }
/* ── RESPONSIVE ── */
@media (max-width: 600px) {
.wrapper { padding: 24px 16px 60px; }
.platform { padding: 20px 16px; }
}
</style>
</head>
<body>
<div class="node node-1"></div>
<div class="node node-2"></div>
<div class="node node-3"></div>
<div class="wrapper">
<!-- HEADER -->
<header>
<div class="badge-row">
<span class="badge badge-dark">DarkBERT</span>
<span class="badge badge-cyber">VERIS Vocabulary</span>
<span class="badge badge-ner">Named Entity Recognition</span>
<span class="badge fixed-badge">🔧 Fixed Endpoint</span>
</div>
<h1>VERISBERTA</h1>
<p class="sub">
Cybersecurity incident NER — identifies <strong>Actors</strong>, <strong>Actions</strong>,
and <strong>Assets</strong> in threat intelligence narratives using VERIS 4A taxonomy.
</p>
<a class="model-link" href="https://huggingface.co/processpl/DarkBERT-finetuned-ner" target="_blank">
🤗 processpl/DarkBERT-finetuned-ner
</a>
<a class="fixed-badge" href="https://huggingface.co/spaces/super9igga/darkbert-ner-demo" target="_blank">
Fork of super9igga demo
</a>
</header>
<!-- TOKEN PLATFORM -->
<div class="platform">
<div class="platform-label">🔑 Authentication</div>
<div class="token-row">
<label for="hf-token">HF Token (recommended)</label>
<input type="password" id="hf-token" placeholder="hf_..." autocomplete="off" />
</div>
<p class="token-hint">
A Hugging Face token is <strong>required</strong> for the Inference Providers API.
Create a free read token at
<a href="https://huggingface.co/settings/tokens" target="_blank">huggingface.co/settings/tokens</a>.
Your token is used only in your browser — it is never stored or sent anywhere except the HF API.
</p>
</div>
<!-- INPUT PLATFORM -->
<div class="platform">
<div class="platform-label">📝 Incident Narrative Input</div>
<div class="input-header">
<span style="font-size:13px; color:var(--text-dim)">Paste a cybersecurity incident description</span>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--actor)"></div> ACTOR</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--action)"></div> ACTION</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--assets)"></div> ASSETS</div>
</div>
</div>
<textarea id="input-text" rows="5" placeholder="e.g. A nation-state actor exploited a zero-day vulnerability in the VPN gateway to steal employee credentials from the HR database..."></textarea>
<div class="samples-row">
<span style="font-size:11px;color:var(--text-dim);align-self:center">Try:</span>
<button class="sample-btn" onclick="setSample(0)">Ransomware</button>
<button class="sample-btn" onclick="setSample(1)">Data Breach</button>
<button class="sample-btn" onclick="setSample(2)">Insider Threat</button>
<button class="sample-btn" onclick="setSample(3)">APT Attack</button>
</div>
<button class="run-btn" id="run-btn" onclick="runNER()">
<div class="spinner" id="spinner"></div>
<span id="btn-text">⚡ Analyze Entities</span>
</button>
<div id="error-box"></div>
</div>
<!-- OUTPUT PLATFORM -->
<div class="platform" id="output-section">
<div class="platform-label">🎯 Entity Detection Results</div>
<div class="stats-grid">
<div class="stat-card stat-actor">
<div class="stat-label">Actor</div>
<div class="stat-value" id="cnt-actor">0</div>
</div>
<div class="stat-card stat-action">
<div class="stat-label">Action</div>
<div class="stat-value" id="cnt-action">0</div>
</div>
<div class="stat-card stat-assets">
<div class="stat-label">Assets</div>
<div class="stat-value" id="cnt-assets">0</div>
</div>
<div class="stat-card stat-total">
<div class="stat-label">Total</div>
<div class="stat-value" id="cnt-total">0</div>
</div>
</div>
<div id="rendered-text"></div>
<button id="json-toggle" onclick="toggleJson()">{ } Raw API Response</button>
<div id="json-pane"></div>
</div>
<!-- FOOTER -->
<footer>
Built on <a href="https://huggingface.co/s2w-ai/DarkBERT" target="_blank">DarkBERT</a> ·
Fine-tuned on <a href="https://huggingface.co/datasets/guidobenb/VCDB_NER_LG2220" target="_blank">VCDB dataset</a> ·
Model by <a href="https://huggingface.co/processpl" target="_blank">processpl</a> ·
Fixed fork by <a href="https://huggingface.co/Gm82" target="_blank">Gm82</a>
</footer>
</div>
<script>
/* ── FIXED: current Inference Providers router endpoint ──
(original demo used the deprecated api-inference.huggingface.co) */
const API_URL = 'https://router.huggingface.co/hf-inference/models/processpl/DarkBERT-finetuned-ner';
const SAMPLES = [
"A ransomware group encrypted the company's file servers after gaining access through a phishing email targeting the finance department. The attacker exfiltrated sensitive payroll data before deploying the malware payload.",
"An external hacker breached the customer database by exploiting an unpatched SQL injection vulnerability in the web application. Personal information including credit card numbers and social security numbers were stolen.",
"A malicious insider with privileged access to the network infrastructure intentionally deleted critical backup systems and leaked proprietary source code to a competitor. The employee abused administrator credentials over several weeks.",
"A nation-state sponsored threat actor performed a spear-phishing campaign against defense contractors. The APT group installed a rootkit on workstations to maintain persistent access and exfiltrated classified documents from secure file shares."
];
function setSample(i) {
document.getElementById('input-text').value = SAMPLES[i];
}
let rawResult = null;
let jsonVisible = false;
function toggleJson() {
const pane = document.getElementById('json-pane');
const btn = document.getElementById('json-toggle');
jsonVisible = !jsonVisible;
pane.style.display = jsonVisible ? 'block' : 'none';
btn.textContent = jsonVisible ? '{ } Hide Raw Response' : '{ } Raw API Response';
if (jsonVisible && rawResult) {
pane.textContent = JSON.stringify(rawResult, null, 2);
}
}
async function runNER() {
const text = document.getElementById('input-text').value.trim();
const token = document.getElementById('hf-token').value.trim();
const errBox = document.getElementById('error-box');
if (!text) { showError('Please enter some text first.'); return; }
if (!token) { showError('An HF token is required. Create one at huggingface.co/settings/tokens and paste it in the Authentication box.'); return; }
errBox.style.display = 'none';
setLoading(true);
const headers = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = `Bearer ${token}`;
try {
const resp = await fetch(API_URL, {
method: 'POST',
headers,
body: JSON.stringify({ inputs: text })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
const msg = err.error || `HTTP ${resp.status}: ${resp.statusText}`;
throw new Error(msg);
}
const data = await resp.json();
rawResult = data;
/* HF token-classification returns array of entities OR nested array */
const entities = Array.isArray(data[0]) ? data[0] : data;
renderResults(text, entities);
} catch (e) {
const m = e.message || '';
showError('API error: ' + m +
(m.includes('401') ? ' — invalid or missing token. Paste a read token above (settings/tokens).' : '') +
(m.includes('403') ? ' — token lacks Inference permission. Create a new token with "Make calls to Inference Providers" enabled.' : '') +
(m.includes('404') ? ' — model not available on the hf-inference provider; check the model page.' : '') +
(m.includes('429') ? ' — rate limited; wait a moment and retry.' : '') +
(m.includes('Failed to fetch') ? ' — network/CORS error. Make sure you are using a valid token and not blocking the request.' : '')
);
} finally {
setLoading(false);
}
}
function setLoading(on) {
const btn = document.getElementById('run-btn');
const spinner = document.getElementById('spinner');
const btnText = document.getElementById('btn-text');
btn.disabled = on;
spinner.style.display = on ? 'block' : 'none';
btnText.textContent = on ? 'Analyzing…' : '⚡ Analyze Entities';
}
function showError(msg) {
const b = document.getElementById('error-box');
b.textContent = msg;
b.style.display = 'block';
}
function renderResults(text, entities) {
const out = document.getElementById('output-section');
const el = document.getElementById('rendered-text');
const typeOf = e => (e.entity_group || e.entity || '').replace('B-','').replace('I-','');
/* Count by type */
const counts = { ACTOR: 0, ACTION: 0, ASSETS: 0 };
entities.forEach(e => {
const t = typeOf(e);
if (counts[t] !== undefined) counts[t]++;
});
document.getElementById('cnt-actor').textContent = counts.ACTOR;
document.getElementById('cnt-action').textContent = counts.ACTION;
document.getElementById('cnt-assets').textContent = counts.ASSETS;
document.getElementById('cnt-total').textContent = counts.ACTOR + counts.ACTION + counts.ASSETS;
/* Build highlighted HTML */
let html = '';
let cursor = 0;
/* Sort by start position */
const sorted = [...entities]
.filter(e => ['ACTOR','ACTION','ASSETS'].includes(typeOf(e)))
.sort((a, b) => (a.start || 0) - (b.start || 0));
sorted.forEach(ent => {
const start = ent.start || 0;
const end = ent.end || 0;
const type = typeOf(ent);
const score = ((ent.score || 0) * 100).toFixed(1) + '%';
const word = ent.word || text.slice(start, end);
if (start > cursor) {
html += escHtml(text.slice(cursor, start));
}
html += `<span class="ent ent-${type}" data-score="${type} · ${score}">${escHtml(word)}<span class="ent-label ent-label-${type}">${type}</span></span>`;
cursor = end;
});
if (cursor < text.length) html += escHtml(text.slice(cursor));
el.innerHTML = html || '<span style="color:var(--text-dim)">No entities found — try a different text.</span>';
if (jsonVisible && rawResult) {
document.getElementById('json-pane').textContent = JSON.stringify(rawResult, null, 2);
}
out.style.display = 'block';
out.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function escHtml(s) {
return s
.replace(/&/g,'&amp;')
.replace(/</g,'&lt;')
.replace(/>/g,'&gt;')
.replace(/"/g,'&quot;');
}
/* Load a sample on first visit */
setSample(0);
</script>
</body>
</html>