add new proofsystem

This commit is contained in:
rodrigo committed 2025-07-27 19:20:10 -03:00
1 parent a63aaeb655
commit 2399cb1f2d
11 files changed
+2009 -6

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useAuth } from '../../hooks/useAuth';
import {
HomeIcon,
DocumentTextIcon,
UserIcon,
CogIcon,
LogoutIcon,
ShieldCheckIcon,
ViewGridIcon,
ClipboardListIcon
} from '@heroicons/react/outline';
const Sidebar: React.FC = () => {
const { pathname } = useLocation();
const { user, logout } = useAuth();
const isActive = (path: string) => {
return pathname === path ? 'bg-blue-500 text-white' : 'text-gray-700';
};
const menuItems = [
{
name: 'Início',
icon: HomeIcon,
href: '/'
},
{
name: 'Provas Digitais',
icon: DocumentTextIcon,
children: [
{ name: 'Verificar Prova', href: '/provas/verificar' },
{ name: 'Enviar Prova', href: '/provas/upload' },
{ name: 'Status da Prova', href: '/provas/status' },
{ name: 'Captura Web', href: '/web-proofs/capture' },
{ name: 'Relatórios Web', href: '/web-proofs' }
]
},
{
name: 'Usuário',
icon: UserIcon,
href: '/profile'
},
{
name: 'Configurações',
icon: CogIcon,
href: '/settings'
},
{
name: 'Sair',
icon: LogoutIcon,
action: logout
}
];
return (
<div className="bg-white shadow-md rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-800">Painel de Controle</h2>
</div>
<nav>
<ul className="space-y-2">
{menuItems.map((item) => (
<li key={item.name}>
{item.children ? (
<div>
<p className="text-xs font-semibold text-gray-500 uppercase">{item.name}</p>
<ul className="mt-1 space-y-1">
{item.children.map((child) => (
<li key={child.name}>
<Link
to={child.href}
className={`flex items-center p-2 text-sm rounded-md transition-all duration-200
${isActive(child.href)} hover:bg-blue-600 hover:text-white`}
>
<ViewGridIcon className="w-5 h-5 mr-2" />
{child.name}
</Link>
</li>
))}
</ul>
</div>
) : (
<Link
to={item.href}
onClick={item.action}
className={`flex items-center p-2 text-sm rounded-md transition-all duration-200
${isActive(item.href)} hover:bg-blue-600 hover:text-white`}
>
<item.icon className="w-5 h-5 mr-2" />
{item.name}
</Link>
)}
</li>
))}
</ul>
</nav>
</div>
);
};
export default Sidebar;
+501
View File
@@ -0,0 +1,501 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
interface CaptureSession {
id: string;
startTime: string;
status: 'PREPARANDO' | 'ATIVO' | 'GRAVANDO' | 'FINALIZANDO' | 'CONCLUIDO' | 'ERRO';
environment: {
sessionId: string;
userAgent: string;
viewportSize: { width: number; height: number };
ipAddress: string;
timestamp: string;
};
}
interface WebProofMetadata {
networkData: {
sourceIP: string;
targetIP: string;
routePath: string[];
dnsRecords: any[];
};
timestamps: {
sessionStart: string;
captureStart: string;
captureEnd: string;
trustedTimestamp?: string;
};
hashes: {
sessionHash: string;
contentHash: string;
videoHash?: string;
pageHash: string;
};
technicalData: {
browserFingerprint: string;
navigationTrace: any[];
domSnapshot: string;
screenshots: string[];
};
}
const SESSION_STATES = {
PREPARING: 'PREPARANDO',
ACTIVE: 'ATIVO',
RECORDING: 'GRAVANDO',
FINALIZING: 'FINALIZANDO',
COMPLETED: 'CONCLUIDO',
ERROR: 'ERRO'
} as const;
export function WebProofCapture() {
const [session, setSession] = useState<CaptureSession | null>(null);
const [isRecording, setIsRecording] = useState(false);
const [targetUrl, setTargetUrl] = useState('');
const [captureDescription, setCaptureDescription] = useState('');
const [metadata, setMetadata] = useState<WebProofMetadata | null>(null);
const [error, setError] = useState<string | null>(null);
const [progress, setProgress] = useState(0);
const [screenshots, setScreenshots] = useState<string[]>([]);
const iframeRef = useRef<HTMLIFrameElement>(null);
const recordedChunksRef = useRef<Blob[]>([]);
const navigate = useNavigate();
// Inicializar ambiente isolado
const initializeIsolatedEnvironment = useCallback(async () => {
try {
setProgress(10);
setError(null);
// Simular criação de ambiente isolado
const sessionId = `webproof_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Coletar informações do ambiente
const environmentData = {
sessionId,
userAgent: navigator.userAgent,
viewportSize: {
width: window.innerWidth,
height: window.innerHeight
},
ipAddress: await getClientIP(),
timestamp: new Date().toISOString()
};
setProgress(30);
const newSession: CaptureSession = {
id: sessionId,
startTime: new Date().toISOString(),
status: SESSION_STATES.PREPARING,
environment: environmentData
};
setSession(newSession);
setProgress(50);
// Inicializar coleta de metadados
await initializeMetadataCollection(sessionId);
setProgress(100);
setSession(prev => prev ? { ...prev, status: SESSION_STATES.ACTIVE } : null);
} catch (error) {
console.error('Erro ao inicializar ambiente:', error);
setError('Falha ao inicializar ambiente isolado');
}
}, []);
// Obter IP do cliente
const getClientIP = async (): Promise<string> => {
try {
const response = await fetch('/api/web-proofs/client-info');
if (!response.ok) {
throw new Error('Falha ao obter informações do cliente');
}
const data = await response.json();
return data.ip || '127.0.0.1';
} catch (error) {
console.warn('Erro ao obter IP do cliente:', error);
return '127.0.0.1';
}
};
// Inicializar coleta de metadados
const initializeMetadataCollection = async (sessionId: string) => {
const initialMetadata: WebProofMetadata = {
networkData: {
sourceIP: await getClientIP(),
targetIP: '',
routePath: [],
dnsRecords: []
},
timestamps: {
sessionStart: new Date().toISOString(),
captureStart: '',
captureEnd: ''
},
hashes: {
sessionHash: await generateHash(sessionId),
contentHash: '',
pageHash: ''
},
technicalData: {
browserFingerprint: generateBrowserFingerprint(),
navigationTrace: [],
domSnapshot: '',
screenshots: []
}
};
setMetadata(initialMetadata);
};
// Gerar hash seguro
const generateHash = async (data: string): Promise<string> => {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data + Date.now());
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
};
// Gerar fingerprint do navegador
const generateBrowserFingerprint = (): string => {
const fingerprint = {
userAgent: navigator.userAgent,
language: navigator.language,
platform: navigator.platform,
cookieEnabled: navigator.cookieEnabled,
doNotTrack: navigator.doNotTrack,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
screen: {
width: screen.width,
height: screen.height,
colorDepth: screen.colorDepth
}
};
return btoa(JSON.stringify(fingerprint));
};
// Validar URL
const validateUrl = (url: string): boolean => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
// Iniciar captura
const startCapture = async () => {
if (!targetUrl.trim()) {
toast.error('Por favor, insira uma URL válida');
return;
}
if (!validateUrl(targetUrl)) {
toast.error('URL inválida. Use formato: https://exemplo.com');
return;
}
try {
setIsRecording(true);
setSession(prev => prev ? { ...prev, status: SESSION_STATES.RECORDING } : null);
// Atualizar metadados com início da captura
if (metadata) {
const updatedMetadata = {
...metadata,
timestamps: {
...metadata.timestamps,
captureStart: new Date().toISOString()
}
};
setMetadata(updatedMetadata);
}
// Navegar para URL alvo no iframe
if (iframeRef.current) {
iframeRef.current.src = targetUrl;
}
toast.success('Captura iniciada - ambiente isolado ativo');
} catch (error) {
console.error('Erro ao iniciar captura:', error);
setError('Falha ao iniciar captura');
setIsRecording(false);
}
};
// Capturar screenshot
const captureScreenshot = async () => {
try {
if (!iframeRef.current) return;
// Simular captura de screenshot
const timestamp = new Date().toISOString();
const screenshotData = `screenshot_${Date.now()}`;
setScreenshots(prev => [...prev, screenshotData]);
// Registrar ação nos metadados
if (metadata && isRecording) {
const updatedMetadata = {
...metadata,
technicalData: {
...metadata.technicalData,
screenshots: [...metadata.technicalData.screenshots, screenshotData],
navigationTrace: [...metadata.technicalData.navigationTrace, {
type: 'screenshot',
timestamp,
url: targetUrl
}]
}
};
setMetadata(updatedMetadata);
}
toast.success('Screenshot capturado');
} catch (error) {
console.error('Erro ao capturar screenshot:', error);
toast.error('Falha ao capturar screenshot');
}
};
// Finalizar captura
const finalizeCaptureSession = async () => {
try {
setSession(prev => prev ? { ...prev, status: SESSION_STATES.FINALIZING } : null);
setIsRecording(false);
if (!metadata || !session) {
throw new Error('Sessão ou metadados não encontrados');
}
// Finalizar metadados
const finalMetadata = {
...metadata,
timestamps: {
...metadata.timestamps,
captureEnd: new Date().toISOString(),
trustedTimestamp: await getTrustedTimestamp()
},
hashes: {
...metadata.hashes,
contentHash: await generateHash(targetUrl + Date.now()),
pageHash: await generateHash(`page_${session.id}_${targetUrl}`)
}
};
// Enviar para processamento
const response = await fetch('/api/web-proofs/sessions', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: session.id,
targetUrl,
description: captureDescription,
metadata: finalMetadata,
environment: session.environment,
screenshots
})
});
if (!response.ok) {
throw new Error('Falha ao processar sessão');
}
const result = await response.json();
setSession(prev => prev ? { ...prev, status: SESSION_STATES.COMPLETED } : null);
toast.success('Captura finalizada com sucesso');
// Navegar para relatório
navigate(`/web-proofs/relatorio/${result.sessionId}`);
} catch (error) {
console.error('Erro ao finalizar captura:', error);
setError('Falha ao finalizar captura');
setSession(prev => prev ? { ...prev, status: SESSION_STATES.ERROR } : null);
}
};
// Obter timestamp confiável
const getTrustedTimestamp = async (): Promise<string> => {
try {
const response = await fetch('/api/web-proofs/trusted-timestamp');
const data = await response.json();
return data.timestamp;
} catch {
return new Date().toISOString();
}
};
return (
<div className="min-h-screen bg-gray-50 py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">Captura de Provas Web</h1>
<p className="mt-2 text-lg text-gray-600">
Coleta forense de evidências digitais em ambiente isolado
</p>
</div>
{!session && (
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Inicializar Ambiente Seguro</h2>
<p className="text-gray-600 mb-6">
O sistema criará um ambiente virtual isolado para garantir a integridade da captura.
</p>
<button
onClick={initializeIsolatedEnvironment}
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 transition-colors"
>
Inicializar Ambiente Isolado
</button>
{progress > 0 && (
<div className="mt-4">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<p className="text-sm text-gray-500 mt-2">Preparando ambiente... {progress}%</p>
</div>
)}
</div>
)}
{session && session.status === SESSION_STATES.ACTIVE && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-1 bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Configuração da Captura</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
URL de Destino
</label>
<input
type="url"
value={targetUrl}
onChange={(e) => setTargetUrl(e.target.value)}
placeholder="https://exemplo.com"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Descrição da Prova
</label>
<textarea
value={captureDescription}
onChange={(e) => setCaptureDescription(e.target.value)}
rows={3}
placeholder="Descreva o que será capturado..."
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
onClick={startCapture}
disabled={!targetUrl.trim() || isRecording}
className="w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 disabled:bg-gray-400 transition-colors"
>
{isRecording ? 'Capturando...' : 'Iniciar Captura'}
</button>
</div>
</div>
<div className="lg:col-span-2 bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Ambiente Virtual Isolado</h2>
<div className="border-2 border-gray-200 rounded-lg h-96 bg-gray-50 flex items-center justify-center">
{isRecording && targetUrl ? (
<iframe
ref={iframeRef}
src={targetUrl}
className="w-full h-full rounded"
title="Captura Web"
sandbox="allow-scripts allow-same-origin"
/>
) : (
<p className="text-gray-500">Ambiente aguardando URL de destino</p>
)}
</div>
<div className="mt-4 flex gap-2">
<button
onClick={captureScreenshot}
disabled={!isRecording}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:bg-gray-400 transition-colors"
>
Capturar Screenshot
</button>
<button
onClick={finalizeCaptureSession}
disabled={!isRecording}
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 disabled:bg-gray-400 transition-colors"
>
Finalizar Captura
</button>
</div>
{screenshots.length > 0 && (
<div className="mt-4">
<p className="text-sm text-gray-600">
Screenshots capturados: {screenshots.length}
</p>
</div>
)}
</div>
</div>
)}
{session && session.status === SESSION_STATES.RECORDING && (
<div className="fixed top-4 right-4 bg-red-600 text-white px-4 py-2 rounded-lg shadow-lg">
<div className="flex items-center">
<div className="w-3 h-3 bg-white rounded-full mr-2 animate-pulse"></div>
GRAVANDO
</div>
</div>
)}
{error && (
<div className="max-w-2xl mx-auto mt-6 bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex">
<div className="text-red-600">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Erro na Captura</h3>
<div className="mt-2 text-sm text-red-700">
<p>{error}</p>
</div>
</div>
</div>
</div>
)}
</div>
</div>
);
}
export default WebProofCapture;
+227
View File
@@ -0,0 +1,227 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
interface WebProofSession {
sessionId: string;
targetUrl: string;
description: string;
status: string;
createdAt: string;
processedAt?: string;
error?: string;
}
export function WebProofList() {
const [sessions, setSessions] = useState<WebProofSession[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchSessions = async () => {
try {
const response = await fetch('/api/web-proofs/admin/sessions?limit=50');
if (!response.ok) {
throw new Error('Erro ao carregar sessões');
}
const data = await response.json();
setSessions(data.sessions || data); // Compatibilidade com formato paginado
} catch (error) {
console.error('Erro ao buscar sessões:', error);
setError('Erro ao carregar lista de sessões');
} finally {
setLoading(false);
}
};
fetchSessions();
}, []);
const formatDate = (dateString: string) => {
try {
return format(new Date(dateString), 'dd/MM/yyyy HH:mm', { locale: ptBR });
} catch {
return 'Data inválida';
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'CONCLUIDO':
return 'bg-green-100 text-green-800';
case 'PROCESSANDO':
return 'bg-blue-100 text-blue-800';
case 'ERRO':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
if (loading) {
return (
<div className="p-6">
<div className="bg-white rounded-lg shadow p-6">
<div className="animate-pulse">
<div className="h-6 bg-gray-300 rounded w-1/4 mb-4"></div>
<div className="space-y-3">
<div className="h-4 bg-gray-300 rounded"></div>
<div className="h-4 bg-gray-300 rounded w-5/6"></div>
<div className="h-4 bg-gray-300 rounded w-4/6"></div>
</div>
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="p-6">
<div className="bg-white rounded-lg shadow p-6 text-center">
<div className="text-red-600 mb-4">{error}</div>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Tentar Novamente
</button>
</div>
</div>
);
}
return (
<div className="p-6">
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex justify-between items-center">
<div>
<h1 className="text-xl font-semibold text-gray-900">Capturas Web Forenses</h1>
<p className="mt-1 text-sm text-gray-600">
Lista de todas as sessões de captura web processadas
</p>
</div>
<Link
to="/web-proofs/capture"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Nova Captura
</Link>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
URL / Descrição
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Data
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Ações
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{sessions.map((session) => (
<tr key={session.sessionId} className="hover:bg-gray-50">
<td className="px-6 py-4">
<div>
<div className="text-sm font-medium text-gray-900 truncate max-w-xs">
{session.targetUrl}
</div>
{session.description && (
<div className="text-sm text-gray-500 truncate max-w-xs">
{session.description}
</div>
)}
<div className="text-xs text-gray-400 font-mono">
{session.sessionId}
</div>
</div>
</td>
<td className="px-6 py-4">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(session.status)}`}>
{session.status}
</span>
{session.error && (
<div className="text-xs text-red-600 mt-1 max-w-xs truncate" title={session.error}>
{session.error}
</div>
)}
</td>
<td className="px-6 py-4 text-sm text-gray-500">
<div>Criado: {formatDate(session.createdAt)}</div>
{session.processedAt && (
<div>Processado: {formatDate(session.processedAt)}</div>
)}
</td>
<td className="px-6 py-4 text-sm font-medium space-x-2">
<Link
to={`/web-proofs/relatorio/${session.sessionId}`}
className="text-blue-600 hover:text-blue-900"
>
Ver Relatório
</Link>
{session.status === 'CONCLUIDO' && (
<>
<span className="text-gray-300">|</span>
<a
href={`/api/web-proofs/relatorio/${session.sessionId}`}
target="_blank"
rel="noopener noreferrer"
className="text-green-600 hover:text-green-900"
>
Download PDF
</a>
<span className="text-gray-300">|</span>
<a
href={`/api/web-proofs/verify/${session.sessionId}`}
target="_blank"
rel="noopener noreferrer"
className="text-purple-600 hover:text-purple-900"
>
Verificar
</a>
</>
)}
</td>
</tr>
))}
</tbody>
</table>
{sessions.length === 0 && (
<div className="text-center py-12">
<svg className="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h3 className="text-lg font-medium text-gray-900 mb-1">Nenhuma captura encontrada</h3>
<p className="text-gray-500 mb-4">Comece criando sua primeira captura web forense</p>
<Link
to="/web-proofs/capture"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
Criar Primeira Captura
</Link>
</div>
)}
</div>
</div>
</div>
);
}
export default WebProofList;
+348
View File
@@ -0,0 +1,348 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import { toast } from 'react-toastify';
interface WebProofSession {
sessionId: string;
targetUrl: string;
description: string;
status: string;
metadata: any;
environment: any;
sessionHash: string;
contentHash: string;
pageHash: string;
trustedTimestamp: string;
networkData: any;
screenshots: string[];
createdAt: string;
processedAt: string;
}
interface VerificationResult {
verified: boolean;
sessionValid: boolean;
contentValid: boolean;
timestampValid: boolean;
details: any;
}
export function WebProofReport() {
const { sessionId } = useParams<{ sessionId: string }>();
const [session, setSession] = useState<WebProofSession | null>(null);
const [verification, setVerification] = useState<VerificationResult | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [verifying, setVerifying] = useState(false);
useEffect(() => {
const fetchSessionData = async () => {
try {
setLoading(true);
const response = await fetch(`/api/web-proofs/sessions/${sessionId}/status`);
if (!response.ok) {
throw new Error('Sessão não encontrada');
}
const data = await response.json();
setSession(data);
// Verificar automaticamente se a sessão estiver completa
if (data.status === 'CONCLUIDO') {
await verifySession(data.sessionId);
}
} catch (error) {
console.error('Erro ao buscar dados da sessão:', error);
setError('Não foi possível carregar os dados da sessão');
} finally {
setLoading(false);
}
};
if (sessionId) {
fetchSessionData();
}
}, [sessionId]);
const verifySession = async (sessionIdToVerify: string) => {
try {
setVerifying(true);
const response = await fetch(`/api/web-proofs/verify/${sessionIdToVerify}`);
const result = await response.json();
setVerification(result);
if (result.verified) {
toast.success('Sessão verificada com sucesso');
} else {
toast.warning('Verificação da sessão inconclusiva');
}
} catch (error) {
console.error('Erro na verificação:', error);
toast.error('Erro ao verificar sessão');
} finally {
setVerifying(false);
}
};
const formatDate = (dateString: string) => {
try {
return format(new Date(dateString), 'dd/MM/yyyy HH:mm:ss', { locale: ptBR });
} catch {
return 'Data inválida';
}
};
const downloadReport = () => {
if (session?.sessionId) {
window.open(`/api/web-proofs/relatorio/${session.sessionId}`, '_blank');
}
};
if (loading) {
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow p-6">
<div className="animate-pulse">
<div className="h-6 bg-gray-300 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-300 rounded w-1/2 mb-2"></div>
<div className="h-4 bg-gray-300 rounded w-2/3"></div>
</div>
</div>
</div>
</div>
);
}
if (error || !session) {
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="bg-white rounded-lg shadow p-6 text-center">
<svg className="w-16 h-16 text-red-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h2 className="text-2xl font-bold text-red-600 mb-4">Relatório não encontrado</h2>
<p className="text-gray-600 mb-6">{error}</p>
<Link
to="/web-proofs"
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700"
>
Voltar às Capturas Web
</Link>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-6xl mx-auto">
<div className="bg-white rounded-lg shadow overflow-hidden">
{/* Cabeçalho */}
<div className="bg-blue-700 text-white px-6 py-4">
<h1 className="text-2xl font-bold">Relatório de Captura Web Forense</h1>
<p className="text-blue-100 mt-1">Sessão: {session.sessionId}</p>
</div>
{/* Status da Verificação */}
<div className="border-b border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900">Status da Verificação</h2>
{verification ? (
<div className={`mt-2 inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
verification.verified
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{verification.verified ? '✓ Verificado' : '⚠ Verificação Inconclusiva'}
</div>
) : (
<div className="mt-2 inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800">
Aguardando verificação
</div>
)}
</div>
<div className="flex gap-3">
<button
onClick={() => verifySession(session.sessionId)}
disabled={verifying}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400"
>
{verifying ? 'Verificando...' : 'Verificar Novamente'}
</button>
<button
onClick={downloadReport}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
Baixar PDF
</button>
</div>
</div>
</div>
{/* Informações da Sessão */}
<div className="p-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h3 className="text-lg font-semibold mb-4">Informações da Captura</h3>
<div className="space-y-3">
<DetailItem label="URL Capturada" value={session.targetUrl} />
<DetailItem label="Descrição" value={session.description || 'Não especificada'} />
<DetailItem label="Status" value={session.status} />
<DetailItem label="Data de Criação" value={formatDate(session.createdAt)} />
{session.processedAt && (
<DetailItem label="Data de Processamento" value={formatDate(session.processedAt)} />
)}
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">Ambiente de Captura</h3>
<div className="space-y-3">
<DetailItem
label="User Agent"
value={session.environment?.userAgent || 'Não disponível'}
/>
<DetailItem
label="Viewport"
value={session.environment?.viewportSize ?
`${session.environment.viewportSize.width}x${session.environment.viewportSize.height}` :
'Não disponível'
}
/>
<DetailItem
label="Endereço IP"
value={session.environment?.ipAddress || 'Não disponível'}
/>
<DetailItem
label="Timestamp do Ambiente"
value={session.environment?.timestamp ? formatDate(session.environment.timestamp) : 'Não disponível'}
/>
</div>
</div>
</div>
{/* Hashes de Integridade */}
<div className="border-t border-gray-200 p-6">
<h3 className="text-lg font-semibold mb-4">Hashes de Integridade</h3>
<div className="bg-gray-50 rounded-lg p-4 space-y-3 font-mono text-sm">
<DetailItem label="Hash da Sessão (SHA-256)" value={session.sessionHash} />
<DetailItem label="Hash do Conteúdo (SHA-256)" value={session.contentHash} />
<DetailItem label="Hash da Página (SHA-512)" value={session.pageHash} />
</div>
</div>
{/* Timestamp Confiável */}
{session.trustedTimestamp && (
<div className="border-t border-gray-200 p-6">
<h3 className="text-lg font-semibold mb-4">Timestamp Confiável</h3>
<div className="bg-green-50 rounded-lg p-4">
<DetailItem
label="Timestamp Certificado"
value={formatDate(session.trustedTimestamp)}
/>
<p className="text-sm text-green-700 mt-2">
Este timestamp foi emitido por uma autoridade certificadora confiável
</p>
</div>
</div>
)}
{/* Dados de Rede */}
{session.networkData && (
<div className="border-t border-gray-200 p-6">
<h3 className="text-lg font-semibold mb-4">Informações de Rede</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<DetailItem label="Domínio" value={session.networkData.domain} />
<DetailItem label="Protocolo" value={session.networkData.protocol} />
<DetailItem label="Porta" value={session.networkData.port} />
<DetailItem label="IP Resolvido" value={session.networkData.resolvedIP} />
</div>
</div>
)}
{/* Screenshots */}
{session.screenshots && session.screenshots.length > 0 && (
<div className="border-t border-gray-200 p-6">
<h3 className="text-lg font-semibold mb-4">Capturas Realizadas</h3>
<div className="bg-blue-50 rounded-lg p-4">
<p className="text-blue-800">
<span className="font-semibold">{session.screenshots.length}</span> screenshot(s) capturado(s) durante a sessão
</p>
<p className="text-sm text-blue-600 mt-1">
As capturas estão incluídas no relatório PDF completo
</p>
</div>
</div>
)}
{/* Detalhes da Verificação */}
{verification && (
<div className="border-t border-gray-200 p-6">
<h3 className="text-lg font-semibold mb-4">Detalhes da Verificação</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className={`p-4 rounded-lg ${verification.sessionValid ? 'bg-green-50' : 'bg-red-50'}`}>
<div className={`font-medium ${verification.sessionValid ? 'text-green-800' : 'text-red-800'}`}>
Hash da Sessão
</div>
<div className={`text-sm ${verification.sessionValid ? 'text-green-600' : 'text-red-600'}`}>
{verification.sessionValid ? 'Válido' : 'Inválido'}
</div>
</div>
<div className={`p-4 rounded-lg ${verification.contentValid ? 'bg-green-50' : 'bg-red-50'}`}>
<div className={`font-medium ${verification.contentValid ? 'text-green-800' : 'text-red-800'}`}>
Hash do Conteúdo
</div>
<div className={`text-sm ${verification.contentValid ? 'text-green-600' : 'text-red-600'}`}>
{verification.contentValid ? 'Válido' : 'Inválido'}
</div>
</div>
<div className={`p-4 rounded-lg ${verification.timestampValid ? 'bg-green-50' : 'bg-yellow-50'}`}>
<div className={`font-medium ${verification.timestampValid ? 'text-green-800' : 'text-yellow-800'}`}>
Timestamp
</div>
<div className={`text-sm ${verification.timestampValid ? 'text-green-600' : 'text-yellow-600'}`}>
{verification.timestampValid ? 'Válido' : 'Não verificado'}
</div>
</div>
</div>
</div>
)}
{/* Rodapé */}
<div className="bg-gray-50 px-6 py-4 text-center border-t border-gray-200">
<p className="text-sm text-gray-500">
Relatório gerado pelo sistema AdvX Web Proof Capture -
Preservando a integridade e autenticidade de evidências digitais
</p>
</div>
</div>
</div>
</div>
);
}
function DetailItem({ label, value }: { label: string; value: string }) {
return (
<div>
<dt className="text-sm font-medium text-gray-500">{label}</dt>
<dd className="mt-1 text-sm text-gray-900 break-words">{value}</dd>
</div>
);
}
export default WebProofReport;