feat: add forensic sample analysis

This commit is contained in:
Christian Krakau-Louis
2026-05-23 13:46:32 +02:00
parent 5445508a71
commit e781b32a01
7 changed files with 523 additions and 24 deletions
@@ -61,6 +61,42 @@
{% endcall %}
</div>
{% call card() %}
{% call card_header() %}
<div class="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
<div>
{% call card_title() %}Failure Sample Analysis{% endcall %}
{% call card_description() %}Privacy-preserving diagnosis from the redacted failure sample{% endcall %}
</div>
<span class="badge uppercase" :class="priorityClass(report.analysis?.priority)" x-text="report.analysis?.priority || 'unknown'"></span>
</div>
{% endcall %}
{% call card_content() %}
<div class="space-y-4">
<p class="font-medium" x-text="report.analysis?.diagnosis || 'No analysis is available for this sample.'"></p>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<p class="text-sm font-semibold mb-2">Recommended Actions</p>
<ul class="text-sm space-y-1 list-disc pl-4">
<template x-for="action in report.analysis?.recommendations || []" :key="action">
<li x-text="action"></li>
</template>
</ul>
</div>
<div>
<p class="text-sm font-semibold mb-2">Signals</p>
<ul class="text-sm space-y-1 list-disc pl-4">
<template x-for="signal in report.analysis?.signals || []" :key="signal">
<li x-text="signal"></li>
</template>
</ul>
</div>
</div>
<p class="text-xs text-base-content/60" x-text="report.analysis?.privacy_note"></p>
</div>
{% endcall %}
{% endcall %}
{% call card() %}
{% call card_header() %}
{% call card_title() %}Message Identity{% endcall %}
@@ -160,6 +196,11 @@ function forensicReportDetailApp(reportId) {
labelize(value) {
return String(value || '').replaceAll('_', ' ').replace(/\b\w/g, (char) => char.toUpperCase());
},
priorityClass(priority) {
if (priority === 'high') return 'badge-error';
if (priority === 'medium') return 'badge-warning';
return 'badge-ghost';
},
};
}
</script>
@@ -101,6 +101,49 @@
{% endcall %}
</div>
{% call card() %}
{% call card_header() %}
<div class="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
<div>
{% call card_title() %}Sample Analysis{% endcall %}
{% call card_description() %}Grouped investigation hints from redacted forensic metadata{% endcall %}
</div>
<div class="flex gap-2">
<span class="badge badge-error badge-outline">High <span x-text="analysis.priority_counts?.high || 0"></span></span>
<span class="badge badge-warning badge-outline">Medium <span x-text="analysis.priority_counts?.medium || 0"></span></span>
</div>
</div>
{% endcall %}
{% call card_content() %}
<template x-if="analysis.groups.length === 0">
<p class="text-base-content/60">No failure samples are available for analysis.</p>
</template>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4" x-show="analysis.groups.length > 0">
<template x-for="group in analysis.groups.slice(0, 3)" :key="group.key">
<div class="border border-base-300 rounded-md p-4 space-y-3">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<p class="font-semibold break-words" x-text="group.domain"></p>
<p class="text-sm font-mono text-base-content/70" x-text="group.source_ip"></p>
</div>
<span class="badge uppercase" :class="priorityClass(group.priority)" x-text="group.priority"></span>
</div>
<p class="text-sm" x-text="group.diagnosis"></p>
<ul class="text-sm space-y-1 list-disc pl-4">
<template x-for="action in group.recommendations.slice(0, 2)" :key="action">
<li x-text="action"></li>
</template>
</ul>
<div class="flex items-center justify-between text-xs text-base-content/60">
<span><span x-text="group.count"></span> samples</span>
<span class="uppercase" x-text="group.auth_failure"></span>
</div>
</div>
</template>
</div>
{% endcall %}
{% endcall %}
{% call card() %}
{% call card_header() %}
<div class="flex items-center justify-between gap-4">
@@ -173,6 +216,7 @@ function forensicReportsApp() {
uploadError: false,
selectedFile: null,
reports: [],
analysis: { groups: [], priority_counts: {}, failure_counts: {}, result_counts: {}, samples: [] },
domainOptions: [],
total: 0,
filters: {
@@ -220,6 +264,7 @@ function forensicReportsApp() {
const data = await response.json();
this.reports = data.reports || [];
this.total = data.total || 0;
await this.fetchAnalysis(params);
if (!this.filters.domain) {
this.domainOptions = [...new Set(this.reports.map((report) => report.domain || report.reported_domain).filter(Boolean))].sort();
}
@@ -229,6 +274,11 @@ function forensicReportsApp() {
this.loading = false;
}
},
async fetchAnalysis(params) {
const response = await fetch(`/api/v1/forensics/analysis?${params.toString()}`);
if (!response.ok) throw new Error('Unable to analyze forensic reports');
this.analysis = await response.json();
},
async uploadReport() {
if (!this.selectedFile) return;
this.uploading = true;
@@ -262,6 +312,11 @@ function forensicReportsApp() {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
},
priorityClass(priority) {
if (priority === 'high') return 'badge-error';
if (priority === 'medium') return 'badge-warning';
return 'badge-ghost';
},
};
}
</script>