Report sections expandable, file-prevention alert rendering, save mkdir

This commit is contained in:
BlackSnufkin
2026-05-04 04:55:01 -07:00
parent ac46c3d3b7
commit b156b6a4a1
4 changed files with 243 additions and 28 deletions
+28
View File
@@ -365,6 +365,32 @@ def _build_details(src: dict) -> dict:
memory_region = proc_ext.get("memory_region") or {}
token = proc_ext.get("token") or {}
# File identity — populated on Defend malware-prevention alerts
# (event.action="creation", event.code="malicious_file") where the
# relevant subject of the alert is the file that got prevented, NOT
# the process that wrote it. The writer process is often Whiskers
# itself in our pipeline; surfacing it as the "subject" misleads
# operators into thinking Whiskers got flagged when really it's
# the payload that did. The renderer prefers `file.name` over
# `process.name` when this block is populated.
file_obj = src.get("file") or {}
file_ext = file_obj.get("Ext") or {}
malware_sig = (file_ext.get("malware_signature") or {}).get("primary") or {}
sig_block = malware_sig.get("signature") or {}
file_info = None
if file_obj.get("name") or file_obj.get("path"):
file_info = {
"name": file_obj.get("name"),
"path": file_obj.get("path"),
"directory": file_obj.get("directory"),
"size": file_obj.get("size"),
"sha256": (file_obj.get("hash") or {}).get("sha256"),
"code_signature": _normalize_code_signature(file_obj.get("code_signature")),
"pe": file_obj.get("pe") or None,
"signature_name": sig_block.get("name"),
"signature_id": sig_block.get("id"),
}
# Process identity (subset that's useful to the operator).
process_info = {
"name": proc.get("name"),
@@ -510,9 +536,11 @@ def _build_details(src: dict) -> dict:
"risk_score": _flat(src, "kibana.alert.risk_score", "event.risk_score"),
"event_action": _flat(src, "event.action"),
"event_category": _flat(src, "event.category"),
"event_code": _flat(src, "event.code"),
"event_outcome": _flat(src, "event.outcome"),
"process": process_info,
"parent": parent_info,
"file": file_info,
"api": api_info,
"memory_region": memory_info,
"call_stack": call_stack,
+6
View File
@@ -133,6 +133,12 @@ class RouteHelpers:
def save_analysis_results(self, results, result_path, results_filename):
results_file_path = os.path.join(result_path, results_filename)
# The result directory may have been deleted between the analyzer
# launch and this save (operator-triggered cleanup mid-run, or a
# `Run All` race). Re-create the directory rather than crashing —
# the operator can delete again if they want it gone. Alternative
# was an unhandled FileNotFoundError that took down the request.
os.makedirs(os.path.dirname(results_file_path), exist_ok=True)
with open(results_file_path, 'w') as f:
json.dump(results, f)
self.logger.debug(f"Analysis results saved to: {results_file_path}")
+30 -1
View File
@@ -290,14 +290,43 @@ function alertKey(a) {
return a.rule_uuid || a.rule_id || `${a.detected_at}:${a.title}`;
}
// Defend malware-prevention alerts (event.action="creation",
// event.code="malicious_file") are *about the file* — the writer
// process recorded as `process.name` is incidental (often Whiskers
// in our pipeline, since Whiskers writes the payload to disk).
// Render the file as the row subject for these so the operator sees
// "this alert is about <payload>", not "this alert is about Whiskers".
function isFilePreventionAlert(d) {
if (!d) return false;
if (d.event_code === 'malicious_file') return true;
const cats = d.event_category;
const catList = Array.isArray(cats) ? cats : (cats ? [cats] : []);
if (d.event_action === 'creation' && catList.some(c => /malware/i.test(c))) {
return true;
}
return false;
}
function renderAlertRow(a, idx) {
const d = a.details || {};
const sevClass = severityTagClass(a.severity);
const sevText = String(a.severity || 'unknown').toUpperCase();
const proc = d.process || {};
const procCell = proc.name
const file = d.file || {};
// Subject column: file for prevention alerts, process for behavior alerts.
let subjectCell;
if (isFilePreventionAlert(d) && file.name) {
const writer = proc.name && proc.name.toLowerCase() !== file.name.toLowerCase()
? ` <span class="lb-muted">(writer: ${escapeHtml(proc.name)})</span>`
: '';
subjectCell = `${escapeHtml(file.name)}${writer}`;
} else {
subjectCell = proc.name
? `${escapeHtml(proc.name)}${proc.pid != null ? ` <span class="lb-muted">(${proc.pid})</span>` : ''}`
: '—';
}
const procCell = subjectCell;
const trigger = (() => {
if (d.api && d.api.name) {
return `<span class="lb-mono" style="font-size: 12px; color: var(--lb-accent-soft);">${escapeHtml(d.api.summary || d.api.name + '()')}</span>`;
+177 -25
View File
@@ -492,6 +492,24 @@
.mt-24 { margin-top: 24px; }
.stack { display: flex; flex-direction: column; gap: 4px; }
/* Expandable "show all" sections — used wherever a section truncates
at N items so reviewers can still see everything when needed. Pure
<details>/<summary>; no JS. Hidden in print so the printed report
stays compact. */
details.report-more { margin-top: 8px; }
details.report-more > summary {
cursor: pointer; user-select: none; list-style: none;
color: var(--text-muted); font-size: 12px;
padding: 4px 8px; border: 1px dashed var(--border);
border-radius: 4px; display: inline-block;
}
details.report-more > summary::-webkit-details-marker { display: none; }
details.report-more > summary::before { content: '▸ '; color: var(--text-faint); }
details.report-more[open] > summary::before { content: '▾ '; }
details.report-more[open] > summary { margin-bottom: 8px; }
details.report-more table.report-table { margin-top: 0; }
@media print { details.report-more { display: none; } }
/* Print */
@media print {
body { background: white; color: #111; }
@@ -789,11 +807,26 @@
<td class="muted">{{ imp.note|truncate(120, true) }}</td>
</tr>
{% endfor %}
{% if pe.suspicious_imports|length > 30 %}
<tr><td colspan="4" class="center">… and {{ pe.suspicious_imports|length - 30 }} more</td></tr>
{% endif %}
</tbody>
</table>
{% if pe.suspicious_imports|length > 30 %}
<details class="report-more">
<summary>… and {{ pe.suspicious_imports|length - 30 }} more (click to expand)</summary>
<table class="report-table">
<thead><tr><th>DLL</th><th>Function</th><th>Category</th><th>Note</th></tr></thead>
<tbody>
{% for imp in pe.suspicious_imports[30:] %}
<tr>
<td class="mono">{{ imp.dll }}</td>
<td class="mono">{{ imp.function }}</td>
<td>{{ imp.category }}</td>
<td class="muted">{{ imp.note|truncate(120, true) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</details>
{% endif %}
{% endif %}
{% endif %}
</section>
@@ -854,10 +887,25 @@
{% endif %}
</li>
{% endfor %}
{% if yara.matches|length > 10 %}
<li class="muted">… and {{ yara.matches|length - 10 }} more</li>
{% endif %}
</ul>
{% if yara.matches|length > 10 %}
<details class="report-more">
<summary>… and {{ yara.matches|length - 10 }} more (click to expand)</summary>
<ul class="finding-list">
{% for m in yara.matches[10:] %}
<li>
<span class="finding-rule">{{ m.rule }}</span>
{% if m.metadata and m.metadata.severity is defined %}
<span class="finding-meta">severity {{ m.metadata.severity }}</span>
{% endif %}
{% if m.strings %}
<span class="finding-meta">{{ m.strings|length }} string match{{ 'es' if m.strings|length != 1 else '' }}</span>
{% endif %}
</li>
{% endfor %}
</ul>
</details>
{% endif %}
{% endif %}
</div>
</div>
@@ -946,7 +994,14 @@
{% if items %}
<h4 class="subsection-title mt-12">{{ label }} ({{ items|length }})</h4>
<pre class="code">{% for item in items[:100] %}{{ item }}
{% endfor %}{% if items|length > 100 %}… and {{ items|length - 100 }} more{% endif %}</pre>
{% endfor %}</pre>
{% if items|length > 100 %}
<details class="report-more">
<summary>… and {{ items|length - 100 }} more (click to expand)</summary>
<pre class="code">{% for item in items[100:] %}{{ item }}
{% endfor %}</pre>
</details>
{% endif %}
{% endif %}
{% endfor %}
{% else %}
@@ -1111,11 +1166,28 @@
<td class="muted">{{ (f.details or '')|truncate(120, true) }}</td>
</tr>
{% endfor %}
{% if pa_findings|length > 15 %}
<tr><td colspan="3" class="center">… and {{ pa_findings|length - 15 }} more</td></tr>
{% endif %}
</tbody>
</table>
{% if pa_findings|length > 15 %}
<details class="report-more">
<summary>… and {{ pa_findings|length - 15 }} more (click to expand)</summary>
<table class="report-table">
<thead><tr><th>Type</th><th>Level</th><th>Details</th></tr></thead>
<tbody>
{% for f in pa_findings[15:] %}
<tr>
<td>{{ f.type or '—' }}</td>
<td>
{% set lvl = (f.level or '')|lower %}
<span class="pill {% if lvl in ['critical','high','suspect'] %}critical{% elif lvl == 'medium' %}medium{% else %}muted{% endif %}">{{ f.level or '—' }}</span>
</td>
<td class="muted">{{ (f.details or '')|truncate(120, true) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</details>
{% endif %}
{% endif %}
</div>
</div>
@@ -1201,11 +1273,29 @@
<td>{% if d.verdict %}<span class="pill {% if is_threat %}critical{% else %}medium{% endif %}">{{ d.verdict }}</span>{% else %}<span class="muted"></span>{% endif %}</td>
</tr>
{% endfor %}
{% if re_defender|length > 15 %}
<tr><td colspan="4" class="center">… and {{ re_defender|length - 15 }} more</td></tr>
{% endif %}
</tbody>
</table>
{% if re_defender|length > 15 %}
<details class="report-more">
<summary>… and {{ re_defender|length - 15 }} more (click to expand)</summary>
<table class="report-table">
<thead><tr><th>Provider</th><th>Event</th><th>Scan Target</th><th>Verdict</th></tr></thead>
<tbody>
{% for d in re_defender[15:] %}
{% set evt = (d.event or '')|lower %}
{% set is_threat = (d.verdict and (d.verdict|string)|trim) %}
{% if not is_threat %}{% for s in threat_signals %}{% if s in evt %}{% set is_threat = true %}{% endif %}{% endfor %}{% endif %}
<tr>
<td class="mono">{{ d.provider or '—' }}</td>
<td class="mono">{{ d.event or '—' }}</td>
<td class="mono muted">{{ (d.scan_target or '—')|truncate(80, true) }}</td>
<td>{% if d.verdict %}<span class="pill {% if is_threat %}critical{% else %}medium{% endif %}">{{ d.verdict }}</span>{% else %}<span class="muted"></span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</details>
{% endif %}
{% endif %}
{% if re_proc and (re_proc.pid or re_children) %}
@@ -1229,11 +1319,27 @@
<td class="mono muted">{{ n.size if n.size is not none else '—' }}</td>
</tr>
{% endfor %}
{% if re_net|length > 25 %}
<tr><td colspan="5" class="center">… and {{ re_net|length - 25 }} more</td></tr>
{% endif %}
</tbody>
</table>
{% if re_net|length > 25 %}
<details class="report-more">
<summary>… and {{ re_net|length - 25 }} more (click to expand)</summary>
<table class="report-table">
<thead><tr><th>Proto</th><th>Local</th><th>Remote</th><th>Op</th><th>Size</th></tr></thead>
<tbody>
{% for n in re_net[25:] %}
<tr>
<td class="mono">{{ n.proto or '?' }}</td>
<td class="mono muted">{{ n.local_addr or '—' }}{% if n.local_port %}:{{ n.local_port }}{% endif %}</td>
<td class="mono">{{ n.remote_addr or '—' }}{% if n.remote_port %}:{{ n.remote_port }}{% endif %}</td>
<td class="mono muted">{{ n.operation or '—' }}</td>
<td class="mono muted">{{ n.size if n.size is not none else '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</details>
{% endif %}
{% endif %}
{% if re_files %}
@@ -1248,11 +1354,25 @@
<td class="mono muted">{{ f.thread_id if f.thread_id is not none else '—' }}</td>
</tr>
{% endfor %}
{% if re_files|length > 25 %}
<tr><td colspan="3" class="center">… and {{ re_files|length - 25 }} more</td></tr>
{% endif %}
</tbody>
</table>
{% if re_files|length > 25 %}
<details class="report-more">
<summary>… and {{ re_files|length - 25 }} more (click to expand)</summary>
<table class="report-table">
<thead><tr><th>Path</th><th>Operation</th><th>Thread</th></tr></thead>
<tbody>
{% for f in re_files[25:] %}
<tr>
<td class="mono" style="word-break: break-all;">{{ (f.path or '—')|truncate(120, true) }}</td>
<td class="mono muted">{{ f.operation or '—' }}</td>
<td class="mono muted">{{ f.thread_id if f.thread_id is not none else '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</details>
{% endif %}
{% endif %}
{% if re_audit %}
@@ -1268,11 +1388,26 @@
<td class="mono muted">{{ a.caller_pid if a.caller_pid is not none else '—' }} / {{ a.caller_tid if a.caller_tid is not none else '—' }}</td>
</tr>
{% endfor %}
{% if re_audit|length > 25 %}
<tr><td colspan="4" class="center">… and {{ re_audit|length - 25 }} more</td></tr>
{% endif %}
</tbody>
</table>
{% if re_audit|length > 25 %}
<details class="report-more">
<summary>… and {{ re_audit|length - 25 }} more (click to expand)</summary>
<table class="report-table">
<thead><tr><th>API</th><th>Target PID</th><th>Target TID</th><th>Caller PID/TID</th></tr></thead>
<tbody>
{% for a in re_audit[25:] %}
<tr>
<td class="mono">{{ a.api or '—' }}</td>
<td class="mono">{{ a.target_pid if a.target_pid is not none else '—' }}</td>
<td class="mono muted">{{ a.target_tid if a.target_tid is not none else '—' }}</td>
<td class="mono muted">{{ a.caller_pid if a.caller_pid is not none else '—' }} / {{ a.caller_tid if a.caller_tid is not none else '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</details>
{% endif %}
{% endif %}
</div>
</div>
@@ -1376,11 +1511,28 @@
<td class="mono muted">{{ a.detected_at or '—' }}</td>
</tr>
{% endfor %}
{% if edr_alerts|length > 25 %}
<tr><td colspan="4" class="center">… and {{ edr_alerts|length - 25 }} more</td></tr>
{% endif %}
</tbody>
</table>
{% if edr_alerts|length > 25 %}
<details class="report-more">
<summary>… and {{ edr_alerts|length - 25 }} more (click to expand)</summary>
<table class="report-table">
<thead><tr><th>Severity</th><th>Rule</th><th>Rule ID</th><th>Detected</th></tr></thead>
<tbody>
{% for a in edr_alerts[25:] %}
{% set sev = (a.severity or 'unknown')|lower %}
{% set sev_class = 'critical' if sev in ('high', 'critical') else ('medium' if sev == 'medium' else 'muted') %}
<tr>
<td><span class="pill {{ sev_class }}">{{ sev|upper }}</span></td>
<td>{{ a.title or 'Unknown alert' }}</td>
<td class="mono muted">{{ a.rule_id or '—' }}</td>
<td class="mono muted">{{ a.detected_at or '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</details>
{% endif %}
{% endif %}
{% if edr_exec.stdout or edr_exec.stderr or edr_exec.message %}