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" + /> +
+ +
+ +