This commit is contained in:
Flatlogic Bot 2026-06-13 09:42:18 +00:00
parent 53e2b59941
commit cab119400a
9 changed files with 852 additions and 168 deletions

View File

@ -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);

View File

@ -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 }));
}
};

View File

@ -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';

View File

@ -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'

View File

@ -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'

View File

@ -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',

View File

@ -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) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
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 (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<div className="min-h-screen bg-slate-950 text-white">
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('AI Cybersecurity Assistant')}</title>
<meta
name="description"
content="A local-first AI cybersecurity assistant for phishing, malware, risk scoring, and explainable threat detection."
/>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your AI Cybersecurity Assistant app!"/>
<div className="space-y-3">
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center '>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</BaseButtons>
</CardBox>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
<header className="mx-auto flex max-w-7xl items-center justify-between px-6 py-6">
<Link href="/" className="flex items-center gap-3 font-black tracking-tight">
<span className="rounded-2xl bg-cyan-300 p-2 text-slate-950 shadow-lg shadow-cyan-500/20">
<BaseIcon path={mdiShieldCheck} />
</span>
SentinelAI
</Link>
</div>
<nav className="flex items-center gap-3 text-sm">
<Link href="/privacy-policy" className="hidden text-slate-300 hover:text-white sm:inline">
Privacy
</Link>
<BaseButton href="/login" label="Login" color="white" small />
</nav>
</header>
<main>
<section className="relative isolate overflow-hidden px-6 py-16 md:py-24">
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_20%_10%,rgba(34,211,238,0.26),transparent_30%),radial-gradient(circle_at_85%_30%,rgba(16,185,129,0.20),transparent_28%),linear-gradient(135deg,#020617_0%,#0f172a_48%,#111827_100%)]" />
<div className="mx-auto grid max-w-7xl gap-10 lg:grid-cols-[1.05fr_0.95fr] lg:items-center">
<div>
<div className="mb-6 inline-flex rounded-full border border-cyan-300/30 bg-cyan-300/10 px-4 py-2 text-xs font-bold uppercase tracking-[0.28em] text-cyan-100">
Local-first threat intelligence
</div>
<h1 className="max-w-4xl text-5xl font-black leading-[0.95] tracking-tight md:text-7xl">
Explainable AI for phishing and malware defense.
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-slate-300">
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.
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<BaseButton href="/login" label="Open admin interface" color="success" className="text-base font-bold" />
<BaseButton href="/login" label="Sign in to analyze" color="info" outline className="border-cyan-300 text-base font-bold" />
</div>
<div className="mt-8 grid max-w-2xl grid-cols-3 gap-3 text-center text-xs text-slate-300">
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<strong className="block text-2xl text-white">0</strong>
external AI calls
</div>
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<strong className="block text-2xl text-white">100</strong>
risk scale
</div>
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<strong className="block text-2xl text-white">SHA-256</strong>
fingerprinting
</div>
</div>
</div>
<div className="rounded-[2rem] border border-white/10 bg-white/10 p-5 shadow-2xl shadow-cyan-950/40 backdrop-blur">
<div className="rounded-[1.5rem] bg-slate-950 p-5">
<div className="mb-5 flex items-center justify-between">
<div>
<p className="text-xs uppercase tracking-[0.28em] text-cyan-200">Live workflow</p>
<h2 className="text-2xl font-black">Threat Analyzer</h2>
</div>
<span className="rounded-full bg-red-500 px-3 py-1 text-xs font-black uppercase">High</span>
</div>
<div className="space-y-3 rounded-2xl bg-slate-900 p-4 text-sm text-slate-300">
<p className="font-semibold text-white">Suspicious invoice request</p>
<p>Urgent verify your password within 24 hours. Open the attachment and enable macros.</p>
</div>
<div className="mt-5">
<div className="mb-2 flex justify-between text-sm font-semibold text-slate-300">
<span>Risk score</span>
<span>81/100</span>
</div>
<div className="h-4 rounded-full bg-slate-800">
<div className="h-full w-[81%] rounded-full bg-gradient-to-r from-emerald-400 via-amber-400 to-red-500" />
</div>
</div>
<div className="mt-5 grid gap-3 sm:grid-cols-2">
{['Credential request', 'Urgent pressure', 'Macro lure', 'Brand impersonation'].map((item) => (
<div key={item} className="rounded-2xl border border-cyan-300/20 bg-cyan-300/10 p-3 text-sm text-cyan-50">
{item}
</div>
))}
</div>
</div>
</div>
</div>
</section>
<section className="bg-slate-50 px-6 py-16 text-slate-950">
<div className="mx-auto max-w-7xl">
<div className="mb-10 max-w-3xl">
<p className="text-sm font-black uppercase tracking-[0.25em] text-emerald-600">MVP slice implemented</p>
<h2 className="mt-3 text-4xl font-black tracking-tight">A complete first workflow, not just a dashboard.</h2>
<p className="mt-4 text-slate-600">
Sign in to use the protected analyzer, create a detection, see the verdict, review explanations, and reopen recent analysis history.
</p>
</div>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
{capabilities.map((item) => (
<div key={item.title} className="rounded-3xl border border-slate-200 bg-white p-6 shadow-xl shadow-slate-200/60">
<div className="mb-5 inline-flex rounded-2xl bg-slate-950 p-3 text-cyan-300">
<BaseIcon path={item.icon} />
</div>
<h3 className="text-xl font-black">{item.title}</h3>
<p className="mt-3 text-sm leading-6 text-slate-600">{item.text}</p>
</div>
))}
</div>
</div>
</section>
</main>
<footer className="border-t border-white/10 px-6 py-8 text-center text-sm text-slate-400">
© 2026 SentinelAI. Built for ethical, explainable, privacy-aware cybersecurity workflows.{' '}
<Link href="/login" className="font-semibold text-cyan-200 hover:text-white">
Login
</Link>
</footer>
</div>
);
)
}
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
return <LayoutGuest>{page}</LayoutGuest>
}
export default Starter

View File

@ -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';

View File

@ -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<string, string> = {
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<SubmissionType>('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<AnalysisResponse | null>(null)
const [recent, setRecent] = useState<Detection[]>([])
const [selectedDetection, setSelectedDetection] = useState<Detection | null>(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 (
<>
<Head>
<title>{getPageTitle('Threat Analyzer')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiShieldAlert} title="AI Threat Analyzer" main>
<BaseButton href="/threat_detections/threat_detections-list" label="All detections" color="info" />
</SectionTitleLineWithButton>
<div className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
<CardBox className="overflow-hidden border-0 bg-slate-950 text-white shadow-2xl shadow-cyan-950/30" cardBoxClassName="p-0">
<div className="relative isolate p-6 md:p-8">
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_top_left,_rgba(34,211,238,0.28),_transparent_35%),radial-gradient(circle_at_bottom_right,_rgba(16,185,129,0.22),_transparent_32%)]" />
<div className="mb-6 flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.35em] text-cyan-200">Local-first triage</p>
<h2 className="mt-2 text-3xl font-black tracking-tight md:text-4xl">Analyze suspicious content</h2>
<p className="mt-3 max-w-2xl text-sm leading-6 text-slate-300">
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.
</p>
</div>
<div className="rounded-2xl border border-cyan-300/20 bg-white/10 p-4 text-sm backdrop-blur">
<div className="flex items-center gap-2 font-semibold text-cyan-100">
<BaseIcon path={mdiBrain} /> Explainable AI
</div>
<p className="mt-1 text-slate-300">Indicators are stored with every result for review and tuning.</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<div className="grid gap-3 md:grid-cols-5">
{typeOptions.map((option) => (
<button
key={option.value}
type="button"
onClick={() => setSubmissionType(option.value)}
className={`rounded-2xl border p-4 text-left transition focus:outline-none focus:ring-2 focus:ring-cyan-300 ${
submissionType === option.value
? 'border-cyan-300 bg-cyan-300 text-slate-950 shadow-lg shadow-cyan-950/40'
: 'border-white/10 bg-white/5 text-slate-200 hover:border-cyan-200/60 hover:bg-white/10'
}`}
>
<span className="block text-sm font-bold">{option.label}</span>
<span className="mt-1 block text-xs opacity-80">{option.helper}</span>
</button>
))}
</div>
<div className="grid gap-4 md:grid-cols-2">
<label className="block">
<span className="text-sm font-semibold text-slate-200">Case title</span>
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
className="mt-2 w-full rounded-xl border border-white/10 bg-white/10 px-4 py-3 text-white placeholder:text-slate-500 focus:border-cyan-300 focus:outline-none focus:ring-2 focus:ring-cyan-300/30"
placeholder="e.g. Payroll redirect email"
maxLength={180}
/>
</label>
<label className="block">
<span className="text-sm font-semibold text-slate-200">URL or destination</span>
<input
value={url}
onChange={(event) => setUrl(event.target.value)}
className="mt-2 w-full rounded-xl border border-white/10 bg-white/10 px-4 py-3 text-white placeholder:text-slate-500 focus:border-cyan-300 focus:outline-none focus:ring-2 focus:ring-cyan-300/30"
placeholder="https://example.com/login"
/>
</label>
</div>
<label className="block">
<span className="text-sm font-semibold text-slate-200">Signals to analyze</span>
<textarea
value={content}
onChange={(event) => setContent(event.target.value)}
className="mt-2 h-40 w-full rounded-2xl border border-white/10 bg-slate-900/80 px-4 py-3 text-white placeholder:text-slate-500 focus:border-cyan-300 focus:outline-none focus:ring-2 focus:ring-cyan-300/30"
placeholder={activeHelper}
maxLength={20000}
/>
</label>
<div className="grid gap-4 md:grid-cols-3">
<input
value={fileName}
onChange={(event) => setFileName(event.target.value)}
className="rounded-xl border border-white/10 bg-white/10 px-4 py-3 text-white placeholder:text-slate-500 focus:border-cyan-300 focus:outline-none focus:ring-2 focus:ring-cyan-300/30"
placeholder="File name: invoice.xlsm"
/>
<input
value={sha256}
onChange={(event) => setSha256(event.target.value)}
className="rounded-xl border border-white/10 bg-white/10 px-4 py-3 text-white placeholder:text-slate-500 focus:border-cyan-300 focus:outline-none focus:ring-2 focus:ring-cyan-300/30"
placeholder="SHA-256 hash (optional)"
/>
<input
value={networkActivity}
onChange={(event) => setNetworkActivity(event.target.value)}
className="rounded-xl border border-white/10 bg-white/10 px-4 py-3 text-white placeholder:text-slate-500 focus:border-cyan-300 focus:outline-none focus:ring-2 focus:ring-cyan-300/30"
placeholder="Network: beacon, port 4444"
/>
</div>
<div className="flex flex-col gap-4 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 p-4 md:flex-row md:items-center md:justify-between">
<label className="flex items-start gap-3 text-sm text-emerald-50">
<input
type="checkbox"
checked={privacyMode}
onChange={(event) => setPrivacyMode(event.target.checked)}
className="mt-1 rounded border-emerald-200 text-emerald-500 focus:ring-emerald-300"
/>
<span>
<strong>Privacy mode on:</strong> store a redacted excerpt and SHA-256 fingerprint instead of full sensitive text.
</span>
</label>
<BaseButton type="submit" label={isSubmitting ? 'Analyzing…' : 'Analyze threat'} color="success" disabled={isSubmitting} />
</div>
{errorMessage && (
<div className="rounded-2xl border border-red-300/30 bg-red-500/20 p-4 text-sm text-red-50">
{errorMessage}
</div>
)}
</form>
</div>
</CardBox>
<div className="space-y-6">
<CardBox className="border-0 bg-white shadow-xl dark:bg-slate-900">
{selectedOrResult ? (
<div className="space-y-5">
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-xs font-bold uppercase tracking-[0.25em] text-slate-400">Detection result</p>
<h3 className="mt-1 text-2xl font-black text-slate-900 dark:text-white">
{selectedOrResult.verdict?.replace('_', ' ')} · {selectedOrResult.threat_type?.replace('_', ' ')}
</h3>
</div>
<span className={`rounded-full px-3 py-1 text-xs font-black uppercase ring-4 ${severityStyles[selectedOrResult.severity] || severityStyles.info}`}>
{selectedOrResult.severity}
</span>
</div>
<div>
<div className="mb-2 flex justify-between text-sm font-semibold text-slate-600 dark:text-slate-300">
<span>Risk score</span>
<span>{riskScore}/100</span>
</div>
<div className="h-4 overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800">
<div
className="h-full rounded-full bg-gradient-to-r from-emerald-400 via-amber-400 to-red-500"
style={{ width: `${Math.min(riskScore, 100)}%` }}
/>
</div>
</div>
<p className="rounded-2xl bg-slate-50 p-4 text-sm leading-6 text-slate-700 dark:bg-slate-800 dark:text-slate-200">
{selectedOrResult.summary}
</p>
<div>
<p className="mb-3 text-sm font-bold text-slate-700 dark:text-slate-200">Why it was flagged</p>
{indicators.length ? (
<div className="grid gap-2">
{indicators.map((indicator) => (
<div key={`${indicator.label}-${indicator.weight}`} className="flex items-center justify-between rounded-xl border border-slate-100 p-3 text-sm dark:border-slate-700">
<span>{indicator.label}</span>
<span className="font-black text-red-500">+{indicator.weight}</span>
</div>
))}
</div>
) : (
<div className="rounded-2xl border border-dashed border-slate-300 p-4 text-sm text-slate-500 dark:border-slate-700">
No high-risk indicators were stored for this result.
</div>
)}
</div>
{result?.privacy && !selectedDetection && (
<div className="rounded-2xl bg-emerald-50 p-4 text-xs text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-100">
Privacy: {result.privacy.mode.replaceAll('_', ' ')} · fingerprint {result.privacy.sha256.slice(0, 16)}
</div>
)}
</div>
) : (
<div className="flex min-h-[420px] flex-col items-center justify-center text-center">
<div className="rounded-full bg-cyan-50 p-5 text-cyan-600 dark:bg-cyan-950 dark:text-cyan-200">
<BaseIcon path={mdiRadar} size={44} />
</div>
<h3 className="mt-5 text-2xl font-black text-slate-900 dark:text-white">Awaiting first scan</h3>
<p className="mt-2 max-w-sm text-sm text-slate-500 dark:text-slate-400">
Run an analysis to see the verdict, risk score, confidence, and explainability indicators here.
</p>
</div>
)}
</CardBox>
<CardBox className="border-0 shadow-xl">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-xs font-bold uppercase tracking-[0.25em] text-slate-400">Recent detections</p>
<h3 className="text-xl font-black text-slate-900 dark:text-white">Saved analysis history</h3>
</div>
</div>
{recent.length ? (
<div className="space-y-3">
{recent.map((item) => (
<button
key={item.id}
type="button"
onClick={() => 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"
>
<div className="flex items-center justify-between gap-3">
<div>
<p className="font-bold text-slate-900 dark:text-white">{item.submission?.title || item.summary}</p>
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">
{item.threat_type?.replace('_', ' ')} · {item.verdict?.replace('_', ' ')}
</p>
</div>
<span className={`rounded-full px-2 py-1 text-[10px] font-black uppercase ${severityStyles[item.severity] || severityStyles.info}`}>
{item.risk_score}
</span>
</div>
</button>
))}
</div>
) : (
<div className="rounded-2xl border border-dashed border-slate-300 p-6 text-center text-sm text-slate-500 dark:border-slate-700">
No detections yet. Your first scan will appear here automatically.
</div>
)}
</CardBox>
</div>
</div>
</SectionMain>
</>
)
}
ThreatAnalyzerPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
}
export default ThreatAnalyzerPage