diff --git a/package.json b/package.json
index d1cd598..ae39d83 100644
--- a/package.json
+++ b/package.json
@@ -12,14 +12,15 @@
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio",
"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",
"dev:full": "concurrently -n \"frontend,backend\" -c \"blue,green\" \"npm run dev\" \"npm run server\"",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"create-admin": "ts-node scripts/create_admin_user.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"
},
"dependencies": {
"@emotion/react": "^11.11.3",
@@ -49,9 +50,18 @@
"multer": "^1.4.5-lts.1",
"opentimestamps-javascript": "^1.0.1",
"pdf-lib": "^1.17.1",
- "pdfkit": "^0.13.0",
- "puppeteer": "^24.5.0",
- "qrcode": "^1.5.3",
+ "html2canvas": "^1.4.1",
+ "puppeteer-core": "^21.11.0",
+ "chrome-aws-lambda": "^10.1.0",
+ "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-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0",
@@ -97,7 +107,10 @@
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.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": {
"seed": "ts-node prisma/seed.ts"
diff --git a/prisma/migrations/create_web_proof_sessions.sql b/prisma/migrations/create_web_proof_sessions.sql
new file mode 100644
index 0000000..622801f
--- /dev/null
+++ b/prisma/migrations/create_web_proof_sessions.sql
@@ -0,0 +1,57 @@
+-- Migration para sistema de captura web de provas
+-- NÃO RESETAR O BANCO DE DADOS - apenas adicionar nova tabela
+
+-- Criar tabela para sessões de captura web
+CREATE TABLE IF NOT EXISTS "web_proof_sessions" (
+ "id" TEXT NOT NULL,
+ "sessionId" TEXT NOT NULL UNIQUE,
+ "targetUrl" TEXT NOT NULL,
+ "description" TEXT,
+ "status" TEXT NOT NULL DEFAULT 'PENDENTE',
+ "metadata" JSONB,
+ "environment" JSONB,
+ "screenshots" TEXT[],
+ "sessionHash" TEXT,
+ "contentHash" TEXT,
+ "pageHash" TEXT,
+ "reportPath" TEXT,
+ "networkData" JSONB,
+ "trustedTimestamp" TEXT,
+ "error" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "processedAt" TIMESTAMP(3),
+
+ CONSTRAINT "web_proof_sessions_pkey" PRIMARY KEY ("id")
+);
+
+-- Criar índices para otimização de consultas
+CREATE INDEX IF NOT EXISTS "web_proof_sessions_sessionId_idx" ON "web_proof_sessions"("sessionId");
+CREATE INDEX IF NOT EXISTS "web_proof_sessions_status_idx" ON "web_proof_sessions"("status");
+CREATE INDEX IF NOT EXISTS "web_proof_sessions_createdAt_idx" ON "web_proof_sessions"("createdAt");
+CREATE INDEX IF NOT EXISTS "web_proof_sessions_contentHash_idx" ON "web_proof_sessions"("contentHash");
+
+-- Adicionar trigger para atualizar updatedAt automaticamente
+CREATE OR REPLACE FUNCTION update_web_proof_sessions_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW."updatedAt" = CURRENT_TIMESTAMP;
+ RETURN NEW;
+END;
+$$ language 'plpgsql';
+
+CREATE TRIGGER update_web_proof_sessions_updated_at
+ BEFORE UPDATE ON "web_proof_sessions"
+ FOR EACH ROW
+ EXECUTE FUNCTION update_web_proof_sessions_updated_at();
+
+-- Adicionar comentários para documentação
+COMMENT ON TABLE "web_proof_sessions" IS 'Tabela para armazenar sessões de captura de provas web forenses';
+COMMENT ON COLUMN "web_proof_sessions"."sessionId" IS 'Identificador único da sessão de captura';
+COMMENT ON COLUMN "web_proof_sessions"."targetUrl" IS 'URL alvo que foi capturada';
+COMMENT ON COLUMN "web_proof_sessions"."metadata" IS 'Metadados técnicos coletados durante a captura';
+COMMENT ON COLUMN "web_proof_sessions"."environment" IS 'Informações do ambiente isolado de captura';
+COMMENT ON COLUMN "web_proof_sessions"."sessionHash" IS 'Hash SHA-256 da sessão para verificação de integridade';
+COMMENT ON COLUMN "web_proof_sessions"."contentHash" IS 'Hash SHA-256 do conteúdo capturado';
+COMMENT ON COLUMN "web_proof_sessions"."pageHash" IS 'Hash SHA-512 da página específica';
+COMMENT ON COLUMN "web_proof_sessions"."trustedTimestamp" IS 'Timestamp de autoridade certificadora confiável';
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 524822d..bc1f475 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -1067,3 +1067,30 @@ model CalculoPensaoAlimenticia {
@@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")
+}
diff --git a/src/App.tsx b/src/App.tsx
index 3a7a7c7..c3f597e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -49,6 +49,9 @@ import { NovoCalculo } from './components/calculos/NovoCalculo';
import { DetalhesCalculo } from './components/calculos/DetalhesCalculo';
import { NovoPensaoAlimenticia } from './components/calculos/NovoPensaoAlimenticia';
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();
@@ -126,6 +129,11 @@ const router = createBrowserRouter(
} />
+
+ {/* Rotas de Captura Web */}
+ } />
+ } />
+ } />
} />
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
new file mode 100644
index 0000000..fd3e432
--- /dev/null
+++ b/src/components/layout/Sidebar.tsx
@@ -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 (
+
+
+
Painel de Controle
+
+
+
+
+ );
+};
+
+export default Sidebar;
\ No newline at end of file
diff --git a/src/components/provas/WebProofCapture.tsx b/src/components/provas/WebProofCapture.tsx
new file mode 100644
index 0000000..a85fe27
--- /dev/null
+++ b/src/components/provas/WebProofCapture.tsx
@@ -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(null);
+ const [isRecording, setIsRecording] = useState(false);
+ const [targetUrl, setTargetUrl] = useState('');
+ const [captureDescription, setCaptureDescription] = useState('');
+ const [metadata, setMetadata] = useState(null);
+ const [error, setError] = useState(null);
+ const [progress, setProgress] = useState(0);
+ const [screenshots, setScreenshots] = useState([]);
+
+ const iframeRef = useRef(null);
+ const recordedChunksRef = useRef([]);
+ 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 => {
+ 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 => {
+ 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 => {
+ try {
+ const response = await fetch('/api/web-proofs/trusted-timestamp');
+ const data = await response.json();
+ return data.timestamp;
+ } catch {
+ return new Date().toISOString();
+ }
+ };
+
+ return (
+
+
+
+
Captura de Provas Web
+
+ Coleta forense de evidências digitais em ambiente isolado
+
+
+
+ {!session && (
+
+
Inicializar Ambiente Seguro
+
+ O sistema criará um ambiente virtual isolado para garantir a integridade da captura.
+
+
+
+
+ {progress > 0 && (
+
+
+
Preparando ambiente... {progress}%
+
+ )}
+
+ )}
+
+ {session && session.status === SESSION_STATES.ACTIVE && (
+
+
+
Configuração da Captura
+
+
+
+
+ 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"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
Ambiente Virtual Isolado
+
+
+ {isRecording && targetUrl ? (
+
+ ) : (
+
Ambiente aguardando URL de destino
+ )}
+
+
+
+
+
+
+
+
+ {screenshots.length > 0 && (
+
+
+ Screenshots capturados: {screenshots.length}
+
+
+ )}
+
+
+ )}
+
+ {session && session.status === SESSION_STATES.RECORDING && (
+
+ )}
+
+ {error && (
+
+ )}
+
+
+ );
+}
+
+export default WebProofCapture;
diff --git a/src/components/provas/WebProofList.tsx b/src/components/provas/WebProofList.tsx
new file mode 100644
index 0000000..2bbca0c
--- /dev/null
+++ b/src/components/provas/WebProofList.tsx
@@ -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([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(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 (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
{error}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
Capturas Web Forenses
+
+ Lista de todas as sessões de captura web processadas
+
+
+
+ Nova Captura
+
+
+
+
+
+
+
+
+ |
+ URL / Descrição
+ |
+
+ Status
+ |
+
+ Data
+ |
+
+ Ações
+ |
+
+
+
+ {sessions.map((session) => (
+
+
+
+
+ {session.targetUrl}
+
+ {session.description && (
+
+ {session.description}
+
+ )}
+
+ {session.sessionId}
+
+
+ |
+
+
+ {session.status}
+
+ {session.error && (
+
+ {session.error}
+
+ )}
+ |
+
+ Criado: {formatDate(session.createdAt)}
+ {session.processedAt && (
+ Processado: {formatDate(session.processedAt)}
+ )}
+ |
+
+
+ Ver Relatório
+
+
+ {session.status === 'CONCLUIDO' && (
+ <>
+ |
+
+ Download PDF
+
+ |
+
+ Verificar
+
+ >
+ )}
+ |
+
+ ))}
+
+
+
+ {sessions.length === 0 && (
+
+
+
Nenhuma captura encontrada
+
Comece criando sua primeira captura web forense
+
+ Criar Primeira Captura
+
+
+ )}
+
+
+
+ );
+}
+
+export default WebProofList;
diff --git a/src/components/provas/WebProofReport.tsx b/src/components/provas/WebProofReport.tsx
new file mode 100644
index 0000000..9734356
--- /dev/null
+++ b/src/components/provas/WebProofReport.tsx
@@ -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(null);
+ const [verification, setVerification] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(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 (
+
+ );
+ }
+
+ if (error || !session) {
+ return (
+
+
+
+
+
Relatório não encontrado
+
{error}
+
+ Voltar às Capturas Web
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {/* Cabeçalho */}
+
+
Relatório de Captura Web Forense
+
Sessão: {session.sessionId}
+
+
+ {/* Status da Verificação */}
+
+
+
+
Status da Verificação
+ {verification ? (
+
+ {verification.verified ? '✓ Verificado' : '⚠ Verificação Inconclusiva'}
+
+ ) : (
+
+ Aguardando verificação
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {/* Informações da Sessão */}
+
+
+
Informações da Captura
+
+
+
+
+
+ {session.processedAt && (
+
+ )}
+
+
+
+
+
Ambiente de Captura
+
+
+
+
+
+
+
+
+
+ {/* Hashes de Integridade */}
+
+
Hashes de Integridade
+
+
+
+
+
+
+
+ {/* Timestamp Confiável */}
+ {session.trustedTimestamp && (
+
+
Timestamp Confiável
+
+
+
+ Este timestamp foi emitido por uma autoridade certificadora confiável
+
+
+
+ )}
+
+ {/* Dados de Rede */}
+ {session.networkData && (
+
+
Informações de Rede
+
+
+
+
+
+
+
+ )}
+
+ {/* Screenshots */}
+ {session.screenshots && session.screenshots.length > 0 && (
+
+
Capturas Realizadas
+
+
+ {session.screenshots.length} screenshot(s) capturado(s) durante a sessão
+
+
+ As capturas estão incluídas no relatório PDF completo
+
+
+
+ )}
+
+ {/* Detalhes da Verificação */}
+ {verification && (
+
+
Detalhes da Verificação
+
+
+
+ Hash da Sessão
+
+
+ {verification.sessionValid ? 'Válido' : 'Inválido'}
+
+
+
+
+
+ Hash do Conteúdo
+
+
+ {verification.contentValid ? 'Válido' : 'Inválido'}
+
+
+
+
+
+ Timestamp
+
+
+ {verification.timestampValid ? 'Válido' : 'Não verificado'}
+
+
+
+
+ )}
+
+ {/* Rodapé */}
+
+
+ Relatório gerado pelo sistema AdvX Web Proof Capture -
+ Preservando a integridade e autenticidade de evidências digitais
+
+
+
+
+
+ );
+}
+
+function DetailItem({ label, value }: { label: string; value: string }) {
+ return (
+
+
{label}
+ {value}
+
+ );
+}
+
+export default WebProofReport;
diff --git a/src/server/app.ts b/src/server/app.ts
new file mode 100644
index 0000000..1393761
--- /dev/null
+++ b/src/server/app.ts
@@ -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;
\ No newline at end of file
diff --git a/src/server/routes/webProofs.ts b/src/server/routes/webProofs.ts
new file mode 100644
index 0000000..09825e3
--- /dev/null
+++ b/src/server/routes/webProofs.ts
@@ -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;
diff --git a/src/services/WebProofService.ts b/src/services/WebProofService.ts
new file mode 100644
index 0000000..79bbd80
--- /dev/null
+++ b/src/services/WebProofService.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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 {
+ // 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 {
+ 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 {
+ 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 {
+ 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 {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+}