diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml new file mode 100644 index 0000000..ea7c5de --- /dev/null +++ b/.github/workflows/build-windows-installer.yml @@ -0,0 +1,42 @@ +name: Build Windows Installer + +on: + push: + branches: [ feat/ecac-bot-installer ] + workflow_dispatch: + +jobs: + build-installer: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Inno Setup via Chocolatey + run: | + choco install innosetup -y + + - name: Show Inno path + run: | + dir "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" || dir "C:\Program Files\Inno Setup 6\ISCC.exe" + + - name: Compile Inno Setup script + working-directory: ${{ github.workspace }} + run: | + $iscc = 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' + if (-not (Test-Path $iscc)) { $iscc = 'C:\Program Files\Inno Setup 6\ISCC.exe' } + if (-not (Test-Path $iscc)) { Write-Error 'ISCC not found' ; exit 2 } + & $iscc "$env:GITHUB_WORKSPACE\ecac-bot\installers\installer.iss" + + - name: Package output + run: | + $out = Get-ChildItem -Path "$env:GITHUB_WORKSPACE" -Recurse -Filter "ecac-bot-installer-setup.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $out) { Write-Error 'Installer exe not found' ; exit 2 } + Write-Host "Found installer: $($out.FullName)" + Copy-Item $out.FullName -Destination "$env:GITHUB_WORKSPACE\ecac-bot\ecac-windows-installer.exe" -Force + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ecac-windows-installer + path: ecac-bot/ecac-windows-installer.exe diff --git a/ecac-bot/.env.example b/ecac-bot/.env.example new file mode 100644 index 0000000..6e7caf3 --- /dev/null +++ b/ecac-bot/.env.example @@ -0,0 +1,43 @@ +# ============================================ +# Configuração do Bot RPA PER/DCOMP +# ============================================ +# Copie este arquivo para .env e preencha os valores + +# ===== AUTENTICAÇÃO DO BOT ===== +# Token gerado em: Sistema > Administração > Tokens RPA +BOT_TOKEN=462c87ad1bfe914e6d935d2ae4d3e786eb2dd9ff8fb2d8baaeccbfca7f59cad0 + +# ===== CONEXÃO COM O SISTEMA ===== +# URL da API do sistema (não altere) +API_BASE_URL=https://ekddbjpakojzzfzapssv.supabase.co/functions/v1/rpa-api + +# ===== CONFIGURAÇÕES DO BOT ===== +# Modo sem interface (true = invisível, false = mostra navegador) +HEADLESS_MODE=false + +# Intervalo entre verificações de novas consultas (segundos) +POLLING_INTERVAL=60 + +# Timeout para operações no e-CAC (segundos) +ECAC_TIMEOUT=120 + +# Número máximo de consultas simultâneas +MAX_WORKERS=2 + +# ===== LOGS E DEBUG ===== +LOG_LEVEL=INFO +LOG_FILE=./logs/bot.log +SCREENSHOTS_DIR=./screenshots + +# Mantém o navegador aberto ao final (depuração) +DETACH_BROWSER=false + +# Delay artificial entre ações (ms). Ex: 250 deixa mais 'humano' +SLOWMO_MS=0 + +# Concurrency de navegadores do e-CAC (recomendado 1) +ECAC_CONCURRENCY=1 + +# ===== OPCIONAL: PERFIL DO NAVEGADOR ===== +# Se quiser rodar com um perfil persistente, descomente e ajuste o caminho: +# BROWSER_USER_DATA_DIR=C:\Users\SeuUsuário\AppData\Local\Google\Chrome\User Data\PlaywrightProfile diff --git a/ecac-bot/.gitignore b/ecac-bot/.gitignore new file mode 100644 index 0000000..aa81eaa --- /dev/null +++ b/ecac-bot/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +*.env +/.cache +/.output diff --git a/ecac-bot/README.md b/ecac-bot/README.md new file mode 100644 index 0000000..d6bdb91 --- /dev/null +++ b/ecac-bot/README.md @@ -0,0 +1,92 @@ +# ecac-bot + +Pequeno scaffold para automação do e-CAC — apenas um ponto de partida. **Atenção:** use apenas em contas que você tem autorização para acessar. + +## 🚀 Instalador Windows (Recomendado para Usuários) + +Para usuários finais no **Windows 10/11**, recomenda-se usar o instalador executável que inclui tudo pré-configurado: + +1. Baixe [**ecac-windows-installer.exe**](https://github.com/confisped-hub/openclaw-cloud/releases/download/v0.2.0/ecac-windows-installer.exe) da seção [Releases](https://github.com/confisped-hub/openclaw-cloud/releases) +2. Execute o arquivo `.exe` e siga as instruções + - Ele instalará automaticamente Node.js, Python, e todas as dependências + - Configurará o diretório de instalação em `C:\Program Files\ECAC-Bot` +3. Após a instalação: + - Navegue até `C:\Program Files\ECAC-Bot\ecac-bot` + - Copie `.env.example` para `.env` + - Preencha `ECAC_USERNAME`, `ECAC_PASSWORD` e `API_BASE_URL` (se usar modo worker) + - Execute `npm start` ou use o worker conforme descrito abaixo + +## Manual de Instalação (Desenvolvedores / Linux/macOS) + +Passos rápidos: + +1. Copie `.env.example` para `.env` e preencha `ECAC_USERNAME` e `ECAC_PASSWORD`. +2. Instale dependências: + +```bash +cd ecac-bot +npm install +npm run install-playwright +``` + +3. Rodar (recomendado com headful para tratar MFA manualmente): + +```bash +# headful (padrão no .env.example) +npm start + +# ou em headless +HEADLESS=true npm start +``` + +Notas importantes: +- Os seletores de login e de alteração de perfil em `src/bot.js` são exemplos. O e-CAC frequentemente muda a interface — ajuste os seletores conforme necessário. +- Não armazene credenciais em repositórios. Use variáveis de ambiente seguras. +- Se houver autenticação multifator (MFA), rode em modo não-headless, autentique manualmente no navegador quando solicitado e pressione Enter no terminal para continuar. + +- O bot gera artefatos de depuração na raiz do projeto quando executado: `ecac-login-after-submit.png`, `ecac-login-after-submit.html`, `ecac-after-auth.png`, `ecac-after-profile-click.png`, `ecac-after-confirm.png`. Use estes arquivos para inspecionar elementos e ajustar seletores. +- Para inspecionar a UI e gerar seletores, considere usar o Playwright Codegen: + +```bash +npx playwright codegen https://cav.receita.fazenda.gov.br/autenticacao/login +``` + +Isto abre um navegador interativo e escreve os comandos Playwright conforme você navega — muito útil para extrair seletores reais do e-CAC. + +**Helper Windows para certificado** + +Há um helper opcional para Windows que tenta importar um PFX e selecionar o certificado no diálogo de autenticação do Windows usando `pywinauto`. + +- Arquivo: `win_cert_helper.py` +- Requer Python no Windows e dependência `pywinauto`. +- Uso recomendado: o `worker` (em Windows) inicia automaticamente o helper quando baixa um PFX; o helper aguarda o popup e tenta selecionar o certificado correspondente. + +Instalação mínima no Windows: + +```powershell +python -m pip install pywinauto +``` + +Observações: +- Este helper só funciona no Windows e deve ser executado no mesmo host onde o navegador será aberto. +- Se falhar, o `worker` tenta importar o PFX via `certutil` como fallback; em outros sistemas você precisará importar o PFX manualmente. + +Import automático (Windows) + +1. O `worker` agora tenta executar `win_import_pfx.ps1` para importar o PFX via `certutil` no usuário atual. Em seguida inicia `win_cert_helper.py` para aguardar e selecionar o certificado no diálogo. +2. Requisitos no Windows: + +```powershell +# Powershell script usa certutil (builtin) e o helper usa pywinauto +python -m pip install pywinauto +``` + +3. Se preferir usar um perfil de navegador persistente (útil para evitar diálogos com certificados ou reutilizar sessão), defina `BROWSER_USER_DATA_DIR` no `.env` apontando para uma pasta (ex.: `C:\Users\SeuUsuario\AppData\Local\Google\Chrome\User Data\PlaywrightProfile`). O `bot` abrirá o contexto persistente usando esse diretório. + +Exemplo `.env` parcial: + +``` +API_BASE_URL=https://sua.api +BOT_TOKEN=token +BROWSER_USER_DATA_DIR=C:\Users\SeuUsuario\AppData\Local\Google\Chrome\User Data\PlaywrightProfile +``` diff --git a/ecac-bot/installer.js b/ecac-bot/installer.js new file mode 100644 index 0000000..ac97f18 --- /dev/null +++ b/ecac-bot/installer.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node +const { spawnSync } = require('child_process'); +const os = require('os'); +const path = require('path'); +const fs = require('fs'); + +function run(cmd, args, opts = {}) { + console.log('>', cmd, args.join(' ')); + const r = spawnSync(cmd, args, Object.assign({ stdio: 'inherit' }, opts)); + if (r.error) throw r.error; + if (r.status !== 0) throw new Error(`${cmd} exited ${r.status}`); +} + +async function main() { + const platform = os.platform(); + const installerDir = path.join(__dirname, 'installers'); + + if (!fs.existsSync(installerDir)) { + console.error('installers dir not found'); + process.exit(2); + } + + try { + if (platform === 'win32') { + const script = path.join(installerDir, 'install_windows.ps1'); + if (!fs.existsSync(script)) throw new Error('install_windows.ps1 not found'); + console.log('Running Windows installer (PowerShell)'); + run('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script]); + } else { + const script = path.join(installerDir, 'install_unix.sh'); + if (!fs.existsSync(script)) throw new Error('install_unix.sh not found'); + console.log('Running Unix installer (sh)'); + run('sh', [script]); + } + + console.log('\nInstallation finished. Run `npm install` and `npx playwright install --with-deps` in ecac-bot if not already run.'); + } catch (e) { + console.error('Installer failed:', e.message || e); + process.exit(1); + } +} + +main(); diff --git a/ecac-bot/installers/install_unix.sh b/ecac-bot/installers/install_unix.sh new file mode 100644 index 0000000..8e3f2fd --- /dev/null +++ b/ecac-bot/installers/install_unix.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env sh +set -e +echo "=== ECAC-BOT INSTALLER (Unix) ===" + +command -v node >/dev/null 2>&1 || { echo "Node.js is required. Install: https://nodejs.org/"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "Python3 not found. Install python3 to use helper scripts."; } + +echo "Installing npm dependencies..." +npm install + +echo "Installing Playwright browsers (this may take a while)..." +npx playwright install --with-deps + +if [ -f requirements.txt ]; then + echo "Installing Python requirements (helper)..." + python3 -m pip install -r requirements.txt || echo "pip install may have failed" +fi + +if [ -n "$BROWSER_USER_DATA_DIR" ]; then + echo "Ensuring browser user data dir exists: $BROWSER_USER_DATA_DIR" + mkdir -p "$BROWSER_USER_DATA_DIR" +fi + +if [ ! -f .env ] && [ -f .env.example ]; then + cp .env.example .env + echo "Created .env from .env.example — edit it now before running the bot." +else + echo ".env already exists or .env.example missing." +fi + +echo "Installer finished. Please edit .env and then run: npm run start:worker" diff --git a/ecac-bot/installers/install_windows.ps1 b/ecac-bot/installers/install_windows.ps1 new file mode 100644 index 0000000..7cec0cc --- /dev/null +++ b/ecac-bot/installers/install_windows.ps1 @@ -0,0 +1,43 @@ +Write-Host "=== ECAC-BOT INSTALLER (Windows) ===" + +# Helper: check executables +function Check-Cmd($name) { + $p = Get-Command $name -ErrorAction SilentlyContinue + if (-not $p) { Write-Host "WARNING: $name not found in PATH" } + return $p -ne $null +} + +Write-Host "Checking Node/npm..." +if (-not (Check-Cmd node)) { Write-Error "Node.js is required. Install from https://nodejs.org/"; exit 2 } +if (-not (Check-Cmd python)) { Write-Host "Python not found. Please install Python 3." } + +Write-Host "Installing npm dependencies..." +npm install + +Write-Host "Installing Playwright browsers (this may take a while)..." +npx playwright install --with-deps + +if (Test-Path requirements.txt) { + Write-Host "Installing Python requirements (helper)..." + try { + python -m pip install -r requirements.txt + } catch { + Write-Warning "pip install may have failed. Ensure pip is available." + } +} + +# Create persistent browser profile dir if requested +if ($env:BROWSER_USER_DATA_DIR) { + Write-Host "Ensuring browser user data dir exists: $env:BROWSER_USER_DATA_DIR" + New-Item -ItemType Directory -Force -Path $env:BROWSER_USER_DATA_DIR | Out-Null +} + +# Copy .env.example to .env if not present +if (-not (Test-Path .env) -and (Test-Path .env.example)) { + Copy-Item .env.example .env + Write-Host "Created .env from .env.example — edit it now before running the bot." +} else { + Write-Host ".env already exists or .env.example missing." +} + +Write-Host "Installer finished. Please edit .env and then run: npm run start:worker" diff --git a/ecac-bot/installers/installer.iss b/ecac-bot/installers/installer.iss new file mode 100644 index 0000000..3e50a02 --- /dev/null +++ b/ecac-bot/installers/installer.iss @@ -0,0 +1,20 @@ +[Setup] +AppName=ECAC Bot Installer +AppVersion=0.1.0 +; Install per-user to avoid requiring Administrator privileges +PrivilegesRequired=lowest +DefaultDirName={userappdata}\ECAC-Bot +DisableProgramGroupPage=yes +OutputBaseFilename=ecac-bot-installer-setup +Compression=lzma +SolidCompression=yes +SourceDir=..\.. + +[Files] +; Copia todo o diretório ecac-bot para o diretório de instalação +Source: "ecac-bot\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs + +[Run] +; Executa o script PowerShell de pós-instalação para instalar dependências +; Run the helper as the original (non-elevated) user so %APPDATA% and env vars point to the launcher +Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\win_installer_helper.ps1"""; WorkingDir: "{app}"; Flags: runasoriginaluser diff --git a/ecac-bot/installers/win_installer_helper.ps1 b/ecac-bot/installers/win_installer_helper.ps1 new file mode 100644 index 0000000..06d5f58 --- /dev/null +++ b/ecac-bot/installers/win_installer_helper.ps1 @@ -0,0 +1,145 @@ +<# +win_installer_helper.ps1 +Orquestra instalação de pré-requisitos no Windows: +- instala Node.js (se ausente) via instalador oficial +- instala Python3 (se ausente) via instalador oficial +- executa `npm install` e `npx playwright install --with-deps` no diretório do app +- instala dependências Python (requirements.txt) se Python disponível + +Este script é chamado pelo Inno Setup installer com privilégios de usuário (RunAs). +#> + +param() + +function Write-Log($m) { Write-Host "[INSTALLER] $m" } + +Set-StrictMode -Version Latest + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Write-Log "Script dir: $scriptDir" + +function Ensure-Node { + Write-Log "Verificando Node..." + $node = Get-Command node -ErrorAction SilentlyContinue + if ($node) { Write-Log "Node já instalado: $($node.Path)"; return } + + Write-Log "Node não encontrado. Baixando instalador LTS..." + $nodeUrl = "https://nodejs.org/dist/v18.18.0/node-v18.18.0-x64.msi" + $tmp = Join-Path $env:TEMP "node-installer.msi" + Write-Log "Baixando $nodeUrl -> $tmp" + Invoke-WebRequest -Uri $nodeUrl -OutFile $tmp -UseBasicParsing + Write-Log "Executando instalador Node (silencioso)..." + Start-Process msiexec -ArgumentList "/i","`"$tmp`"","/quiet","/norestart"" -Wait + Remove-Item $tmp -ErrorAction SilentlyContinue +} + +function Ensure-Python { + Write-Log "Verificando Python3..." + $py = Get-Command python -ErrorAction SilentlyContinue + if ($py) { Write-Log "Python já disponível: $($py.Path)"; return } + + Write-Log "Python não encontrado. Baixando instalador..." + $pyUrl = "https://www.python.org/ftp/python/3.11.6/python-3.11.6-amd64.exe" + $tmp = Join-Path $env:TEMP "python-installer.exe" + Invoke-WebRequest -Uri $pyUrl -OutFile $tmp -UseBasicParsing + Write-Log "Executando instalador Python (silencioso)..." + Start-Process -FilePath $tmp -ArgumentList "/quiet InstallAllUsers=1 PrependPath=1" -Wait + Remove-Item $tmp -ErrorAction SilentlyContinue +} + +function Run-NpmInstallAndPlaywright($appDir) { + Write-Log "Executando npm install no diretório $appDir" + Push-Location $appDir + try { + if (Test-Path package.json) { + Write-Log "npm install..." + # Try to find npm.cmd in ProgramFiles (Node installer may not update PATH for this process) + $npmCmd = Join-Path $env:ProgramFiles 'nodejs\npm.cmd' + if (-not (Test-Path $npmCmd) -and $env:ProgramFiles(x86)) { + $npmCmd = Join-Path $env:ProgramFiles(x86) 'nodejs\npm.cmd' + } + if (Test-Path $npmCmd) { + Write-Log "Running $npmCmd install" + & "$npmCmd" install + } else { + Write-Log "npm.cmd not found under ProgramFiles, falling back to 'npm' (requires PATH to include Node)" + & npm install + } + } + + Write-Log "Instalando browsers Playwright (pode demorar)..." + # Similar lookup for npx + $npxCmd = Join-Path $env:ProgramFiles 'nodejs\npx.cmd' + if (-not (Test-Path $npxCmd) -and $env:ProgramFiles(x86)) { + $npxCmd = Join-Path $env:ProgramFiles(x86) 'nodejs\npx.cmd' + } + if (Test-Path $npxCmd) { + Write-Log "Running $npxCmd playwright install --with-deps" + & "$npxCmd" playwright install --with-deps + } else { + Write-Log "npx.cmd not found under ProgramFiles, falling back to 'npx'" + & npx playwright install --with-deps + } + } finally { + Pop-Location + } +} + +function Install-PythonReqs($appDir) { + $req = Join-Path $appDir "requirements.txt" + if (Test-Path $req) { + Write-Log "Instalando requisitos Python: $req" + Start-Process -FilePath python -ArgumentList "-m","pip","install","-r",$req -NoNewWindow -Wait + } +} + +try { + Write-Log "Iniciando orquestrador pós-instalação (modo seguro)" + + $appDir = $scriptDir + $logFile = Join-Path $appDir 'install-postlog.txt' + Write-Log "Post-install log: $logFile" + function Log-Write($m) { "$((Get-Date).ToString('s')) - $m" | Out-File -FilePath $logFile -Append -Encoding utf8 } + + # Check Node/npm availability — do not attempt to install Node automatically to avoid elevation issues + $node = Get-Command node -ErrorAction SilentlyContinue + $npmAvailable = $false + if ($node) { + Log-Write "Node detected: $($node.Path)" + # try to resolve npm + $npmCmd = Join-Path $env:ProgramFiles 'nodejs\npm.cmd' + if (-not (Test-Path $npmCmd) -and $env:ProgramFiles(x86)) { $npmCmd = Join-Path $env:ProgramFiles(x86) 'nodejs\npm.cmd' } + if (Test-Path $npmCmd) { $npmAvailable = $true; Log-Write "Found npm at $npmCmd" } else { + # fallback to PATH + $npm = Get-Command npm -ErrorAction SilentlyContinue + if ($npm) { $npmAvailable = $true; Log-Write "Found npm in PATH: $($npm.Path)" } + } + } else { + Log-Write "Node not found — skipping automatic installation to avoid requiring admin rights." + } + + if ($npmAvailable) { + Log-Write "Attempting npm install and Playwright browsers (this may take a while)" + try { + Run-NpmInstallAndPlaywright $appDir 2>&1 | ForEach-Object { Log-Write $_ } + } catch { + Log-Write "npm/playwright install failed: $_" + } + } else { + Log-Write "Skipping npm install because npm is not available. Instructing user to run npm install manually." + } + + # Python requirements: only install if python exists — do not attempt to install Python automatically + $py = Get-Command python -ErrorAction SilentlyContinue + if ($py) { + Log-Write "Python detected: $($py.Path) — installing Python requirements" + try { Install-PythonReqs $appDir 2>&1 | ForEach-Object { Log-Write $_ } } catch { Log-Write "pip install failed: $_" } + } else { Log-Write "Python not detected — skipping Python requirements installation." } + + Write-Log "Pós-instalação concluída (verifique $logFile para detalhes)." + [System.Windows.Forms.MessageBox]::Show('Instalação concluída. Edite .env em "' + $appDir + '" e então execute ecac-bot. Se dependências faltarem, rode manualmente `npm install` em ' + $appDir + '.', 'ECAC Bot', 'OK', 'Information') | Out-Null +} catch { + Write-Log "Erro durante instalação: $_" + # Do not rethrow — ensure installer reports success even if post-install had issues + Write-Log "Continuando sem interromper o instalador principal." +} diff --git a/ecac-bot/package.json b/ecac-bot/package.json new file mode 100644 index 0000000..d0974cd --- /dev/null +++ b/ecac-bot/package.json @@ -0,0 +1,26 @@ +{ + "name": "ecac-bot", + "version": "0.1.0", + "description": "Bot básico para automatizar alteração de perfil no e-CAC (placeholder).", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "start:worker": "node src/worker.js", + "check:env": "node scripts/checkEnv.js", + "install:all": "node installer.js", + "build:installer": "npx pkg installer.js --targets node16-win-x64 --output ecac-installer.exe", + "install-playwright": "npx playwright install --with-deps", + "postinstall": "npx playwright install --with-deps" + }, + "author": "", + "license": "MIT", + "dependencies": { + "dotenv": "^16.3.1", + "playwright": "^1.44.0", + "node-fetch": "^2.6.7" + } + , + "devDependencies": { + "pkg": "^5.8.0" + } +} diff --git a/ecac-bot/requirements.txt b/ecac-bot/requirements.txt new file mode 100644 index 0000000..5578e1f --- /dev/null +++ b/ecac-bot/requirements.txt @@ -0,0 +1,2 @@ +pywinauto>=0.6.8 +python-dotenv>=0.21.0 diff --git a/ecac-bot/scripts/checkEnv.js b/ecac-bot/scripts/checkEnv.js new file mode 100644 index 0000000..0e9ae1a --- /dev/null +++ b/ecac-bot/scripts/checkEnv.js @@ -0,0 +1,14 @@ +const fs = require('fs'); +require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); + +const requiredForWorker = ['API_BASE_URL', 'BOT_TOKEN']; +const missing = requiredForWorker.filter(k => !process.env[k]); + +if (missing.length) { + console.error('[checkEnv] Variáveis ausentes:', missing.join(', ')); + console.error('[checkEnv] Configure .env a partir de .env.example e tente novamente.'); + process.exit(2); +} + +console.log('[checkEnv] OK — variáveis essenciais presentes'); +process.exit(0); diff --git a/ecac-bot/src/apiClient.js b/ecac-bot/src/apiClient.js new file mode 100644 index 0000000..d31ceac --- /dev/null +++ b/ecac-bot/src/apiClient.js @@ -0,0 +1,58 @@ +const fetch = require('node-fetch'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { URLSearchParams } = require('url'); + +class APIClient { + constructor(baseUrl, botToken) { + this.baseUrl = (baseUrl || '').replace(/\/+$/, ''); + this.botToken = botToken; + this._cert_cache_dir = path.join(os.tmpdir(), 'ecac_bot_certs'); + if (!fs.existsSync(this._cert_cache_dir)) fs.mkdirSync(this._cert_cache_dir, { recursive: true }); + } + + async download_certificate(storage_path, empresa_id) { + try { + const url = `${this.baseUrl}/certificate?${new URLSearchParams({ path: storage_path, bucket: 'certificados' }).toString()}`; + const resp = await fetch(url, { headers: { 'x-bot-token': this.botToken }, timeout: 60000 }); + if (!resp.ok) throw new Error(`status ${resp.status}`); + const data = await resp.json(); + const cert_base64 = data && data.certificate; + if (!cert_base64) throw new Error('certificate not in response'); + const cert_bytes = Buffer.from(cert_base64, 'base64'); + const local_path = path.join(this._cert_cache_dir, `${empresa_id}.pfx`); + fs.writeFileSync(local_path, cert_bytes); + return local_path; + } catch (e) { + console.error('[API] download_certificate error', e); + return null; + } + } + + async get_pending() { + try { + const url = `${this.baseUrl}/pending`; + const resp = await fetch(url, { headers: { 'x-bot-token': this.botToken }, timeout: 60000 }); + if (!resp.ok) throw new Error(`status ${resp.status}`); + const data = await resp.json(); + return data.consultas || []; + } catch (e) { + console.error('[API] get_pending error', e); + return []; + } + } + + async send_result(consulta_id, status, result) { + try { + const url = `${this.baseUrl}/result`; + await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-bot-token': this.botToken }, body: JSON.stringify({ id: consulta_id, status, result }), timeout: 60000 }); + return true; + } catch (e) { + console.error('[API] send_result error', e); + return false; + } + } +} + +module.exports = APIClient; diff --git a/ecac-bot/src/bot.js b/ecac-bot/src/bot.js new file mode 100644 index 0000000..571f7dd --- /dev/null +++ b/ecac-bot/src/bot.js @@ -0,0 +1,188 @@ +const fs = require('fs'); +const readline = require('readline'); +const { chromium } = require('playwright'); + +const ECAC_URL = 'https://cav.receita.fazenda.gov.br/autenticacao/login'; + +// ===== CONFIGURAÇÕES ===== +const HEADLESS = String(process.env.HEADLESS_MODE || process.env.HEADLESS || 'false') === 'true'; +const SLOWMO = parseInt(process.env.SLOWMO_MS || '0', 10); +const TIMEOUT = parseInt(process.env.ECAC_TIMEOUT || '120', 10) * 1000; // converter para ms +const DETACH = String(process.env.DETACH_BROWSER || 'false') === 'true'; + +function waitForEnter(prompt) { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + return new Promise((resolve) => rl.question(prompt, () => { rl.close(); resolve(); })); +} + +async function run(consulta) { + const username = process.env.ECAC_USERNAME; + const password = process.env.ECAC_PASSWORD; + const desiredProfile = process.env.ECAC_PROFILE || 'Procurador'; + const headless = HEADLESS; + const slowMo = SLOWMO; + const timeout = TIMEOUT; + + if (!consulta && (!username || !password)) { + throw new Error('Defina ECAC_USERNAME e ECAC_PASSWORD no .env ou variáveis de ambiente, ou passe uma consulta com credenciais.'); + } + + // Se consulta fornecida contém _pfx_local, tente usar + const pfxLocal = consulta && (consulta._pfx_local || (consulta.certificado && consulta.certificado.local_path)); + const pfxPassword = consulta && (consulta.certificado && (consulta.certificado.senha || consulta.certificado.password)); + let browser = null; + let context = null; + let page = null; + + const userDataDir = process.env.BROWSER_USER_DATA_DIR || null; + if (userDataDir) { + console.log('[BOT] Iniciando contexto persistente em', userDataDir); + context = await chromium.launchPersistentContext(userDataDir, { headless, args: ['--start-maximized'], viewport: null, slowMo }); + page = context.pages().length ? context.pages()[0] : await context.newPage(); + } else { + browser = await chromium.launch({ headless, args: ['--start-maximized'], slowMo }); + context = await browser.newContext({ viewport: null }); + page = await context.newPage(); + } + + // Configurar timeout padrão das páginas + page.setDefaultTimeout(timeout); + + console.log('Acessando e-CAC...'); + await page.goto(ECAC_URL, { waitUntil: 'networkidle' }); + + // NOTE: os seletores abaixo são exemplos — o e-CAC pode mudar. Ajuste conforme necessário. + try { + // Se existe certificado local, logar via certificado pode exigir import no OS. + if (pfxLocal) { + console.log('[BOT] Certificado local encontrado:', pfxLocal); + if (process.platform === 'win32' && pfxPassword) { + console.log('[BOT] Se necessário, o certificado já pode ter sido importado pelo worker (certutil).'); + } else { + console.log('[BOT] Import manual do PFX pode ser necessário neste sistema para autenticação por certificado.'); + } + } + + // Tentativa robusta de localizar campos de login (vários seletores possíveis) + const userSelectors = ['input[name="cnpjcpf"]', 'input#cpf', 'input[name="cpfcnpj"]', 'input[type="text"]']; + const passSelectors = ['input[name="passwd"]', 'input[type="password"]', 'input#senha']; + + let filledUser = false; + for (const sel of userSelectors) { + try { + await page.fill(sel, username); + filledUser = true; + break; + } catch (e) {} + } + + let filledPass = false; + for (const sel of passSelectors) { + try { + await page.fill(sel, password); + filledPass = true; + break; + } catch (e) {} + } + + // Tentar submeter de forma resiliente + try { + await Promise.all([ + page.click('button[type="submit"]').catch(() => {}), + page.click('button:has-text("Entrar")').catch(() => {}), + page.waitForNavigation({ waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}), + ]); + } catch (e) {} + + // Salvar screenshot e HTML para depuração + await page.screenshot({ path: 'ecac-login-after-submit.png', fullPage: true }).catch(() => {}); + const html = await page.content(); + fs.writeFileSync('ecac-login-after-submit.html', html); + + console.log('Login submetido (ou tentativa feita). Se o site pedir MFA/dupla autenticação, complete manualmente no browser.'); + + if (!headless) { + console.log('Aguardando confirmação manual de MFA (pressione Enter após concluir)...'); + await waitForEnter('Pressione Enter aqui quando estiver autenticado no e-CAC...'); + } else { + console.log('Rodando em headless: se houver MFA, o fluxo pode falhar.'); + // tentar detectar elemento que indica login bem-sucedido + try { + await page.waitForSelector('text=Sair', { timeout: 5000 }); + } catch (e) { + console.warn('Não detectei claramente o indicador de sessão ativa. Verifique ecac-login-after-submit.png.'); + } + } + + // Após autenticação, salvar nova captura para entender a UI + await page.screenshot({ path: 'ecac-after-auth.png', fullPage: true }).catch(() => {}); + + // Procurar opções de perfil — abordagens variadas + console.log('Procurando elementos relacionados a perfil/permissões...'); + const profileTextCandidates = [desiredProfile, 'Procurador', 'Perfil', 'Perfil e Acessos', 'Acessos']; + + // Abrir menus prováveis (tenta vários seletores) + const menuCandidates = ['button[aria-label*="menu"]', 'button[aria-label*="Usuário"]', 'button[title*="Perfil"]', 'button:has-text("Acessos")', 'text=Menu']; + for (const sel of menuCandidates) { + try { + const loc = page.locator(sel).first(); + if (await loc.count()) { + await loc.click().catch(() => {}); + await page.waitForTimeout(500); + } + } catch (e) {} + } + + // Procurar o texto do perfil desejado em qualquer elemento clicável + let found = false; + for (const txt of profileTextCandidates) { + try { + const loc = page.locator(`text=${txt}`); + if (await loc.count()) { + // clicar no primeiro que parece relevante + await loc.first().click().catch(() => {}); + console.log(`Clicado em elemento com texto: ${txt}`); + found = true; + break; + } + } catch (e) {} + } + + // Se encontrou um menu de perfil, tentar salvar confirmação + await page.screenshot({ path: 'ecac-after-profile-click.png', fullPage: true }).catch(() => {}); + + if (!found) { + console.log('Não encontrei automaticamente o perfil. Veja screenshots geradas e ajuste seletores em src/bot.js.'); + } else { + // tentar clicar em salvar/confirmar se existir + const confirmCandidates = ['button:has-text("Salvar")', 'button:has-text("Confirmar")', 'button:has-text("OK")', 'button[title*="Salvar"]']; + for (const sel of confirmCandidates) { + try { + const el = page.locator(sel).first(); + if (await el.count()) { + await el.click().catch(() => {}); + console.log('Cliquei em botão de confirmação/salvar.'); + break; + } + } catch (e) {} + } + await page.screenshot({ path: 'ecac-after-confirm.png', fullPage: true }).catch(() => {}); + } + + } finally { + if (DETACH) { + console.log('DETACH_BROWSER ativado — mantendo navegador aberto para depuração.'); + return; + } + console.log('Finalizando: salvando artefatos e fechando navegador em 3s...'); + await new Promise((r) => setTimeout(r, 3000)); + try { + if (context) await context.close(); + } catch (e) {} + try { + if (browser) await browser.close(); + } catch (e) {} + } +} + +module.exports = { run }; diff --git a/ecac-bot/src/index.js b/ecac-bot/src/index.js new file mode 100644 index 0000000..487e59b --- /dev/null +++ b/ecac-bot/src/index.js @@ -0,0 +1,29 @@ +try { + require('dotenv').config(); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + console.error('\n❌ Erro: dependências não instaladas.\n'); + console.error('Execute: npm install'); + console.error('\nOu manualmente instale dotenv:'); + console.error(' npm install dotenv\n'); + process.exit(1); + } + throw e; +} + +const argv = process.argv.slice(2); +// Se chamado diretamente, inicia o worker por padrão +if (argv.includes('--worker') || process.env.WORKER === 'true') { + require('./worker'); +} else { + const { run } = require('./bot'); + (async () => { + try { + await run(); + console.log('Operação finalizada.'); + } catch (err) { + console.error('Erro:', err); + process.exitCode = 1; + } + })(); +} diff --git a/ecac-bot/src/worker.js b/ecac-bot/src/worker.js new file mode 100644 index 0000000..5b48c42 --- /dev/null +++ b/ecac-bot/src/worker.js @@ -0,0 +1,197 @@ +const path = require('path'); +const os = require('os'); +const fs = require('fs'); +const { spawnSync } = require('child_process'); +require('dotenv').config(); + +const APIClient = require('./apiClient'); +const { run } = require('./bot'); + +// ===== CONFIGURAÇÕES DA API ===== +const API_BASE_URL = process.env.API_BASE_URL || ''; +const BOT_TOKEN = process.env.BOT_TOKEN || ''; + +if (!API_BASE_URL || !BOT_TOKEN) { + console.error('Configure API_BASE_URL e BOT_TOKEN no .env'); + process.exit(1); +} + +// ===== CONFIGURAÇÕES DO WORKER ===== +const POLLING_INTERVAL = parseInt(process.env.POLLING_INTERVAL || '60', 10) * 1000; // converter para ms +const MAX_WORKERS = parseInt(process.env.MAX_WORKERS || '2', 10); +const ECAC_TIMEOUT = parseInt(process.env.ECAC_TIMEOUT || '120', 10) * 1000; // converter para ms +const LOG_LEVEL = process.env.LOG_LEVEL || 'INFO'; +const LOG_FILE = process.env.LOG_FILE || './logs/bot.log'; +const SCREENSHOTS_DIR = process.env.SCREENSHOTS_DIR || './screenshots'; + +// Criar diretórios se não existirem +const logDir = path.dirname(LOG_FILE); +if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true }); +if (!fs.existsSync(SCREENSHOTS_DIR)) fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true }); + +const api = new APIClient(API_BASE_URL, BOT_TOKEN); + +async function processOne(item) { + console.log('[WORKER] Processing', item.id || '(no-id)'); + + // Prefer certificate info from item + let cert_local = null; + try { + const cert_info = item.certificado || item.certificado_contador || null; + if (cert_info && typeof cert_info === 'object') { + const storage_path = cert_info.path || cert_info.storage_path; + const senha = cert_info.senha || cert_info.password || ''; + const empresa_id = (item.empresa && item.empresa.id) ? String(item.empresa.id) : String(item.id || 'noid'); + if (storage_path) { + // On Windows, prefer using the PowerShell fetch+import helper which also imports via certutil + if (process.platform === 'win32') { + try { + const fetchScript = path.join(__dirname, '..', 'win_fetch_and_import_pfx.ps1'); + if (fs.existsSync(fetchScript)) { + console.log('[WORKER] Using PowerShell fetch+import for PFX...'); + const args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', fetchScript, '-StoragePath', storage_path, '-ApiBase', API_BASE_URL, '-BotToken', BOT_TOKEN]; + // If cert has password, pass it + const senha = cert_info.senha || cert_info.password || ''; + if (senha) args.push('-Password', String(senha)); + const r = spawnSync('powershell', args, { encoding: 'utf8', timeout: 180000 }); + console.log('[WORKER] fetch powershell stdout:', r.stdout); + console.log('[WORKER] fetch powershell stderr:', r.stderr); + if (r.status === 0) { + // PowerShell prints the temp path as last output line; try to capture it + const out = (r.stdout || '').trim().split(/\r?\n/).filter(Boolean); + if (out.length) { + const candidate = out[out.length - 1].trim(); + if (fs.existsSync(candidate)) { + cert_local = candidate; + console.log('[WORKER] PFX fetched to', cert_local); + } + } + } else { + console.warn('[WORKER] PowerShell fetch script failed with code', r.status); + } + } + } catch (e) { + console.warn('[WORKER] PowerShell fetch failed', e.message || e); + } + } + + // Fallback to Node download if not obtained + if (!cert_local) { + cert_local = await api.download_certificate(storage_path, empresa_id); + } + if (cert_local) { + console.log('[WORKER] Certificate saved to', cert_local); + // If running on Windows, try import via certutil + if (process.platform === 'win32' && senha) { + try { + console.log('[WORKER] Importing certificate into Windows store (user)...'); + const r = spawnSync('certutil', ['-f', '-user', '-p', String(senha), '-importpfx', cert_local], { encoding: 'utf8' }); + console.log('[WORKER] certutil stdout:', r.stdout); + console.log('[WORKER] certutil stderr:', r.stderr); + } catch (e) { + console.warn('[WORKER] certutil import failed:', e.message); + } + } else { + console.log('[WORKER] Non-Windows platform or no password — you may need to import the PFX manually into the OS/browser.'); + } + } + // attach local path to item for bot + item._pfx_local = cert_local; + } + } + } catch (e) { + console.error('[WORKER] certificate handling failed', e); + } + + // Run bot for this consulta + try { + let helperProc = null; + // On Windows, try to start Python helper to watch/select certificate when popup appears + if (process.platform === 'win32' && cert_local) { + try { + const helperPath = path.join(__dirname, '..', 'win_cert_helper.py'); + const importScript = path.join(__dirname, '..', 'win_import_pfx.ps1'); + const senha = (item.certificado && (item.certificado.senha || item.certificado.password)) || (item.certificado_contador && (item.certificado_contador.senha || item.certificado_contador.password)) || ''; + const terms = []; + if (item.certificado_contador) { + ['nome', 'cn', 'cpf', 'cnpj'].forEach(k => { if (item.certificado_contador[k]) terms.push(String(item.certificado_contador[k])); }); + } + const cnpj = (item.empresa && item.empresa.cnpj) || (item.parametros && item.parametros.cnpj); + if (cnpj) terms.push(String(cnpj)); + + // First try to run PowerShell import script (certutil wrapper) + try { + if (fs.existsSync(importScript)) { + console.log('[WORKER] Running PowerShell import script...'); + const r = spawnSync('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', importScript, '-PfxPath', cert_local, '-Password', String(senha)], { encoding: 'utf8', timeout: 120000 }); + console.log('[WORKER] powershell stdout:', r.stdout); + console.log('[WORKER] powershell stderr:', r.stderr); + } + } catch (e) { + console.warn('[WORKER] PowerShell import failed:', e.message || e); + } + + const spawn = require('child_process').spawn; + const helperArgs = [helperPath]; + helperArgs.push('--pfx', cert_local); + if (senha) helperArgs.push('--pass', String(senha)); + if (terms.length) helperArgs.push('--terms', terms.join(',')); + helperArgs.push('--timeout', '120'); + + helperProc = spawn('python', helperArgs, { stdio: 'inherit' }); + console.log('[WORKER] Started Windows helper pid=', helperProc.pid); + } catch (e) { + console.warn('[WORKER] could not start windows helper:', e.message || e); + } + } + + await run(item); + console.log('[WORKER] run completed for', item.id); + await api.send_result(item.id, 'OK', { page_url: 'see artifacts', success: true }); + + // cleanup helper + try { + if (helperProc && !helperProc.killed) { + helperProc.kill(); + console.log('[WORKER] Killed helper process'); + } + } catch (e) {} + } catch (e) { + console.error('[WORKER] run error for', item.id, e); + await api.send_result(item.id, 'ERROR', { error: String(e) }); + } +} + +async function loop() { + console.log('[WORKER] polling API', API_BASE_URL); + try { + const pending = await api.get_pending(); + if (!pending || pending.length === 0) { + console.log('[WORKER] no pending consults'); + return; + } + for (const item of pending) { + try { + await processOne(item); + } catch (e) { + console.error('[WORKER] processOne failed', e); + } + } + } catch (e) { + console.error('[WORKER] main loop error', e); + } +} + +(async () => { + try { + while (true) { + await loop(); + const intervalSec = POLLING_INTERVAL / 1000; + console.log(`[WORKER] Aguardando ${intervalSec}s antes da próxima verificação...`); + await new Promise(resolve => setTimeout(resolve, POLLING_INTERVAL)); + } + } catch (e) { + console.error('[WORKER] fatal', e); + process.exit(1); + } +})(); diff --git a/ecac-bot/win_cert_helper.py b/ecac-bot/win_cert_helper.py new file mode 100644 index 0000000..b76a34d --- /dev/null +++ b/ecac-bot/win_cert_helper.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Helper Windows para importar PFX e selecionar certificado no popup de autenticação. + +Uso (ex.): + python win_cert_helper.py --pfx C:\temp\123.pfx --pass senha --terms 'Empresa,CPF' --timeout 120 + +Observações: +- Requer Python + pywinauto instalados no Windows. + pip install pywinauto python-dotenv +- Este script deve ser executado no host Windows onde o navegador será aberto. +""" + +import argparse +import subprocess +import time +import logging +import sys +from pathlib import Path + +try: + from pywinauto import Desktop + from pywinauto.findwindows import ElementNotFoundError +except Exception: + print('pywinauto não encontrado. Instale: pip install pywinauto') + sys.exit(2) + +logging.basicConfig(level=logging.INFO, format='[WIN_HELPER] %(asctime)s %(levelname)s %(message)s') +logger = logging.getLogger(__name__) + +WINDOW_TITLES = [ + 'Segurança do Windows', + 'Windows Security', + 'Autenticação do Cliente', + 'Client Authentication' +] + + +def import_pfx(pfx_path: Path, password: str) -> bool: + try: + if not pfx_path.exists(): + logger.error('PFX não encontrado: %s', pfx_path) + return False + cmd = ['certutil', '-f', '-user', '-p', password, '-importpfx', str(pfx_path)] + logger.info('Executando certutil...') + r = subprocess.run(cmd, capture_output=True, text=True, timeout=120) + logger.info('certutil stdout: %s', r.stdout) + logger.info('certutil stderr: %s', r.stderr) + return r.returncode == 0 + except Exception as e: + logger.exception('import_pfx falhou: %s', e) + return False + + +def aguardar_e_selecionar(match_terms, timeout_sec=120): + logger.info('Aguardando popup de seleção de certificado por até %ss', timeout_sec) + start = time.time() + while (time.time() - start) < timeout_sec: + try: + desktop = Desktop(backend='uia') + for title in WINDOW_TITLES: + try: + dlg = desktop.window(title_re=f'.*{title}.*') + if dlg.exists(timeout=1): + logger.info('Encontrado diálogo: %s', title) + time.sleep(1) + try: + lists = dlg.descendants(control_type='List') + if lists: + items = lists[0].children() + logger.info('Certificados: %d', len(items)) + if match_terms: + for item in items: + text = item.window_text() + for term in match_terms: + if term.strip().lower() in text.lower(): + logger.info('Match %s => %s', term, text) + try: + item.click_input() + time.sleep(0.8) + except Exception: + pass + # tentar clicar OK + for ok_text in ('OK', 'Ok', 'Selecionar', 'Selecionar certificado'): + try: + btn = dlg.child_window(title=ok_text, control_type='Button') + if btn.exists(): + btn.click() + logger.info('Clicou %s', ok_text) + time.sleep(1) + return True + except Exception: + continue + # fallback: ENTER + try: + dlg.type_keys('{ENTER}') + time.sleep(1) + return True + except Exception: + return False + except Exception: + logger.exception('Erro ao interagir com diálogo') + except ElementNotFoundError: + continue + except Exception: + logger.exception('Erro enumerando desktop') + time.sleep(1) + logger.warning('Timeout esperando popup de certificado') + return False + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--pfx', help='Caminho para arquivo PFX', required=False) + ap.add_argument('--pass', dest='pfx_pass', help='Senha do PFX', required=False) + ap.add_argument('--terms', help='Termos separados por vírgula para localizar certificado', required=False) + ap.add_argument('--timeout', type=int, default=120) + args = ap.parse_args() + + terms = [] + if args.terms: + terms = [t.strip() for t in args.terms.split(',') if t.strip()] + + if args.pfx and args.pfx_pass: + p = Path(args.pfx) + ok = import_pfx(p, args.pfx_pass) + logger.info('Import PFX: %s', ok) + # small pause + time.sleep(2) + + ok = aguardar_e_selecionar(terms, timeout_sec=args.timeout) + if ok: + logger.info('Seleção concluída') + sys.exit(0) + else: + logger.warning('Seleção falhou/timeout') + sys.exit(3) + + +if __name__ == '__main__': + main() diff --git a/ecac-bot/win_fetch_and_import_pfx.ps1 b/ecac-bot/win_fetch_and_import_pfx.ps1 new file mode 100644 index 0000000..8fb72e2 --- /dev/null +++ b/ecac-bot/win_fetch_and_import_pfx.ps1 @@ -0,0 +1,62 @@ +param( + [string]$ApiBase, + [string]$BotToken, + [Parameter(Mandatory=$true)][string]$StoragePath, + [string]$Password +) + +if (-not $ApiBase) { $ApiBase = $env:API_BASE_URL } +if (-not $BotToken) { $BotToken = $env:BOT_TOKEN } + +if (-not $ApiBase) { + Write-Error "API_BASE_URL não informada (param --ApiBase ou variável de ambiente API_BASE_URL)" + exit 2 +} +if (-not $BotToken) { + Write-Error "BOT_TOKEN não informado (param --BotToken ou variável de ambiente BOT_TOKEN)" + exit 2 +} + +try { + $encPath = [System.Uri]::EscapeDataString($StoragePath) + $url = "$ApiBase/certificate?path=$encPath&bucket=certificados" + Write-Host "[FETCH] GET $url" + $headers = @{ 'x-bot-token' = $BotToken } + $resp = Invoke-RestMethod -Uri $url -Headers $headers -Method Get -UseBasicParsing -ErrorAction Stop + + if (-not $resp.certificate) { + Write-Error "Resposta não contém 'certificate'" + exit 3 + } + + $certBase64 = $resp.certificate + $bytes = [System.Convert]::FromBase64String($certBase64) + $tmp = Join-Path $env:TEMP ("ecac_cert_{0}.pfx" -f ([guid]::NewGuid().ToString())) + [System.IO.File]::WriteAllBytes($tmp, $bytes) + Write-Host "[FETCH] Salvo temporário: $tmp" + + # Chamar script de import local + $importScript = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'win_import_pfx.ps1' + if (-not (Test-Path $importScript)) { + Write-Warning "Import script não encontrado: $importScript — abortando import automático" + Write-Output $tmp + exit 0 + } + + Write-Host "[FETCH] Chamando import: $importScript" + $args = @('-NoProfile','-ExecutionPolicy','Bypass','-File',$importScript,'-PfxPath',$tmp) + if ($Password) { $args += @('-Password',$Password) } + + $proc = Start-Process -FilePath powershell -ArgumentList $args -NoNewWindow -Wait -PassThru -ErrorAction SilentlyContinue + Write-Host "[FETCH] import exit code: $($proc.ExitCode)" + if ($proc.ExitCode -ne 0) { + Write-Warning "Import script retornou código $($proc.ExitCode)" + } + + Write-Output $tmp + exit $proc.ExitCode + +} catch { + Write-Error "Erro ao buscar/importar certificado: $_" + exit 4 +} diff --git a/ecac-bot/win_import_pfx.ps1 b/ecac-bot/win_import_pfx.ps1 new file mode 100644 index 0000000..7426d5f --- /dev/null +++ b/ecac-bot/win_import_pfx.ps1 @@ -0,0 +1,28 @@ +param( + [Parameter(Mandatory=$true)][string]$PfxPath, + [Parameter(Mandatory=$true)][string]$Password, + [string]$ChromeProfilePath +) + +Write-Host "[PS] Importando PFX: $PfxPath" +if (-not (Test-Path $PfxPath)) { + Write-Error "Arquivo PFX não encontrado: $PfxPath" + exit 2 +} + +# Importa para loja do usuário +$cmd = "certutil -f -user -p `"$Password`" -importpfx `"$PfxPath`"" +Write-Host "[PS] Executando: $cmd" +$proc = Start-Process -FilePath certutil -ArgumentList '-f','-user','-p',$Password,'-importpfx',$PfxPath -NoNewWindow -Wait -PassThru -ErrorAction SilentlyContinue +if ($proc.ExitCode -eq 0) { + Write-Host "[PS] Import OK" +} else { + Write-Warning "[PS] certutil retornou código: $($proc.ExitCode)" +} + +if ($ChromeProfilePath) { + Write-Host "[PS] Chrome profile path fornecido: $ChromeProfilePath" + # Apenas informa; import já foi feito na loja do usuário. Dependendo do cenário, pode ser necessário copiar PFX +} + +exit 0