Compare commits

2 Commits
Author SHA1 Message Date
rodrigo fe6f65447b add new proofsystem 2025-07-27 19:35:53 -03:00
rodrigo 2399cb1f2d add new proofsystem 2025-07-27 19:20:10 -03:00
10 changed files with 1956 additions and 9 deletions

No files matched your search

+23 -9
View File
@@ -12,14 +12,20 @@
"prisma:migrate": "prisma migrate dev", "prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio", "prisma:studio": "prisma studio",
"prisma:seed": "ts-node -T prisma/seed.ts", "prisma:seed": "ts-node -T prisma/seed.ts",
"postinstall": "prisma generate && npx puppeteer browsers install chrome", "postinstall": "prisma generate && npx playwright install chromium",
"server": "ts-node --transpile-only src/server/index.ts", "server": "ts-node --transpile-only src/server/index.ts",
"dev:full": "concurrently -n \"frontend,backend\" -c \"blue,green\" \"npm run dev\" \"npm run server\"", "dev:full": "concurrently -n \"frontend,backend\" -c \"blue,green\" \"npm run dev\" \"npm run server\"",
"lint": "eslint . --ext .ts,.tsx", "lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix", "lint:fix": "eslint . --ext .ts,.tsx --fix",
"create-admin": "ts-node scripts/create_admin_user.ts", "create-admin": "ts-node scripts/create_admin_user.ts",
"worker": "ts-node src/workers/startWorker.ts", "worker": "ts-node src/workers/startWorker.ts",
"install-deps": "npm install ua-parser-js uuid axios xlsx" "install-deps": "npm install ua-parser-js uuid axios xlsx html2canvas puppeteer-core chrome-aws-lambda jspdf canvas ws socket.io socket.io-client",
"install-web-proof-deps": "npm install html2canvas puppeteer-core chrome-aws-lambda jspdf canvas node-html-to-image playwright ws socket.io socket.io-client",
"prisma:resolve-migration": "npx prisma migrate resolve --applied",
"prisma:sync-schema": "npx prisma db push && npx prisma generate",
"prisma:reset-migrations": "npx prisma migrate reset --force",
"prisma:fix-manual-migration": "npx prisma migrate status && npx prisma db push && npx prisma generate",
"prisma:mark-applied": "npx prisma migrate resolve --applied"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.3", "@emotion/react": "^11.11.3",
@@ -49,9 +55,17 @@
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"opentimestamps-javascript": "^1.0.1", "opentimestamps-javascript": "^1.0.1",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfkit": "^0.13.0", "html2canvas": "^1.4.1",
"puppeteer": "^24.5.0", "puppeteer-core": "^24.15.0",
"qrcode": "^1.5.3", "jspdf": "^2.5.1",
"canvas": "^3.1.2",
"screenshot-desktop": "^1.12.7",
"node-html-to-image": "^4.0.0",
"playwright": "^1.40.0",
"ws": "^8.14.2",
"socket.io": "^4.7.4",
"socket.io-client": "^4.7.4",
"@types/ws": "^8.5.10",
"react": "^18.2.0", "react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -87,9 +101,6 @@
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.17",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"eslint": "^9.23.0", "eslint": "^9.23.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-prettier": "^5.2.5",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"postcss": "^8.4.33", "postcss": "^8.4.33",
"prettier": "^3.5.3", "prettier": "^3.5.3",
@@ -97,7 +108,10 @@
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vite": "^6.2.4" "vite": "^6.2.4",
"@types/html2canvas": "^1.0.0",
"@types/canvas": "^2.11.6",
"@types/node-html-to-image": "^4.0.0"
}, },
"prisma": { "prisma": {
"seed": "ts-node prisma/seed.ts" "seed": "ts-node prisma/seed.ts"
+27
View File
@@ -1067,3 +1067,30 @@ model CalculoPensaoAlimenticia {
@@map("calculos_pensao_alimenticia") @@map("calculos_pensao_alimenticia")
} }
model WebProofSession {
id String @id @default(cuid())
sessionId String @unique
targetUrl String
description String?
status String @default("PENDENTE")
metadata Json?
environment Json?
screenshots String[]
sessionHash String?
contentHash String?
pageHash String?
reportPath String?
networkData Json?
trustedTimestamp String?
error String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
processedAt DateTime?
@@index([sessionId])
@@index([status])
@@index([createdAt])
@@index([contentHash])
@@map("web_proof_sessions")
}
+8
View File
@@ -49,6 +49,9 @@ import { NovoCalculo } from './components/calculos/NovoCalculo';
import { DetalhesCalculo } from './components/calculos/DetalhesCalculo'; import { DetalhesCalculo } from './components/calculos/DetalhesCalculo';
import { NovoPensaoAlimenticia } from './components/calculos/NovoPensaoAlimenticia'; import { NovoPensaoAlimenticia } from './components/calculos/NovoPensaoAlimenticia';
import { DetalhePensaoAlimenticia } from './components/calculos/DetalhePensaoAlimenticia'; import { DetalhePensaoAlimenticia } from './components/calculos/DetalhePensaoAlimenticia';
import WebProofCapture from './components/provas/WebProofCapture';
import WebProofReport from './components/provas/WebProofReport';
import WebProofList from './components/provas/WebProofList';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -126,6 +129,11 @@ const router = createBrowserRouter(
<DetalhePensaoAlimenticia /> <DetalhePensaoAlimenticia />
</ProtectedRoute> </ProtectedRoute>
} /> } />
{/* Rotas de Captura Web */}
<Route path="/web-proofs" element={<WebProofList />} />
<Route path="/web-proofs/capture" element={<WebProofCapture />} />
<Route path="/web-proofs/relatorio/:sessionId" element={<WebProofReport />} />
</Route> </Route>
</Route> </Route>
<Route path="*" element={<Navigate to="/login" />} /> <Route path="*" element={<Navigate to="/login" />} />
+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;
+41
View File
@@ -0,0 +1,41 @@
import express from 'express';
import cors from 'cors';
import { json, urlencoded } from 'body-parser';
import { createServer } from 'http';
import { Server } from 'socket.io';
import path from 'path';
import { fileURLToPath } from 'url';
import provasRoutes from './routes/provas';
import webProofsRoutes from './routes/webProofs';
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer);
// Middleware
app.use(cors());
app.use(json());
app.use(urlencoded({ extended: true }));
// Definir diretório público para arquivos estáticos
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
app.use(express.static(path.join(__dirname, 'public')));
// Rotas
app.use('/api/provas', provasRoutes);
app.use('/api/web-proofs', webProofsRoutes);
// Rota padrão
app.get('/', (req, res) => {
res.send('API do sistema de validação de provas digitais');
});
// Inicializar servidor
const PORT = process.env.PORT || 5000;
httpServer.listen(PORT, () => {
console.log(`Servidor rodando na porta ${PORT}`);
});
export default app;
+296
View File
@@ -0,0 +1,296 @@
import express from 'express';
import { WebProofService } from '../services/WebProofService';
import { prisma } from '../prismaClient';
import * as path from 'path';
import * as fs from 'fs';
const router = express.Router();
const webProofService = new WebProofService();
// Criar nova sessão de captura web
router.post('/sessions', async (req, res) => {
try {
const {
sessionId,
targetUrl,
description,
metadata,
environment,
screenshots
} = req.body;
// Validar dados de entrada
if (!sessionId || !targetUrl || !metadata || !environment) {
return res.status(400).json({
error: 'Dados obrigatórios não fornecidos',
required: ['sessionId', 'targetUrl', 'metadata', 'environment']
});
}
// Validar URL
try {
new URL(targetUrl);
} catch {
return res.status(400).json({ error: 'URL inválida fornecida' });
}
// Coletar informações de rede do cliente
const clientInfo = {
ip: req.headers['x-forwarded-for']?.toString().split(',')[0].trim() ||
req.ip ||
req.socket.remoteAddress ||
'127.0.0.1',
port: req.socket.remotePort || 0,
userAgent: req.headers['user-agent'] || '',
timestamp: new Date().toISOString()
};
// Processar sessão de captura
const result = await webProofService.processWebCapture({
sessionId,
targetUrl,
description,
metadata: {
...metadata,
clientInfo,
serverInfo: {
hostname: require('os').hostname(),
nodeVersion: process.version,
timestamp: new Date().toISOString()
}
},
environment,
screenshots: screenshots || []
});
res.json({
success: true,
sessionId: result.id,
status: result.status,
message: 'Sessão de captura processada com sucesso'
});
} catch (error) {
console.error('Erro ao processar sessão de captura:', error);
res.status(500).json({
error: 'Falha ao processar sessão de captura',
details: error.message
});
}
});
// Obter status da sessão
router.get('/sessions/:sessionId/status', async (req, res) => {
try {
const { sessionId } = req.params;
const session = await prisma.webProofSession.findUnique({
where: { sessionId }
});
if (!session) {
return res.status(404).json({ error: 'Sessão não encontrada' });
}
res.json({
sessionId: session.sessionId,
status: session.status,
targetUrl: session.targetUrl,
description: session.description,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
reportPath: session.reportPath,
error: session.error
});
} catch (error) {
console.error('Erro ao buscar status da sessão:', error);
res.status(500).json({ error: 'Erro ao buscar status da sessão' });
}
});
// Obter timestamp confiável
router.get('/trusted-timestamp', async (req, res) => {
try {
// Simular obtenção de timestamp de autoridade certificadora
const trustedTimestamp = await webProofService.getTrustedTimestamp();
res.json({
timestamp: trustedTimestamp.timestamp,
authority: trustedTimestamp.authority,
signature: trustedTimestamp.signature
});
} catch (error) {
console.error('Erro ao obter timestamp confiável:', error);
res.status(500).json({
error: 'Erro ao obter timestamp confiável',
fallback: new Date().toISOString()
});
}
});
// Obter informações do cliente
router.get('/client-info', async (req, res) => {
try {
const clientInfo = {
ip: req.headers['x-forwarded-for']?.toString().split(',')[0].trim() ||
req.ip ||
req.socket.remoteAddress ||
'127.0.0.1',
port: req.socket.remotePort || 0,
userAgent: req.headers['user-agent'] || '',
timestamp: new Date().toISOString(),
headers: {
'x-forwarded-for': req.headers['x-forwarded-for'],
'user-agent': req.headers['user-agent'],
'accept-language': req.headers['accept-language']
}
};
res.json(clientInfo);
} catch (error) {
console.error('Erro ao obter informações do cliente:', error);
res.status(500).json({
error: 'Erro ao obter informações do cliente',
fallback: {
ip: '127.0.0.1',
port: 0,
timestamp: new Date().toISOString()
}
});
}
});
// Verificar autenticidade de captura web com detalhes aprimorados
router.get('/verify/:sessionId', async (req, res) => {
try {
const { sessionId } = req.params;
if (!sessionId || typeof sessionId !== 'string') {
return res.status(400).json({
verified: false,
error: 'SessionId inválido'
});
}
const verification = await webProofService.verifyWebCapture(sessionId);
res.json({
verified: verification.verified,
sessionId,
sessionValid: verification.sessionValid || false,
contentValid: verification.contentValid || false,
timestampValid: verification.timestampValid || false,
verificationDetails: verification.details,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Erro ao verificar captura:', error);
res.status(500).json({
verified: false,
sessionValid: false,
contentValid: false,
timestampValid: false,
error: 'Erro ao verificar captura web'
});
}
});
// Baixar relatório com validação aprimorada
router.get('/relatorio/:sessionId', async (req, res) => {
try {
const { sessionId } = req.params;
if (!sessionId || typeof sessionId !== 'string') {
return res.status(400).json({ error: 'SessionId inválido' });
}
const session = await prisma.webProofSession.findUnique({
where: { sessionId }
});
if (!session) {
return res.status(404).json({ error: 'Sessão não encontrada' });
}
if (session.status !== 'CONCLUIDO') {
return res.status(400).json({
error: 'Relatório ainda não está disponível',
status: session.status
});
}
const reportPath = path.join(process.cwd(), 'storage', 'web-proofs', `${sessionId}_report.pdf`);
if (!fs.existsSync(reportPath)) {
console.error('Arquivo de relatório não encontrado:', reportPath);
return res.status(404).json({ error: 'Arquivo do relatório não encontrado' });
}
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="captura_web_${sessionId}_relatorio.pdf"`);
res.setHeader('Cache-Control', 'public, max-age=3600'); // Cache de 1 hora
const fileStream = fs.createReadStream(reportPath);
fileStream.on('error', (error) => {
console.error('Erro ao ler arquivo de relatório:', error);
if (!res.headersSent) {
res.status(500).json({ error: 'Erro ao ler arquivo de relatório' });
}
});
fileStream.pipe(res);
} catch (error) {
console.error('Erro ao servir relatório:', error);
if (!res.headersSent) {
res.status(500).json({ error: 'Erro ao gerar relatório' });
}
}
});
// Listar sessões com paginação
router.get('/admin/sessions', async (req, res) => {
try {
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 100;
const offset = (page - 1) * limit;
const [sessions, total] = await Promise.all([
prisma.webProofSession.findMany({
orderBy: { createdAt: 'desc' },
skip: offset,
take: limit,
select: {
sessionId: true,
targetUrl: true,
description: true,
status: true,
createdAt: true,
processedAt: true,
error: true
}
}),
prisma.webProofSession.count()
]);
res.json({
sessions,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
});
} catch (error) {
console.error('Erro ao listar sessões:', error);
res.status(500).json({ error: 'Erro ao listar sessões' });
}
});
export default router;
+381
View File
@@ -0,0 +1,381 @@
import { prisma } from '../prismaClient';
import { createHash } from 'crypto';
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import * as fs from 'fs/promises';
import * as path from 'path';
interface WebCaptureData {
sessionId: string;
targetUrl: string;
description: string;
metadata: any;
environment: any;
screenshots: string[];
}
interface TrustedTimestamp {
timestamp: string;
authority: string;
signature: string;
}
export class WebProofService {
// Processar captura web completa
async processWebCapture(data: WebCaptureData) {
try {
console.log('Iniciando processamento de captura web:', data.sessionId);
// Criar registro da sessão
const session = await prisma.webProofSession.create({
data: {
sessionId: data.sessionId,
targetUrl: data.targetUrl,
description: data.description,
status: 'PROCESSANDO',
metadata: data.metadata,
environment: data.environment,
screenshots: data.screenshots
}
});
// Processar em background
this.processInBackground(session.id, data);
return session;
} catch (error) {
console.error('Erro ao processar captura web:', error);
throw new Error(`Falha no processamento: ${error.message}`);
}
}
// Processamento em background
private async processInBackground(sessionDbId: string, data: WebCaptureData) {
try {
// Simular processamento complexo
await this.delay(2000);
// Gerar hashes da sessão
const sessionHash = await this.generateSessionHash(data);
const contentHash = await this.generateContentHash(data.targetUrl, data.metadata);
const pageHash = await this.generatePageHash(data.targetUrl, data.environment);
// Coletar dados de rede simulados
const networkData = await this.collectNetworkData(data.targetUrl);
// Obter timestamp confiável
const trustedTimestamp = await this.getTrustedTimestamp();
// Gerar relatório PDF
const reportPath = await this.generateWebProofReport({
...data,
sessionHash,
contentHash,
pageHash,
networkData,
trustedTimestamp
});
// Atualizar status para concluído
await prisma.webProofSession.update({
where: { id: sessionDbId },
data: {
status: 'CONCLUIDO',
sessionHash,
contentHash,
pageHash,
reportPath,
networkData,
trustedTimestamp: trustedTimestamp.timestamp,
processedAt: new Date()
}
});
console.log('Processamento de captura web concluído:', data.sessionId);
} catch (error) {
console.error('Erro no processamento em background:', error);
await prisma.webProofSession.update({
where: { id: sessionDbId },
data: {
status: 'ERRO',
error: error.message
}
});
}
}
// Gerar hash da sessão
private async generateSessionHash(data: WebCaptureData): Promise<string> {
const sessionData = JSON.stringify({
sessionId: data.sessionId,
targetUrl: data.targetUrl,
environment: data.environment,
timestamp: data.metadata.timestamps?.sessionStart
});
return createHash('sha256').update(sessionData).digest('hex');
}
// Gerar hash do conteúdo
private async generateContentHash(url: string, metadata: any): Promise<string> {
const contentData = JSON.stringify({
url,
timestamp: metadata.timestamps?.captureStart,
browserFingerprint: metadata.technicalData?.browserFingerprint
});
return createHash('sha256').update(contentData).digest('hex');
}
// Gerar hash da página
private async generatePageHash(url: string, environment: any): Promise<string> {
const pageData = JSON.stringify({
url,
environment,
timestamp: new Date().toISOString()
});
return createHash('sha512').update(pageData).digest('hex');
}
// Coletar dados de rede (simulado)
private async collectNetworkData(targetUrl: string) {
try {
const url = new URL(targetUrl);
// Simular coleta de dados de rede
return {
domain: url.hostname,
protocol: url.protocol,
port: url.port || (url.protocol === 'https:' ? '443' : '80'),
path: url.pathname,
resolvedIP: '192.168.1.1', // Simulado
routeTrace: ['10.0.0.1', '192.168.1.1'], // Simulado
dnsRecords: [
{ type: 'A', value: '192.168.1.1' },
{ type: 'MX', value: 'mail.example.com' }
]
};
} catch {
return {
domain: 'unknown',
error: 'Falha na resolução de rede'
};
}
}
// Obter timestamp confiável
async getTrustedTimestamp(): Promise<TrustedTimestamp> {
// Simular autoridade certificadora de tempo
const timestamp = new Date().toISOString();
const signature = createHash('sha256')
.update(timestamp + 'trusted_authority_key')
.digest('hex');
return {
timestamp,
authority: 'AdvX Timestamp Authority',
signature
};
}
// Gerar relatório PDF da captura web
private async generateWebProofReport(data: any): Promise<string> {
try {
const pdfDoc = await PDFDocument.create();
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
// Página principal
const page = pdfDoc.addPage([595, 842]); // A4
const { width, height } = page.getSize();
// Cabeçalho
page.drawText('RELATÓRIO DE CAPTURA WEB FORENSE', {
x: 50,
y: height - 50,
size: 18,
font: helveticaBold,
color: rgb(0, 0, 0.8)
});
// Informações da sessão
let yPosition = height - 100;
const addSection = (title: string, content: string) => {
page.drawText(title, {
x: 50,
y: yPosition,
size: 12,
font: helveticaBold
});
yPosition -= 20;
const lines = content.split('\n');
lines.forEach(line => {
page.drawText(line, {
x: 50,
y: yPosition,
size: 10,
font: helveticaFont
});
yPosition -= 15;
});
yPosition -= 10;
};
addSection('IDENTIFICAÇÃO DA SESSÃO:',
`ID: ${data.sessionId}\nURL Capturada: ${data.targetUrl}\nDescrição: ${data.description || 'Não especificada'}`
);
addSection('INFORMAÇÕES TEMPORAIS:',
`Início da Sessão: ${data.metadata.timestamps?.sessionStart || 'N/A'}\nInício da Captura: ${data.metadata.timestamps?.captureStart || 'N/A'}\nFim da Captura: ${data.metadata.timestamps?.captureEnd || 'N/A'}\nTimestamp Confiável: ${data.trustedTimestamp || 'N/A'}`
);
addSection('HASHES DE INTEGRIDADE:',
`Hash da Sessão (SHA-256): ${data.sessionHash}\nHash do Conteúdo (SHA-256): ${data.contentHash}\nHash da Página (SHA-512): ${data.pageHash}`
);
addSection('AMBIENTE DE CAPTURA:',
`User Agent: ${data.environment.userAgent}\nTamanho da Viewport: ${data.environment.viewportSize?.width}x${data.environment.viewportSize?.height}\nEndereço IP: ${data.environment.ipAddress}\nTimestamp do Ambiente: ${data.environment.timestamp}`
);
if (data.networkData) {
addSection('DADOS DE REDE:',
`Domínio: ${data.networkData.domain}\nProtocolo: ${data.networkData.protocol}\nPorta: ${data.networkData.port}\nIP Resolvido: ${data.networkData.resolvedIP || 'N/A'}`
);
}
if (data.screenshots && data.screenshots.length > 0) {
addSection('CAPTURAS REALIZADAS:',
`Número de Screenshots: ${data.screenshots.length}\nTimestamps: ${data.screenshots.map((_, i) => `Screenshot ${i + 1}`).join(', ')}`
);
}
// Rodapé
page.drawText('Este relatório foi gerado automaticamente pelo sistema AdvX Web Proof Capture', {
x: 50,
y: 50,
size: 8,
font: helveticaFont,
color: rgb(0.5, 0.5, 0.5)
});
// Salvar PDF
const pdfBytes = await pdfDoc.save();
const reportDir = path.join(process.cwd(), 'storage', 'web-proofs');
await fs.mkdir(reportDir, { recursive: true });
const reportPath = path.join(reportDir, `${data.sessionId}_report.pdf`);
await fs.writeFile(reportPath, pdfBytes);
return reportPath;
} catch (error) {
console.error('Erro ao gerar relatório PDF:', error);
throw new Error('Falha na geração do relatório');
}
}
// Verificar autenticidade da captura
async verifyWebCapture(sessionId: string) {
try {
const session = await prisma.webProofSession.findUnique({
where: { sessionId }
});
if (!session) {
return {
verified: false,
details: 'Sessão não encontrada'
};
}
// Verificar integridade dos hashes
const sessionHashValid = await this.verifySessionHash(session);
const contentHashValid = await this.verifyContentHash(session);
const timestampValid = this.verifyTimestamp(session.trustedTimestamp);
return {
verified: sessionHashValid && contentHashValid && timestampValid,
sessionValid: sessionHashValid,
contentValid: contentHashValid,
timestampValid: timestampValid,
details: {
sessionId: session.sessionId,
targetUrl: session.targetUrl,
status: session.status,
sessionHashValid,
contentHashValid,
timestampValid,
createdAt: session.createdAt,
processedAt: session.processedAt
}
};
} catch (error) {
console.error('Erro na verificação:', error);
return {
verified: false,
sessionValid: false,
contentValid: false,
timestampValid: false,
details: 'Erro durante a verificação'
};
}
}
// Verificar hash da sessão
private async verifySessionHash(session: any): Promise<boolean> {
try {
const expectedHash = await this.generateSessionHash({
sessionId: session.sessionId,
targetUrl: session.targetUrl,
metadata: session.metadata,
environment: session.environment,
screenshots: session.screenshots
});
return expectedHash === session.sessionHash;
} catch {
return false;
}
}
// Verificar hash do conteúdo
private async verifyContentHash(session: any): Promise<boolean> {
try {
const expectedHash = await this.generateContentHash(
session.targetUrl,
session.metadata
);
return expectedHash === session.contentHash;
} catch {
return false;
}
}
// Verificar timestamp confiável
private verifyTimestamp(trustedTimestamp: string | null): boolean {
if (!trustedTimestamp) return false;
try {
const timestamp = new Date(trustedTimestamp);
const now = new Date();
const diffInHours = Math.abs(now.getTime() - timestamp.getTime()) / (1000 * 60 * 60);
// Considerar válido se o timestamp for de até 24 horas atrás
return diffInHours <= 24;
} catch {
return false;
}
}
// Função auxiliar para delay
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}