diff --git a/backend/src/routes/threat_detections.js b/backend/src/routes/threat_detections.js
index edb6fc1..0c496e7 100644
--- a/backend/src/routes/threat_detections.js
+++ b/backend/src/routes/threat_detections.js
@@ -5,7 +5,6 @@ const Threat_detectionsService = require('../services/threat_detections');
const Threat_detectionsDBApi = require('../db/api/threat_detections');
const wrapAsync = require('../helpers').wrapAsync;
-const config = require('../config');
const router = express.Router();
@@ -125,6 +124,21 @@ router.post('/', wrapAsync(async (req, res) => {
* description: Some server error
*
*/
+
+
+router.post('/analyze', wrapAsync(async (req, res) => {
+ const payload = await Threat_detectionsService.analyze(req.body.data, req.currentUser);
+ res.status(200).send(payload);
+}));
+
+router.get('/assistant/recent', wrapAsync(async (req, res) => {
+ const payload = await Threat_detectionsService.recentAssistantFindings(
+ req.currentUser,
+ req.query.limit,
+ );
+ res.status(200).send({ rows: payload, count: payload.length });
+}));
+
router.post('/bulk-import', wrapAsync(async (req, res) => {
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
const link = new URL(referer);
diff --git a/backend/src/services/threat_detections.js b/backend/src/services/threat_detections.js
index 421f413..9016a65 100644
--- a/backend/src/services/threat_detections.js
+++ b/backend/src/services/threat_detections.js
@@ -3,12 +3,157 @@ const Threat_detectionsDBApi = require('../db/api/threat_detections');
const processFile = require("../middlewares/upload");
const ValidationError = require('./notifications/errors/validation');
const csv = require('csv-parser');
-const axios = require('axios');
-const config = require('../config');
const stream = require('stream');
+const PHISHING_PATTERNS = [
+ { label: 'Urgent pressure language', weight: 14, pattern: /\b(urgent|immediately|final notice|act now|within 24 hours|account will be closed)\b/i },
+ { label: 'Credential or password request', weight: 18, pattern: /\b(password|credentials|verify your account|confirm your identity|login to continue|security update)\b/i },
+ { label: 'Payment or gift-card lure', weight: 15, pattern: /\b(wire transfer|gift card|bitcoin|crypto wallet|invoice attached|payment failed)\b/i },
+ { label: 'Attachment execution lure', weight: 16, pattern: /\b(enable macros|open the attachment|download invoice|run the file|security patch)\b/i },
+ { label: 'Brand impersonation wording', weight: 11, pattern: /\b(microsoft|google|paypal|docusign|dropbox|office 365|banking portal)\b/i },
+];
+
+const URL_PATTERNS = [
+ { label: 'URL contains an IP address host', weight: 20, pattern: /https?:\/\/\d{1,3}(\.\d{1,3}){3}/i },
+ { label: 'Punycode or homograph indicator', weight: 16, pattern: /xn--/i },
+ { label: 'Suspicious top-level domain', weight: 12, pattern: /\.(zip|mov|top|click|work|rest|country|gq|tk|ml)(\/|$)/i },
+ { label: 'URL hides destination with @ symbol', weight: 18, pattern: /https?:\/\/[^\s]+@/i },
+ { label: 'Known URL shortener', weight: 10, pattern: /\b(bit\.ly|tinyurl\.com|t\.co|goo\.gl|ow\.ly|is\.gd)\b/i },
+];
+
+const MALWARE_PATTERNS = [
+ { label: 'Executable or script file extension', weight: 22, pattern: /\.(exe|scr|bat|cmd|js|vbs|ps1|jar|dll|hta|iso)\b/i },
+ { label: 'Office macro-enabled file extension', weight: 14, pattern: /\.(docm|xlsm|pptm)\b/i },
+ { label: 'Suspicious process or shell behavior', weight: 20, pattern: /\b(powershell|cmd\.exe|rundll32|regsvr32|wscript|cscript|encodedcommand)\b/i },
+ { label: 'Persistence or credential-access behavior', weight: 18, pattern: /\b(run key|startup folder|credential dump|mimikatz|lsass|keylogger)\b/i },
+ { label: 'Command-and-control network indicator', weight: 17, pattern: /\b(beacon|tor exit|dns tunnel|c2|command and control|port 4444|port 1337)\b/i },
+];
+
+const SAFE_PATTERNS = [
+ { label: 'Uses HTTPS URL', weight: -4, pattern: /https:\/\//i },
+ { label: 'Mentions SPF/DKIM/DMARC pass', weight: -8, pattern: /\b(spf pass|dkim pass|dmarc pass)\b/i },
+ { label: 'Known institutional domain signal', weight: -6, pattern: /\b(\.edu|\.gov)\b/i },
+];
+
+function clamp(value, min, max) {
+ return Math.max(min, Math.min(max, value));
+}
+
+function redactSensitiveText(value) {
+ if (!value) return '';
+ return String(value)
+ .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[email]')
+ .replace(/https?:\/\/\S+/gi, '[url]')
+ .replace(/\b\d{12,19}\b/g, '[number]')
+ .slice(0, 600);
+}
+
+function addMatches(source, patterns, indicators) {
+ patterns.forEach((item) => {
+ if (item.pattern.test(source)) {
+ indicators.push({ label: item.label, weight: item.weight });
+ }
+ });
+}
+
+function assessThreat(data) {
+ const source = [
+ data.title,
+ data.content_text,
+ data.url,
+ data.fileName,
+ data.sha256,
+ data.network_activity,
+ ].filter(Boolean).join('\n');
+
+ const indicators = [];
+ addMatches(source, PHISHING_PATTERNS, indicators);
+ addMatches(source, URL_PATTERNS, indicators);
+ addMatches(source, MALWARE_PATTERNS, indicators);
+ addMatches(source, SAFE_PATTERNS, indicators);
+
+ let riskScore = 12 + indicators.reduce((sum, item) => sum + item.weight, 0);
+
+ if (data.submission_type === 'url') riskScore += 4;
+ if (data.submission_type === 'file') riskScore += 6;
+ if (data.submission_type === 'network_traffic') riskScore += 8;
+ if (source.length > 2500) riskScore += 3;
+
+ riskScore = clamp(Math.round(riskScore), 0, 100);
+
+ const positiveIndicators = indicators.filter((item) => item.weight > 0);
+ const topIndicators = positiveIndicators
+ .sort((a, b) => b.weight - a.weight)
+ .slice(0, 6);
+
+ const threatType = data.submission_type === 'file' || data.submission_type === 'network_traffic'
+ ? (riskScore >= 35 ? 'malware' : 'benign')
+ : (riskScore >= 35 ? (data.submission_type === 'url' ? 'suspicious_url' : 'phishing') : 'benign');
+
+ const severity = riskScore >= 90
+ ? 'critical'
+ : riskScore >= 72
+ ? 'high'
+ : riskScore >= 45
+ ? 'medium'
+ : riskScore >= 20
+ ? 'low'
+ : 'info';
+
+ const verdict = riskScore >= 75
+ ? 'block'
+ : riskScore >= 45
+ ? 'warn'
+ : riskScore >= 20
+ ? 'needs_review'
+ : 'allow';
+
+ const confidence = clamp(
+ Number((0.56 + topIndicators.length * 0.055 + Math.abs(riskScore - 50) / 250).toFixed(2)),
+ 0.56,
+ 0.96,
+ );
+
+ const summary = topIndicators.length
+ ? `${severity.toUpperCase()} risk ${threatType.replace('_', ' ')} signal: ${topIndicators.map((item) => item.label).join(', ')}.`
+ : 'No strong malicious indicators were found; keep monitoring and verify sender/source context.';
+
+ return {
+ threatType,
+ severity,
+ verdict,
+ riskScore,
+ confidence,
+ summary,
+ indicators: topIndicators,
+ allIndicators: indicators,
+ };
+}
+
+function validateAnalysisPayload(data) {
+ const validTypes = ['email', 'message', 'url', 'file', 'network_traffic'];
+
+ if (!data || typeof data !== 'object') {
+ throw new ValidationError('analysisPayloadMissing', 'Analysis payload is required.');
+ }
+
+ if (!validTypes.includes(data.submission_type)) {
+ throw new ValidationError('analysisTypeInvalid', 'Choose email, message, URL, file, or network traffic.');
+ }
+
+ const hasSignal = [data.content_text, data.url, data.fileName, data.sha256, data.network_activity]
+ .some((value) => String(value || '').trim().length > 0);
+
+ if (!hasSignal) {
+ throw new ValidationError('analysisSignalMissing', 'Add text, a URL, a file name/hash, or network behavior to analyze.');
+ }
+
+ if (String(data.content_text || '').length > 20000) {
+ throw new ValidationError('analysisTextTooLong', 'Analysis text must be 20,000 characters or less.');
+ }
+}
module.exports = class Threat_detectionsService {
@@ -28,9 +173,9 @@ module.exports = class Threat_detectionsService {
await transaction.rollback();
throw error;
}
- };
+ }
- static async bulkImport(req, res, sendInvitationEmails = true, host) {
+ static async bulkImport(req, res) {
const transaction = await db.sequelize.transaction();
try {
@@ -95,7 +240,7 @@ module.exports = class Threat_detectionsService {
await transaction.rollback();
throw error;
}
- };
+ }
static async deleteByIds(ids, currentUser) {
const transaction = await db.sequelize.transaction();
@@ -132,6 +277,127 @@ module.exports = class Threat_detectionsService {
}
}
+
+ static async analyze(data, currentUser) {
+ validateAnalysisPayload(data);
+
+ const transaction = await db.sequelize.transaction();
+
+ try {
+ const now = new Date();
+ const assessment = assessThreat(data);
+ const organizationId = currentUser?.organizationsId
+ || currentUser?.organizations?.id
+ || currentUser?.organization?.id
+ || null;
+ const rawSignal = [
+ data.title,
+ data.content_text,
+ data.url,
+ data.fileName,
+ data.sha256,
+ data.network_activity,
+ ].filter(Boolean).join('\n');
+ const signalHash = crypto.createHash('sha256').update(rawSignal).digest('hex');
+ const privacyMode = data.privacy_mode !== false;
+ const storedText = privacyMode ? redactSensitiveText(data.content_text || data.network_activity || '') : String(data.content_text || data.network_activity || '').slice(0, 20000);
+
+ const submission = await db.analysis_submissions.create({
+ submission_type: data.submission_type,
+ title: String(data.title || data.fileName || data.url || 'Untitled analysis').slice(0, 180),
+ content_text: storedText,
+ url: data.submission_type === 'url' ? String(data.url || '').slice(0, 2000) : null,
+ sha256: String(data.sha256 || signalHash).slice(0, 128),
+ processing_location: 'local',
+ status: 'completed',
+ submitted_at: now,
+ completed_at: now,
+ submitted_byId: currentUser?.id || null,
+ organizationsId: organizationId,
+ createdById: currentUser?.id || null,
+ updatedById: currentUser?.id || null,
+ }, { transaction });
+
+ const detection = await db.threat_detections.create({
+ threat_type: assessment.threatType,
+ severity: assessment.severity,
+ risk_score: assessment.riskScore,
+ verdict: assessment.verdict,
+ is_false_positive: false,
+ is_false_negative: false,
+ summary: assessment.summary,
+ detected_at: now,
+ submissionId: submission.id,
+ organizationsId: organizationId,
+ createdById: currentUser?.id || null,
+ updatedById: currentUser?.id || null,
+ }, { transaction });
+
+ const explanationText = assessment.indicators.length
+ ? `The analyzer flagged this item because it matched ${assessment.indicators.length} high-signal indicator(s). Review the indicators before blocking business-critical traffic.`
+ : 'The analyzer did not find strong malicious signals. Treat this as a low-risk result, not a guarantee of safety.';
+
+ const explanation = await db.explanations.create({
+ explanation_type: assessment.indicators.length ? 'rule_match' : 'feature_importance',
+ explanation_text: explanationText,
+ top_indicators: JSON.stringify(assessment.indicators),
+ confidence: assessment.confidence,
+ generated_at: now,
+ detectionId: detection.id,
+ organizationsId: organizationId,
+ createdById: currentUser?.id || null,
+ updatedById: currentUser?.id || null,
+ }, { transaction });
+
+ await transaction.commit();
+
+ return {
+ submission: submission.get({ plain: true }),
+ detection: detection.get({ plain: true }),
+ explanation: explanation.get({ plain: true }),
+ indicators: assessment.indicators,
+ privacy: {
+ mode: privacyMode ? 'redacted_local_processing' : 'full_text_stored_by_request',
+ sha256: signalHash,
+ },
+ };
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+
+ static async recentAssistantFindings(currentUser, limit = 8) {
+ const globalAccess = currentUser?.app_role?.globalAccess;
+ const organizationId = currentUser?.organizationsId
+ || currentUser?.organizations?.id
+ || currentUser?.organization?.id
+ || null;
+ const where = {};
+
+ if (!globalAccess) {
+ where[db.Sequelize.Op.or] = [
+ { createdById: currentUser?.id || null },
+ ];
+
+ if (organizationId) {
+ where[db.Sequelize.Op.or].push({ organizationsId: organizationId });
+ }
+ }
+
+ const rows = await db.threat_detections.findAll({
+ where,
+ include: [
+ { model: db.analysis_submissions, as: 'submission' },
+ { model: db.explanations, as: 'explanations_detection' },
+ ],
+ order: [['createdAt', 'desc']],
+ limit: Math.min(Number(limit) || 8, 25),
+ });
+
+ return rows.map((row) => row.get({ plain: true }));
+ }
+
};
diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx
index b4cb2c6..55b30a6 100644
--- a/frontend/src/components/AsideMenuLayer.tsx
+++ b/frontend/src/components/AsideMenuLayer.tsx
@@ -3,10 +3,9 @@ import { mdiLogout, mdiClose } from '@mdi/js'
import BaseIcon from './BaseIcon'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
-import { useAppSelector } from '../stores/hooks'
+import { useAppDispatch, useAppSelector } from '../stores/hooks'
import Link from 'next/link';
-import { useAppDispatch } from '../stores/hooks';
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx
index eb155e3..fb0fca2 100644
--- a/frontend/src/components/NavBarItem.tsx
+++ b/frontend/src/components/NavBarItem.tsx
@@ -1,6 +1,5 @@
-import React, {useEffect, useRef} from 'react'
+import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
-import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'
diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx
index 1b9907d..73d8391 100644
--- a/frontend/src/layouts/Authenticated.tsx
+++ b/frontend/src/layouts/Authenticated.tsx
@@ -1,5 +1,4 @@
-import React, { ReactNode, useEffect } from 'react'
-import { useState } from 'react'
+import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'
diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts
index 7378cc4..1267c8d 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -8,6 +8,15 @@ const menuAside: MenuAsideItem[] = [
label: 'Dashboard',
},
+ {
+ href: '/threat-analyzer',
+ label: 'Threat Analyzer',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiShieldAlert ?? icon.mdiTable,
+ permissions: 'CREATE_THREAT_DETECTIONS'
+ },
+
{
href: '/users/users-list',
label: 'Users',
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index 54b612c..f5e26c7 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -1,166 +1,166 @@
+import { mdiBrain, mdiLockCheckOutline, mdiRadar, mdiShieldAlert, mdiShieldCheck } from '@mdi/js'
+import Head from 'next/head'
+import Link from 'next/link'
+import React, { ReactElement } from 'react'
+import BaseButton from '../components/BaseButton'
+import BaseIcon from '../components/BaseIcon'
+import LayoutGuest from '../layouts/Guest'
+import { getPageTitle } from '../config'
-import React, { useEffect, useState } from 'react';
-import type { ReactElement } from 'react';
-import Head from 'next/head';
-import Link from 'next/link';
-import BaseButton from '../components/BaseButton';
-import CardBox from '../components/CardBox';
-import SectionFullScreen from '../components/SectionFullScreen';
-import LayoutGuest from '../layouts/Guest';
-import BaseDivider from '../components/BaseDivider';
-import BaseButtons from '../components/BaseButtons';
-import { getPageTitle } from '../config';
-import { useAppSelector } from '../stores/hooks';
-import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
-import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
-
-
-export default function Starter() {
- const [illustrationImage, setIllustrationImage] = useState({
- src: undefined,
- photographer: undefined,
- photographer_url: undefined,
- })
- const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
- const [contentType, setContentType] = useState('image');
- const [contentPosition, setContentPosition] = useState('background');
- const textColor = useAppSelector((state) => state.style.linkColor);
-
- const title = 'AI Cybersecurity Assistant'
-
- // Fetch Pexels image/video
- useEffect(() => {
- async function fetchData() {
- const image = await getPexelsImage();
- const video = await getPexelsVideo();
- setIllustrationImage(image);
- setIllustrationVideo(video);
- }
- fetchData();
- }, []);
-
- const imageBlock = (image) => (
-
- );
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
- Your browser does not support the video tag.
-
-
-
)
- }
- };
+const capabilities = [
+ {
+ icon: mdiShieldAlert,
+ title: 'Phishing triage',
+ text: 'Analyze emails, messages, and URLs for urgency, credential theft, impersonation, and suspicious destination signals.',
+ },
+ {
+ icon: mdiRadar,
+ title: 'Malware signals',
+ text: 'Review file names, hashes, behavior notes, and network indicators for executable, persistence, and C2 patterns.',
+ },
+ {
+ icon: mdiBrain,
+ title: 'Explainable scoring',
+ text: 'Every detection includes a risk score, verdict, and the top indicators that caused the assistant to flag it.',
+ },
+ {
+ icon: mdiLockCheckOutline,
+ title: 'Privacy-first storage',
+ text: 'The analyzer stores redacted samples and SHA-256 fingerprints by default to reduce sensitive-data retention.',
+ },
+]
+const Starter = () => {
return (
-
+
-
{getPageTitle('Starter Page')}
+
{getPageTitle('AI Cybersecurity Assistant')}
+
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
© 2026 {title} . All rights reserved
-
- Privacy Policy
+
+
+
+ Privacy
+
+
+
+
+
+
+
+
+
+
+ Local-first threat intelligence
+
+
+ Explainable AI for phishing and malware defense.
+
+
+ A modern cybersecurity assistant for individuals, schools, businesses, and public-sector teams: submit a suspicious message,
+ link, file signal, or network behavior and get a saved risk verdict with clear reasons.
+
+
+
+
+
+
+
+ 0
+ external AI calls
+
+
+ 100
+ risk scale
+
+
+ SHA-256
+ fingerprinting
+
+
+
+
+
+
+
+
+
Live workflow
+
Threat Analyzer
+
+
High
+
+
+
Suspicious invoice request
+
“Urgent — verify your password within 24 hours. Open the attachment and enable macros.”
+
+
+
+ Risk score
+ 81/100
+
+
+
+
+ {['Credential request', 'Urgent pressure', 'Macro lure', 'Brand impersonation'].map((item) => (
+
+ {item}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
MVP slice implemented
+
A complete first workflow, not just a dashboard.
+
+ Sign in to use the protected analyzer, create a detection, see the verdict, review explanations, and reopen recent analysis history.
+
+
+
+ {capabilities.map((item) => (
+
+
+
+
+
{item.title}
+
{item.text}
+
+ ))}
+
+
+
+
+
+
+ © 2026 SentinelAI. Built for ethical, explainable, privacy-aware cybersecurity workflows.{' '}
+
+ Login
+
+
- );
+ )
}
Starter.getLayout = function getLayout(page: ReactElement) {
- return
{page} ;
-};
+ return
{page}
+}
+export default Starter
diff --git a/frontend/src/pages/search.tsx b/frontend/src/pages/search.tsx
index 00f5168..005eb07 100644
--- a/frontend/src/pages/search.tsx
+++ b/frontend/src/pages/search.tsx
@@ -1,9 +1,7 @@
import React, { ReactElement, useEffect, useState } from 'react';
import Head from 'next/head';
import 'react-datepicker/dist/react-datepicker.css';
-import { useAppDispatch } from '../stores/hooks';
-
-import { useAppSelector } from '../stores/hooks';
+import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router';
import LayoutAuthenticated from '../layouts/Authenticated';
diff --git a/frontend/src/pages/threat-analyzer.tsx b/frontend/src/pages/threat-analyzer.tsx
new file mode 100644
index 0000000..74ac008
--- /dev/null
+++ b/frontend/src/pages/threat-analyzer.tsx
@@ -0,0 +1,400 @@
+import { mdiBrain, mdiRadar, mdiShieldAlert } from '@mdi/js'
+import Head from 'next/head'
+import React, { FormEvent, ReactElement, useEffect, useMemo, useState } from 'react'
+import axios from 'axios'
+import BaseButton from '../components/BaseButton'
+import CardBox from '../components/CardBox'
+import LayoutAuthenticated from '../layouts/Authenticated'
+import SectionMain from '../components/SectionMain'
+import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
+import BaseIcon from '../components/BaseIcon'
+import { getPageTitle } from '../config'
+
+type SubmissionType = 'email' | 'message' | 'url' | 'file' | 'network_traffic'
+
+type Indicator = {
+ label: string
+ weight: number
+}
+
+type Detection = {
+ id: string
+ threat_type: string
+ severity: string
+ risk_score: number | string
+ verdict: string
+ summary: string
+ detected_at?: string
+ submission?: {
+ title?: string
+ submission_type?: string
+ processing_location?: string
+ sha256?: string
+ }
+ explanations_detection?: Array<{
+ explanation_text?: string
+ top_indicators?: string
+ confidence?: number | string
+ }>
+}
+
+type AnalysisResponse = {
+ detection: Detection
+ indicators: Indicator[]
+ explanation: {
+ explanation_text?: string
+ confidence?: number | string
+ }
+ privacy: {
+ mode: string
+ sha256: string
+ }
+}
+
+const typeOptions: Array<{ value: SubmissionType; label: string; helper: string }> = [
+ { value: 'email', label: 'Email', helper: 'Paste headers or body text.' },
+ { value: 'message', label: 'Message', helper: 'Analyze SMS, chat, or DM copy.' },
+ { value: 'url', label: 'Website / URL', helper: 'Inspect a suspicious link.' },
+ { value: 'file', label: 'File metadata', helper: 'Use name, hash, and observed behavior.' },
+ { value: 'network_traffic', label: 'Network activity', helper: 'Summarize ports, domains, and process behavior.' },
+]
+
+const severityStyles: Record
= {
+ critical: 'bg-red-600 text-white ring-red-300',
+ high: 'bg-orange-500 text-white ring-orange-300',
+ medium: 'bg-amber-400 text-slate-950 ring-amber-200',
+ low: 'bg-sky-500 text-white ring-sky-200',
+ info: 'bg-emerald-500 text-white ring-emerald-200',
+}
+
+const getIndicatorList = (detection?: Detection, fallback: Indicator[] = []) => {
+ if (fallback.length) return fallback
+ const raw = detection?.explanations_detection?.[0]?.top_indicators
+ if (!raw) return []
+
+ try {
+ const parsed = JSON.parse(raw)
+ return Array.isArray(parsed) ? parsed : []
+ } catch (error) {
+ console.error('Failed to parse threat indicators:', error)
+ return []
+ }
+}
+
+const ThreatAnalyzerPage = () => {
+ const [submissionType, setSubmissionType] = useState('email')
+ const [title, setTitle] = useState('Suspicious invoice request')
+ const [content, setContent] = useState(
+ 'Urgent: verify your Microsoft password within 24 hours or your account will be closed. Open the attachment and enable macros.',
+ )
+ const [url, setUrl] = useState('')
+ const [fileName, setFileName] = useState('')
+ const [sha256, setSha256] = useState('')
+ const [networkActivity, setNetworkActivity] = useState('')
+ const [privacyMode, setPrivacyMode] = useState(true)
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [errorMessage, setErrorMessage] = useState('')
+ const [result, setResult] = useState(null)
+ const [recent, setRecent] = useState([])
+ const [selectedDetection, setSelectedDetection] = useState(null)
+
+ const activeHelper = useMemo(
+ () => typeOptions.find((item) => item.value === submissionType)?.helper,
+ [submissionType],
+ )
+
+ const loadRecent = async () => {
+ const response = await axios.get('/threat_detections/assistant/recent?limit=8')
+ setRecent(Array.isArray(response.data?.rows) ? response.data.rows : [])
+ }
+
+ useEffect(() => {
+ loadRecent().catch((error) => {
+ console.error('Failed to load recent detections:', error)
+ })
+ }, [])
+
+ const selectedOrResult = selectedDetection || result?.detection || null
+ const indicators = getIndicatorList(selectedDetection || undefined, selectedDetection ? [] : result?.indicators || [])
+ const riskScore = selectedOrResult ? Number(selectedOrResult.risk_score || 0) : 0
+
+ const handleSubmit = async (event: FormEvent) => {
+ event.preventDefault()
+ setErrorMessage('')
+ setSelectedDetection(null)
+ setIsSubmitting(true)
+
+ try {
+ const response = await axios.post('/threat_detections/analyze', {
+ data: {
+ submission_type: submissionType,
+ title,
+ content_text: content,
+ url,
+ fileName,
+ sha256,
+ network_activity: networkActivity,
+ privacy_mode: privacyMode,
+ },
+ })
+ setResult(response.data)
+ await loadRecent()
+ } catch (error) {
+ console.error('Threat analysis failed:', error)
+ if (axios.isAxiosError(error)) {
+ setErrorMessage(error.response?.data?.message || 'Analysis failed. Please check the input and try again.')
+ } else {
+ setErrorMessage('Analysis failed. Please check the input and try again.')
+ }
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ return (
+ <>
+
+ {getPageTitle('Threat Analyzer')}
+
+
+
+
+
+
+
+
+
+
+
+
+
Local-first triage
+
Analyze suspicious content
+
+ Submit an email, link, file signal, or network observation. The assistant creates a saved detection,
+ risk score, verdict, and explanation without calling an external AI provider.
+
+
+
+
+ Explainable AI
+
+
Indicators are stored with every result for review and tuning.
+
+
+
+
+
+
+
+
+
+ {selectedOrResult ? (
+
+
+
+
Detection result
+
+ {selectedOrResult.verdict?.replace('_', ' ')} · {selectedOrResult.threat_type?.replace('_', ' ')}
+
+
+
+ {selectedOrResult.severity}
+
+
+
+
+
+ Risk score
+ {riskScore}/100
+
+
+
+
+
+ {selectedOrResult.summary}
+
+
+
+
Why it was flagged
+ {indicators.length ? (
+
+ {indicators.map((indicator) => (
+
+ {indicator.label}
+ +{indicator.weight}
+
+ ))}
+
+ ) : (
+
+ No high-risk indicators were stored for this result.
+
+ )}
+
+
+ {result?.privacy && !selectedDetection && (
+
+ Privacy: {result.privacy.mode.replaceAll('_', ' ')} · fingerprint {result.privacy.sha256.slice(0, 16)}…
+
+ )}
+
+ ) : (
+
+
+
+
+
Awaiting first scan
+
+ Run an analysis to see the verdict, risk score, confidence, and explainability indicators here.
+
+
+ )}
+
+
+
+
+
+
Recent detections
+
Saved analysis history
+
+
+ {recent.length ? (
+
+ {recent.map((item) => (
+
setSelectedDetection(item)}
+ className="w-full rounded-2xl border border-slate-100 p-4 text-left transition hover:-translate-y-0.5 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-cyan-300 dark:border-slate-700"
+ >
+
+
+
{item.submission?.title || item.summary}
+
+ {item.threat_type?.replace('_', ' ')} · {item.verdict?.replace('_', ' ')}
+
+
+
+ {item.risk_score}
+
+
+
+ ))}
+
+ ) : (
+
+ No detections yet. Your first scan will appear here automatically.
+
+ )}
+
+
+
+
+ >
+ )
+}
+
+ThreatAnalyzerPage.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
+
+export default ThreatAnalyzerPage