Smart AI
This commit is contained in:
parent
53e2b59941
commit
cab119400a
@ -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);
|
||||
|
||||
@ -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 }));
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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';
|
||||
|
||||
400
frontend/src/pages/threat-analyzer.tsx
Normal file
400
frontend/src/pages/threat-analyzer.tsx
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user