PC SOFT

PROFESSIONAL NEWSGROUPS
WINDEVWEBDEV and WINDEV Mobile

Home → WINDEV 25 → Solução segura para bloquear comandos Drop, delete e outros do Postgresql com Windev
Solução segura para bloquear comandos Drop, delete e outros do Postgresql com Windev
Started by Boller, Jun., 11 2025 3:12 PM - 4 replies
Registered member
4,520 messages
Posted on June, 11 2025 - 3:12 PM
Vou criar uma solução completa para bloquear comandos SQL perigosos no PostgreSQL usando um arquivo INI. Esta implementação intercepta comandos antes de enviá-los ao banco.

## 1. Estrutura do Arquivo INI

Primeiro, vamos definir o arquivo `pgsql_security.ini`:

```ini
[SECURITY]
; Ativa ou desativa o bloqueio de comandos
EnableBlocking=1
; Modo: 0=Bloquear, 1=Avisar apenas, 2=Log apenas
BlockMode=0
; Ativar log de tentativas
EnableLogging=1
; Caminho do arquivo de log
LogFile=C:\ProgramData\PostgreSQL\security.log

[BLOCKED_COMMANDS]
; Comandos completamente bloqueados
DELETE=1
TRUNCATE=1
DROP=1
ALTER=1
UPDATE=1
GRANT=1
REVOKE=1
INSERT=1

[WHITELIST]
; Queries permitidas mesmo com comandos bloqueados
; Use hashes MD5 das queries completas
Query1=5d41402abc4b2a76b9719d911017c592
Query2=098f6bcd4621d373cade4e832627b4f6

[EXCEPTIONS]
; Usuários com permissão total
AdminUsers=postgres,admin_sistema
; IPs com permissão total
TrustedIPs=127.0.0.1,192.168.1.100
; Aplicações com permissão total
TrustedApps=pgAdmin.exe,maintenance.exe
```



## 2. Classe WinDev para Gerenciar Segurança

```wlanguage
// Classe para gerenciar segurança PostgreSQL
CLASS PgSecurityManager
PRIVATE
m_sIniPath is string
m_bEnabled is boolean
m_nBlockMode is int
m_bLogging is boolean
m_sLogFile is string
m_arrBlockedCommands is array of string
m_arrWhitelistHashes is array of string
m_arrAdminUsers is array of string
m_arrTrustedIPs is array of string
m_arrTrustedApps is array of string

PUBLIC
// Constructor
CONSTRUCTOR()
// Buscar o diretório das DLLs do PostgreSQL
m_sIniPath = GetPostgreSQLPath() + "\pgsql_security.ini"
LoadConfiguration()
END

// Obter caminho do PostgreSQL
PROCEDURE PRIVATE GetPostgreSQLPath(): string
sPath is string

// Tentar localizar via registro do Windows
sPath = RegistryQueryValue("HKEY_LOCAL_MACHINE\SOFTWARE\PostgreSQL\Installations\postgresql-14", "Base Directory")
IF sPath = "" THEN
// Tentar caminhos padrão
IF fDirectoryExist("C:\Program Files\PostgreSQL\14") THEN
sPath = "C:\Program Files\PostgreSQL\14\bin"
ELSE IF fDirectoryExist("C:\Program Files\PostgreSQL\13") THEN
sPath = "C:\Program Files\PostgreSQL\13\bin"
ELSE
// Usar diretório da aplicação como fallback
sPath = fExeDir()
END
ELSE
sPath += "\bin"
END

RETURN sPath
END

// Carregar configuração do INI
PROCEDURE PRIVATE LoadConfiguration()
// Verificar se o arquivo existe
IF NOT fFileExist(m_sIniPath) THEN
m_bEnabled = False
RETURN
END

// Carregar configurações gerais
m_bEnabled = (INIRead("SECURITY", "EnableBlocking", "1", m_sIniPath) = "1")
m_nBlockMode = Val(INIRead("SECURITY", "BlockMode", "0", m_sIniPath))
m_bLogging = (INIRead("SECURITY", "EnableLogging", "1", m_sIniPath) = "1")
m_sLogFile = INIRead("SECURITY", "LogFile", fExeDir() + "\pgsql_security.log", m_sIniPath)

// Carregar comandos bloqueados
DeleteAll(m_arrBlockedCommands)
arrCommands is array of string = ["DELETE", "TRUNCATE", "DROP", "ALTER", "UPDATE", "GRANT", "REVOKE", "INSERT"]
FOR EACH sCmd OF arrCommands
IF INIRead("BLOCKED_COMMANDS", sCmd, "0", m_sIniPath) = "1" THEN
Add(m_arrBlockedCommands, sCmd)
END
END

// Carregar whitelist
DeleteAll(m_arrWhitelistHashes)
i is int = 1
sHash is string
LOOP
sHash = INIRead("WHITELIST", "Query" + i, "", m_sIniPath)
IF sHash = "" THEN BREAK
Add(m_arrWhitelistHashes, sHash)
i++
END

// Carregar exceções
LoadExceptions()
END

// Carregar lista de exceções
PROCEDURE PRIVATE LoadExceptions()
sValue is string

// Usuários admin
sValue = INIRead("EXCEPTIONS", "AdminUsers", "", m_sIniPath)
IF sValue <> "" THEN
StringToArray(sValue, m_arrAdminUsers, ",")
END

// IPs confiáveis
sValue = INIRead("EXCEPTIONS", "TrustedIPs", "", m_sIniPath)
IF sValue <> "" THEN
StringToArray(sValue, m_arrTrustedIPs, ",")
END

// Aplicações confiáveis
sValue = INIRead("EXCEPTIONS", "TrustedApps", "", m_sIniPath)
IF sValue <> "" THEN
StringToArray(sValue, m_arrTrustedApps, ",")
END
END

// Verificar se query é permitida
PROCEDURE ValidateQuery(sQuery is string, sUser is string = "", sIP is string = ""): boolean
// Se segurança desabilitada, permitir tudo
IF NOT m_bEnabled THEN RETURN True

// Verificar exceções
IF IsException(sUser, sIP) THEN RETURN True

// Verificar whitelist (hash MD5 da query)
sQueryHash is string = HashString(HA_MD5, Upper(NoSpace(sQuery)))
IF Seek(m_arrWhitelistHashes, sQueryHash) > 0 THEN
RETURN True
END

// Verificar comandos bloqueados
sQueryUpper is string = Upper(sQuery)
FOR EACH sCmd OF m_arrBlockedCommands
IF StringContains(sQueryUpper, sCmd) THEN
// Verificar se é realmente o comando (não parte de string)
IF IsActualCommand(sQueryUpper, sCmd) THEN
HandleBlockedCommand(sQuery, sCmd, sUser, sIP)
RETURN (m_nBlockMode = 1) // Retorna True se modo for "Avisar apenas"
END
END
END

RETURN True
END

// Verificar se é realmente um comando SQL
PROCEDURE PRIVATE IsActualCommand(sQuery is string, sCommand is string): boolean
// Remover comentários SQL
sClean is string = RemoveSQLComments(sQuery)

// Padrões para identificar comandos reais
arrPatterns is array of string
Add(arrPatterns, "^" + sCommand + "\s") // Início da query
Add(arrPatterns, ";\s*" + sCommand + "\s") // Após ponto e vírgula

// Verificar padrões
FOR EACH sPattern OF arrPatterns
IF RegexMatch(sClean, sPattern) THEN
RETURN True
END
END

RETURN False
END

// Remover comentários SQL
PROCEDURE PRIVATE RemoveSQLComments(sQuery is string): string
sResult is string = sQuery

// Remover comentários de linha (--)
sResult = RegexReplace(sResult, "--.*?$", "", RegexMultiline)

// Remover comentários de bloco (/* */)
sResult = RegexReplace(sResult, "/\*.*?\*/", "", RegexDotAll)

RETURN sResult
END

// Verificar exceções
PROCEDURE PRIVATE IsException(sUser is string, sIP is string): boolean
// Verificar usuário admin
IF sUser <> "" AND Seek(m_arrAdminUsers, sUser) > 0 THEN
RETURN True
END

// Verificar IP confiável
IF sIP <> "" AND Seek(m_arrTrustedIPs, sIP) > 0 THEN
RETURN True
END

// Verificar aplicação confiável
sAppName is string = fExtractPath(ExeInfo(exeName), fFileName + fExtension)
IF Seek(m_arrTrustedApps, sAppName) > 0 THEN
RETURN True
END

RETURN False
END

// Tratar comando bloqueado
PROCEDURE PRIVATE HandleBlockedCommand(sQuery is string, sCommand is string, sUser is string, sIP is string)
sMessage is string

SWITCH m_nBlockMode
CASE 0: // Bloquear
sMessage = StringBuild("BLOQUEADO: Comando %1 não permitido", sCommand)
Error(sMessage)

CASE 1: // Avisar
sMessage = StringBuild("AVISO: Comando %1 detectado mas executado", sCommand)
Warning(sMessage)

CASE 2: // Log apenas
sMessage = StringBuild("LOG: Comando %1 executado", sCommand)
END

// Registrar no log
IF m_bLogging THEN
LogCommand(sQuery, sCommand, sUser, sIP, sMessage)
END
END

// Registrar no log
PROCEDURE PRIVATE LogCommand(sQuery is string, sCommand is string, sUser is string, sIP is string, sMessage is string)
sLogEntry is string
sLogEntry = DateToString(Today(), maskDateSystem) + " " + TimeToString(Now(), "HH:MM:SS") + TAB
sLogEntry += sUser + TAB + sIP + TAB + sCommand + TAB
sLogEntry += sMessage + TAB + Left(sQuery, 200) + CR

fSaveText(m_sLogFile, sLogEntry, foAdd)
END

// Recarregar configuração
PROCEDURE ReloadConfiguration()
LoadConfiguration()
END

// Obter status
PROCEDURE GetStatus(): string
IF NOT m_bEnabled THEN RETURN "Segurança desabilitada"

sStatus is string = "Segurança ativa - "
SWITCH m_nBlockMode
CASE 0: sStatus += "Modo: Bloquear"
CASE 1: sStatus += "Modo: Avisar"
CASE 2: sStatus += "Modo: Log apenas"
END

RETURN sStatus
END
END
```




## 3. Wrapper para Conexão PostgreSQL Segura

```wlanguage
// Classe wrapper para conexão PostgreSQL com segurança
CLASS SecurePostgreSQLConnection
PRIVATE
m_Connection is Connection
m_Security is PgSecurityManager
m_sCurrentUser is string
m_sClientIP is string

PUBLIC
// Constructor
CONSTRUCTOR(sServer is string, sDatabase is string, sUser is string, sPassword is string)
// Inicializar gerenciador de segurança
m_Security = new PgSecurityManager()

// Configurar conexão
m_Connection..Provider = hAccessNativePostgreSQL
m_Connection..Server = sServer
m_Connection..Database = sDatabase
m_Connection..User = sUser
m_Connection..Password = sPassword

m_sCurrentUser = sUser
m_sClientIP = NetIPAddress()

// Abrir conexão
IF NOT HOpenConnection(m_Connection) THEN
Error("Erro ao conectar: " + HErrorInfo())
END
END

// Executar query com validação
PROCEDURE ExecuteQuery(sQuery is string, sDataSource is string = "QueryResult"): boolean
// Validar query antes de executar
IF NOT m_Security.ValidateQuery(sQuery, m_sCurrentUser, m_sClientIP) THEN
RETURN False
END

// Executar query
RETURN HExecuteSQLQuery(sDataSource, m_Connection, hQueryWithoutCorrection, sQuery)
END

// Executar query parametrizada (mais segura)
PROCEDURE ExecuteParameterizedQuery(sQuery is string, ...): boolean
// Validar query base
IF NOT m_Security.ValidateQuery(sQuery, m_sCurrentUser, m_sClientIP) THEN
RETURN False
END

// Montar query com parâmetros
sFullQuery is string = StringBuild(sQuery, MyParameters)

RETURN HExecuteSQLQuery("QueryResult", m_Connection, hQueryWithoutCorrection, sFullQuery)
END

// Adicionar query à whitelist
PROCEDURE AddToWhitelist(sQuery is string, sDescription is string = "")
sHash is string = HashString(HA_MD5, Upper(NoSpace(sQuery)))

// Encontrar próximo índice disponível
i is int = 1
WHILE INIRead("WHITELIST", "Query" + i, "", m_Security.m_sIniPath) <> ""
i++
END

// Adicionar ao INI
INIWrite("WHITELIST", "Query" + i, sHash, m_Security.m_sIniPath)
IF sDescription <> "" THEN
INIWrite("WHITELIST", "Query" + i + "_Desc", sDescription, m_Security.m_sIniPath)
END

// Recarregar configuração
m_Security.ReloadConfiguration()
END

// Obter status de segurança
PROCEDURE GetSecurityStatus(): string
RETURN m_Security.GetStatus()
END
END
```




## 4. Exemplo de Uso

```wlanguage
// Inicializar conexão segura
MyDB is SecurePostgreSQLConnection("localhost", "meu_banco", "usuario", "senha")

// Queries permitidas
IF MyDB.ExecuteQuery("SELECT * FROM clientes WHERE ativo = true") THEN
// Processar resultados
WHILE NOT HOut()
Info(QueryResult.nome)
HReadNext()
END
END

// Esta query será bloqueada
IF NOT MyDB.ExecuteQuery("DELETE FROM clientes WHERE id = 1") THEN
// Mensagem de erro já foi exibida
END

// Adicionar uma query específica à whitelist
sQueryEspecial is string = "UPDATE configuracoes SET valor = 'novo' WHERE chave = 'versao'"
MyDB.AddToWhitelist(sQueryEspecial, "Atualização de versão do sistema")

// Verificar status
Info(MyDB.GetSecurityStatus())
```



## 5. Utilitário de Configuração

```wlanguage
// Window para gerenciar configurações de segurança
PROCEDURE WIN_SecurityConfig()
// Carregar configurações atuais
sIniPath is string = fExeDir() + "\pgsql_security.ini"

// Interface para editar configurações
EDT_EnableBlocking = INIRead("SECURITY", "EnableBlocking", "1", sIniPath)
COMBO_BlockMode = Val(INIRead("SECURITY", "BlockMode", "0", sIniPath)) + 1
EDT_LogFile = INIRead("SECURITY", "LogFile", "", sIniPath)

// Carregar comandos bloqueados
TABLE_Commands.DeleteAll()
arrCommands is array of string = ["DELETE", "TRUNCATE", "DROP", "ALTER", "UPDATE", "GRANT", "REVOKE", "INSERT"]
FOR EACH sCmd OF arrCommands
nRow is int = TABLE_Commands.AddLine(sCmd, INIRead("BLOCKED_COMMANDS", sCmd, "0", sIniPath) = "1")
END
END

// Botão Salvar
PROCEDURE BTN_Save_Click()
sIniPath is string = fExeDir() + "\pgsql_security.ini"

// Salvar configurações gerais
INIWrite("SECURITY", "EnableBlocking", EDT_EnableBlocking, sIniPath)
INIWrite("SECURITY", "BlockMode", COMBO_BlockMode - 1, sIniPath)
INIWrite("SECURITY", "LogFile", EDT_LogFile, sIniPath)

// Salvar comandos bloqueados
FOR nRow = 1 TO TABLE_Commands.Count
sCommand is string = TABLE_Commands[nRow].COL_Command
bBlocked is boolean = TABLE_Commands[nRow].COL_Blocked
INIWrite("BLOCKED_COMMANDS", sCommand, bBlocked ? "1" : "0", sIniPath)
END

Info("Configurações salvas com sucesso!")
END
```



## 6. Monitoramento e Análise de Logs

```wlanguage
// Procedure para analisar logs de segurança
PROCEDURE AnalyzeSecurityLogs(sLogFile is string): string
sReport is string
mapCommandCount is associative array of int
mapUserViolations is associative array of int

// Ler arquivo de log
sContent is string = fLoadText(sLogFile)
arrLines is array of string
StringToArray(sContent, arrLines, CR)

// Analisar cada linha
FOR EACH sLine OF arrLines
arrFields is array of string
StringToArray(sLine, arrFields, TAB)

IF Count(arrFields) >= 5 THEN
sCommand is string = arrFields[4]
sUser is string = arrFields[2]

// Contar por comando
IF mapCommandCount[sCommand] = 0 THEN
mapCommandCount[sCommand] = 0
END
mapCommandCount[sCommand]++

// Contar por usuário
IF mapUserViolations[sUser] = 0 THEN
mapUserViolations[sUser] = 0
END
mapUserViolations[sUser]++
END
END

// Gerar relatório
sReport = "=== RELATÓRIO DE SEGURANÇA ===" + CR + CR
sReport += "Comandos bloqueados:" + CR
FOR EACH sCmd, nCount OF mapCommandCount
sReport += StringBuild(" %1: %2 tentativas", sCmd, nCount) + CR
END

sReport += CR + "Violações por usuário:" + CR
FOR EACH sUser, nCount OF mapUserViolations
sReport += StringBuild(" %1: %2 tentativas", sUser, nCount) + CR
END

RETURN sReport
END
```



**Principais características desta solução:**

1. **Flexibilidade**: Configuração via arquivo INI permite ajustes sem recompilar
2. **Whitelist**: Permite exceções para queries específicas usando hash MD5
3. **Modos de operação**: Bloquear, avisar ou apenas registrar
4. **Exceções**: Por usuário, IP ou aplicação
5. **Logging completo**: Registra todas as tentativas bloqueadas
6. **Fácil manutenção**: Interface para gerenciar configurações

A solução intercepta queries antes de enviá-las ao PostgreSQL, garantindo que comandos perigosos sejam bloqueados conforme configurado no arquivo INI.

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,520 messages
Posted on June, 11 2025 - 3:27 PM
Outra forma para desativar esses comandos SQL perigosos, você pode usar diferentes estratégias dependendo do SGBD e do contexto.

Vou mostrar as principais abordagens:

## 1. Controle via Permissões do Banco de Dados

### MySQL/MariaDB
```sql
-- Criar usuário com permissões limitadas (apenas SELECT)
CREATE USER 'usuario_leitura'@'localhost' IDENTIFIED BY 'senha';
GRANT SELECT ON database.* TO 'usuario_leitura'@'localhost';

-- Revogar permissões específicas de um usuário existente
REVOKE DELETE, DROP, ALTER, UPDATE, INSERT, CREATE, GRANT OPTION ON database.* FROM 'usuario'@'localhost';
```




### SQL Server
```sql
-- Criar role com permissões limitadas
CREATE ROLE db_readonly;
GRANT SELECT ON SCHEMA::dbo TO db_readonly;

-- Negar permissões específicas
DENY DELETE, UPDATE, INSERT, ALTER TO [usuario];
DENY DROP TO [usuario];
```



### PostgreSQL
```sql
-- Revogar permissões
REVOKE DELETE, TRUNCATE, DROP, ALTER, UPDATE, INSERT ON ALL TABLES IN SCHEMA public FROM usuario;
REVOKE CREATE ON SCHEMA public FROM usuario;
```




## 2. Implementação no WinDev/WebDev
### Criar Camada de Validação
```wlanguage
// Procedure para validar queries antes de executar
PROCEDURE ValidateSQL(sQuery is string): boolean
// Lista de comandos proibidos
arrForbiddenCommands is array of strings = ["DELETE", "TRUNCATE", "DROP", "ALTER", "UPDATE", "GRANT", "REVOKE", "INSERT"]

// Converter query para maiúsculas para comparação
sQueryUpper is string = Upper(NoSpace(sQuery))

// Verificar cada comando proibido
FOR EACH sCommand OF arrForbiddenCommands
IF Position(sQueryUpper, sCommand) > 0 THEN
Error("Comando SQL não permitido: " + sCommand)
RETURN False
END
END

RETURN True
```




### Wrapper Seguro para Execução SQL
```wlanguage
// Procedure segura para executar queries
PROCEDURE ExecuteSafeSQL(sQuery is string, sConnection is string = ""): boolean
// Validar query primeiro
IF NOT ValidateSQL(sQuery) THEN
RETURN False
END

// Executar apenas se for SELECT
IF Position(Upper(sQuery), "SELECT") = 1 THEN
IF sConnection = "" THEN
RETURN HExecuteSQLQuery(dsResult, sQuery)
ELSE


RETURN HExecuteSQLQuery(dsResult, sConnection, hQueryWithoutCorrection, sQuery)
END
ELSE
Error("Apenas consultas SELECT são permitidas")
RETURN False
END
```
[/code]



## 3. Configuração de Conexão Restrita no WinDev
```wlanguage
// Criar conexão com usuário limitado
MyConnection is Connection
MyConnection.Provider = hAccessHFClientServer
MyConnection.Server = "servidor:4900"
MyConnection.Database = "MinhaBase"
MyConnection.User = "usuario_leitura" // Usuário com apenas SELECT
MyConnection.Password = "senha"

// Usar sempre esta conexão para operações de usuários
IF NOT HOpenConnection(MyConnection) THEN
Error("Erro na conexão")
ELSE
HChangeConnection("*", MyConnection)
END
```



## 4. Usar Views e Stored Procedures
### Criar Views para Leitura
```sql
-- Criar views que os usuários podem acessar
CREATE VIEW vw_ClientesPublicos AS
SELECT ID, Nome, Email FROM Clientes WHERE Ativo = 1;

-- Dar permissão apenas nas views
GRANT SELECT ON vw_ClientesPublicos TO usuario_leitura;
```

### Stored Procedures Controladas
```wlanguage
// No WinDev, criar procedures servidor HFSQL
PROCEDURE SERVIDOR GetClientData(nIDClient): string
// Aqui você controla exatamente o que pode ser feito
sResult is string

// Apenas leitura permitida
HReadSeekFirst(Cliente, IDCliente, nIDClient)
IF HFound(Cliente) THEN
sResult = Cliente.Nome + TAB + Cliente.Email
END

RETURN sResult
```




## 5. Implementar Auditoria e Logs
```wlanguage
// Procedure para logar tentativas de comandos proibidos
PROCEDURE LogSecurityViolation(sUser is string, sCommand is string)
LogFile is Log
LogFile.NomeFicheiro = "security_violations.log"
LogFile.Separador = TAB

sMessage is string = DateToString(Today()) + TAB + TimeToString(Now()) + TAB
sMessage += sUser + TAB + "Tentativa de executar: " + sCommand

LogFile.Adiciona(sMessage)
```




## 6. Configuração Completa de Segurança
```wlanguage
// Classe para gerenciar SQL seguro
CLASS SecureDatabase
PRIVATE
m_bAllowWrite is boolean = False
m_sCurrentUser is string

PUBLIC
// Constructor
CONSTRUCTOR(sUser is string)
m_sCurrentUser = sUser
// Verificar permissões do usuário
m_bAllowWrite = CheckUserPermissions(sUser)
END

// Executar query com validação
PROCEDURE ExecuteQuery(sQuery is string): boolean
// Remover espaços e converter para maiúsculas
sCleanQuery is string = Upper(Trim(sQuery))

// Lista de comandos de escrita
IF NOT m_bAllowWrite THEN
arrWriteCommands is array of strings = [
"INSERT", "UPDATE", "DELETE", "TRUNCATE",
"DROP", "ALTER", "CREATE", "GRANT", "REVOKE"
]

FOR EACH sCmd OF arrWriteCommands
IF Position(sCleanQuery, sCmd) > 0 THEN
LogSecurityViolation(m_sCurrentUser, sQuery)
Error(StringBuild("Usuário %1 não tem permissão para executar %2",
m_sCurrentUser, sCmd))
RETURN False
END
END
END

// Executar query
RETURN HExecuteSQLQuery(dsResult, sQuery)
END
END
```




## Exemplo de Uso Prático
```wlanguage
// No início da aplicação
MyDB is SecureDatabase("usuario_atual")

// Tentar executar comandos
IF NOT MyDB.ExecuteQuery("SELECT * FROM Clientes") THEN
// Erro será mostrado
END

// Este comando será bloqueado
IF NOT MyDB.ExecuteQuery("DELETE FROM Clientes WHERE ID = 1") THEN
// Mensagem: "Usuário não tem permissão para executar DELETE"
END
```



**Recomendações Finais:**
1. Sempre use o princípio do menor privilégio
2. Crie usuários diferentes para leitura e escrita
3. Implemente validação em múltiplas camadas
4. Mantenha logs de todas as tentativas bloqueadas
5. Use conexões SSL/TLS para o banco de dados
6. Realize auditorias regulares de segurança

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,520 messages
Posted on June, 11 2025 - 3:52 PM
A seguir analisei a estrutura do PostgreSQL e identifiquei os pontos de modificação feitos por esses comandos e estou sugerindo implementar o bloqueio via DLL CUSTOMIZADA DO POSTGRESQL com suporte a múltiplos bancos de dados.

Espero que essa contribuição venha a servir para os senhores:

## 1. Arquivos-chave do PostgreSQL para Modificação

### Arquivos Principais a Modificar:
```c
// src/interfaces/libpq/fe-exec.c
// Este arquivo contém as funções principais de execução de queries
PGresult *PQexec(PGconn *conn, const char *query)
PGresult *PQexecParams(PGconn *conn, const char *command, ...)
PGresult *PQexecPrepared(PGconn *conn, const char *stmtName, ...)

// src/interfaces/libpq/libpq-fe.h
// Header principal da libpq - interface cliente

// src/backend/tcop/postgres.c
// Loop principal de processamento de comandos
static void exec_simple_query(const char *query_string)
```




## 2. Estrutura do Arquivo INI Estendido
```ini
[GLOBAL]
; Configurações globais
EnableSecurity=1
DefaultMode=0
LogPath=C:\PostgreSQL\logs\security\
; DLL de segurança customizada
SecurityDLL=pgsql_security.dll

[DATABASE:producao]
; Banco de produção - somente leitura
BlockMode=0
AllowedCommands=SELECT
BlockedCommands=DELETE,TRUNCATE,DROP,ALTER,UPDATE,GRANT,REVOKE,INSERT
LogFile=producao_security.log

[DATABASE:desenvolvimento]
; Banco de desenvolvimento - acesso completo
BlockMode=2
AllowedCommands=ALL
LogFile=dev_security.log

[DATABASE:relatorios]
; Banco de relatórios - leitura + views
BlockMode=0
AllowedCommands=SELECT,CREATE VIEW,DROP VIEW
BlockedCommands=DELETE,TRUNCATE,DROP TABLE,ALTER,UPDATE,INSERT
LogFile=relatorios_security.log

[WHITELIST:producao]
; Queries permitidas por hash MD5 para o banco producao
Query1=5d41402abc4b2a76b9719d911017c592
Query1_Desc=Update de status de pedidos pelo sistema

[EXCEPTIONS:producao]
AdminUsers=postgres,dba_master
TrustedIPs=192.168.1.10,192.168.1.11
TrustedApps=maintenance.exe,backup_tool.exe
```




## 3. DLL de Interceptação (pgsql_security.dll)
```c
// pgsql_security.h
#ifndef PGSQL_SECURITY_H
#define PGSQL_SECURITY_H

#include <windows.h>
#include <stdbool.h>

// Estrutura de configuração por banco
typedef struct DatabaseConfig {
char dbname[128];
int block_mode; // 0=Block, 1=Warn, 2=Log only
char allowed_commands[1024];
char blocked_commands[1024];
char log_file[256];
struct DatabaseConfig *next;
} DatabaseConfig;

// Estrutura principal de segurança
typedef struct SecurityContext {
bool enabled;
char ini_path[256];
char log_path[256];
DatabaseConfig *db_configs;
CRITICAL_SECTION cs;
} SecurityContext;

// Funções exportadas
__declspec(dllexport) bool InitializeSecurity(const char *ini_path);
__declspec(dllexport) bool ValidateQuery(const char *dbname, const char *query, const char *user, const char *ip);
__declspec(dllexport) void CleanupSecurity(void);
__declspec(dllexport) void ReloadConfiguration(void);

#endif
```




```c
// pgsql_security.c
#include "pgsql_security.h"
#include <stdio.h>
#include <string.h>
#include <time.h>

static SecurityContext g_security = {0};

// Função para carregar configuração do INI
static void LoadINIConfiguration(const char *ini_path) {
char section[256];
char key[256];
char value[1024];

// Carregar configurações globais
GetPrivateProfileString("GLOBAL", "EnableSecurity", "1", value, sizeof(value), ini_path);
g_security.enabled = (atoi(value) == 1);

GetPrivateProfileString("GLOBAL", "LogPath", "C:\\PostgreSQL\\logs\\",
g_security.log_path, sizeof(g_security.log_path), ini_path);

// Limpar configurações antigas
DatabaseConfig *current = g_security.db_configs;
while (current) {
DatabaseConfig *next = current->next;
free(current);
current = next;
}
g_security.db_configs = NULL;

// Carregar seções de banco de dados
char sections[8192];
GetPrivateProfileSectionNames(sections, sizeof(sections), ini_path);

char *section_ptr = sections;
while (*section_ptr) {
if (strncmp(section_ptr, "DATABASE:", 9) == 0) {
// Extrair nome do banco
char *dbname = section_ptr + 9;

// Criar nova configuração
DatabaseConfig *config = (DatabaseConfig*)calloc(1, sizeof(DatabaseConfig));
strcpy(config->dbname, dbname);

// Carregar configurações do banco
sprintf(section, "DATABASE:%s", dbname);

GetPrivateProfileString(section, "BlockMode", "0", value, sizeof(value), ini_path);
config->block_mode = atoi(value);

GetPrivateProfileString(section, "AllowedCommands", "",
config->allowed_commands, sizeof(config->allowed_commands), ini_path);

GetPrivateProfileString(section, "BlockedCommands", "",
config->blocked_commands, sizeof(config->blocked_commands), ini_path);

GetPrivateProfileString(section, "LogFile", "security.log",
config->log_file, sizeof(config->log_file), ini_path);

// Adicionar à lista
config->next = g_security.db_configs;
g_security.db_configs = config;
}

section_ptr += strlen(section_ptr) + 1;
}
}

// Inicializar sistema de segurança
bool InitializeSecurity(const char *ini_path) {
InitializeCriticalSection(&g_security.cs);

if (ini_path) {
strcpy(g_security.ini_path, ini_path);
} else {
// Caminho padrão
GetModuleFileName(NULL, g_security.ini_path, sizeof(g_security.ini_path));
char *last_slash = strrchr(g_security.ini_path, '\\');
if (last_slash) {
strcpy(last_slash + 1, "pgsql_security.ini");
}
}

LoadINIConfiguration(g_security.ini_path);
return g_security.enabled;
}

// Encontrar configuração para um banco específico
static DatabaseConfig* FindDatabaseConfig(const char *dbname) {
DatabaseConfig *current = g_security.db_configs;
while (current) {
if (strcmp(current->dbname, dbname) == 0) {
return current;
}
current = current->next;
}
return NULL;
}

// Verificar se comando está na lista
static bool IsCommandInList(const char *command, const char *command_list) {
if (strcmp(command_list, "ALL") == 0) return true;

char list_copy[1024];
strcpy(list_copy, command_list);

char *token = strtok(list_copy, ",");
while (token) {
// Remover espaços
while (*token == ' ') token++;
char *end = token + strlen(token) - 1;
while (end > token && *end == ' ') *end-- = '\0';

if (strcasecmp(token, command) == 0) {
return true;
}
token = strtok(NULL, ",");
}

return false;
}

// Extrair comando principal da query
static void ExtractCommand(const char *query, char *command, size_t cmd_size) {
// Pular espaços iniciais
while (*query && isspace(*query)) query++;

// Copiar até encontrar espaço ou fim
size_t i = 0;
while (*query && !isspace(*query) && i < cmd_size - 1) {
command[i++] = toupper(*query++);
}
command[i] = '\0';
}

// Registrar no log
static void LogSecurityEvent(const char *dbname, const char *query, const char *user,
const char *ip, const char *action, const char *log_file) {
EnterCriticalSection(&g_security.cs);

char full_path[512];
sprintf(full_path, "%s%s", g_security.log_path, log_file);

FILE *fp = fopen(full_path, "a");
if (fp) {
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
char timestamp[64];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);

fprintf(fp, "%s|%s|%s|%s|%s|%.200s\n",
timestamp, dbname, user, ip, action, query);
fclose(fp);
}

LeaveCriticalSection(&g_security.cs);
}

// Validar query
bool ValidateQuery(const char *dbname, const char *query, const char *user, const char *ip) {
if (!g_security.enabled) return true;

EnterCriticalSection(&g_security.cs);

// Encontrar configuração do banco
DatabaseConfig *config = FindDatabaseConfig(dbname);
if (!config) {
// Se não há configuração específica, permitir
LeaveCriticalSection(&g_security.cs);
return true;
}

// Extrair comando
char command[64];
ExtractCommand(query, command, sizeof(command));

bool allowed = true;
char action[64] = "ALLOWED";

// Verificar se comando está bloqueado
if (strlen(config->blocked_commands) > 0) {
if (IsCommandInList(command, config->blocked_commands)) {
switch (config->block_mode) {
case 0: // Block
allowed = false;
strcpy(action, "BLOCKED");
break;
case 1: // Warn
strcpy(action, "WARNING");
break;
case 2: // Log only
strcpy(action, "LOGGED");
break;
}
}
}

// Verificar se comando está permitido (override blocked)
if (strlen(config->allowed_commands) > 0) {
if (!IsCommandInList(command, config->allowed_commands)) {
if (config->block_mode == 0) {
allowed = false;
strcpy(action, "BLOCKED");
}
}
}

// Registrar evento
if (!allowed || config->block_mode > 0) {
LogSecurityEvent(dbname, query, user, ip, action, config->log_file);
}

LeaveCriticalSection(&g_security.cs);

return allowed;
}

// Limpar recursos
void CleanupSecurity(void) {
EnterCriticalSection(&g_security.cs);

DatabaseConfig *current = g_security.db_configs;
while (current) {
DatabaseConfig *next = current->next;
free(current);
current = next;
}
g_security.db_configs = NULL;

LeaveCriticalSection(&g_security.cs);
DeleteCriticalSection(&g_security.cs);
}

// Recarregar configuração
void ReloadConfiguration(void) {
EnterCriticalSection(&g_security.cs);
LoadINIConfiguration(g_security.ini_path);
LeaveCriticalSection(&g_security.cs);
}
```





## 4. Modificação do libpq (fe-exec.c)
```c
// Adicionar no início do arquivo fe-exec.c
#include "pgsql_security.h"

typedef bool (*ValidateQueryFunc)(const char*, const char*, const char*, const char*);
static ValidateQueryFunc g_validateQuery = NULL;
static HMODULE g_securityDll = NULL;

// Função para carregar DLL de segurança
static void LoadSecurityDLL() {
if (g_securityDll) return;

char dll_path[MAX_PATH];
GetModuleFileName(NULL, dll_path, sizeof(dll_path));
char *last_slash = strrchr(dll_path, '\\');
if (last_slash) {
strcpy(last_slash + 1, "pgsql_security.dll");
}

g_securityDll = LoadLibrary(dll_path);
if (g_securityDll) {
g_validateQuery = (ValidateQueryFunc)GetProcAddress(g_securityDll, "ValidateQuery");

// Inicializar segurança
typedef bool (*InitFunc)(const char*);
InitFunc initSecurity = (InitFunc)GetProcAddress(g_securityDll, "InitializeSecurity");
if (initSecurity) {
initSecurity(NULL); // Usa caminho padrão
}
}
}

// Modificar PQexec para incluir validação
PGresult *
PQexec(PGconn *conn, const char *query)
{
// Carregar DLL na primeira vez
static int dll_loaded = 0;
if (!dll_loaded) {
LoadSecurityDLL();
dll_loaded = 1;
}

// Validar query se DLL está carregada
if (g_validateQuery && conn && conn->dbName) {
char client_ip[64] = "";
if (conn->pghostaddr) {
strcpy(client_ip, conn->pghostaddr);
}

if (!g_validateQuery(conn->dbName, query, conn->pguser, client_ip)) {
// Criar resultado de erro
PGresult *res = PQmakeEmptyPGresult(conn, PGRES_FATAL_ERROR);
if (res) {
res->errMsg = strdup("Query blocked by security policy");
}
return res;
}
}

// Continuar com execução normal
if (!PQexecStart(conn))
return NULL;
if (!PQsendQuery(conn, query))
return NULL;
return PQexecFinish(conn);
}
```




## 5. Script de Build para Windows
```batch
@echo off
REM build_security_dll.bat

set PGSQL_SRC=C:\postgresql-source
set PGSQL_INSTALL=C:\Program Files\PostgreSQL\14

REM Compilar DLL de segurança
cl /LD /MT /I"%PGSQL_SRC%\src\include" ^
/I"%PGSQL_SRC%\src\interfaces\libpq" ^
pgsql_security.c ^
/link /DEF:pgsql_security.def ^
/OUT:pgsql_security.dll

REM Copiar para diretório do PostgreSQL
copy pgsql_security.dll "%PGSQL_INSTALL%\bin\"
copy pgsql_security.ini "%PGSQL_INSTALL%\bin\"

echo Build concluído!
```




## 6. Arquivo DEF para exportação
```def
; pgsql_security.def
LIBRARY pgsql_security
EXPORTS
InitializeSecurity
ValidateQuery
CleanupSecurity
ReloadConfiguration
```



## 7. Classe WinDev para Gerenciar Múltiplos Bancos
```wlanguage
// Classe para gerenciar segurança multi-banco
CLASS PostgreSQLMultiDBSecurity
PRIVATE
m_sIniPath is string
m_mapDatabaseConfigs is associative array of DatabaseSecurityConfig

PUBLIC
// Constructor
CONSTRUCTOR(sIniPath is string = "")
IF sIniPath = "" THEN
m_sIniPath = CompleteDir(fExeDir()) + "pgsql_security.ini"
ELSE
m_sIniPath = sIniPath
END

LoadAllConfigurations()
END

// Carregar todas as configurações
PROCEDURE PRIVATE LoadAllConfigurations()
// Obter todas as seções DATABASE:*
sSections is string = INISectionList(m_sIniPath)
arrSections is array of string
StringToArray(sSections, arrSections, CR)

FOR EACH sSection OF arrSections
IF sSection.StartsWith("DATABASE:") THEN
sDBName is string = sSection[[10 TO]]
LoadDatabaseConfig(sDBName)
END
END
END

// Carregar configuração de um banco específico
PROCEDURE PRIVATE LoadDatabaseConfig(sDBName is string)
config is DatabaseSecurityConfig
config.DatabaseName = sDBName

sSection is string = "DATABASE:" + sDBName
config.BlockMode = Val(INIRead(sSection, "BlockMode", "0", m_sIniPath))
config.AllowedCommands = INIRead(sSection, "AllowedCommands", "", m_sIniPath)
config.BlockedCommands = INIRead(sSection, "BlockedCommands", "", m_sIniPath)
config.LogFile = INIRead(sSection, "LogFile", sDBName + "_security.log", m_sIniPath)

m_mapDatabaseConfigs[sDBName] = config
END

// Adicionar nova configuração de banco
PROCEDURE AddDatabaseConfig(sDBName is string, nMode is int = 0,
sAllowed is string = "SELECT",
sBlocked is string = "DELETE,UPDATE,INSERT,DROP,ALTER,TRUNCATE")
sSection is string = "DATABASE:" + sDBName

INIWrite(sSection, "BlockMode", nMode, m_sIniPath)
INIWrite(sSection, "AllowedCommands", sAllowed, m_sIniPath)
INIWrite(sSection, "BlockedCommands", sBlocked, m_sIniPath)
INIWrite(sSection, "LogFile", sDBName + "_security.log", m_sIniPath)

// Recarregar
LoadDatabaseConfig(sDBName)
END

// Obter configuração de um banco
PROCEDURE GetDatabaseConfig(sDBName is string): DatabaseSecurityConfig
IF m_mapDatabaseConfigs[sDBName]..Empty THEN
RESULT DatabaseSecurityConfig
END
RESULT m_mapDatabaseConfigs[sDBName]
END

// Listar todos os bancos configurados
PROCEDURE ListConfiguredDatabases(): array of string
arrDatabases is array of string
FOR EACH config, sDBName OF m_mapDatabaseConfigs
arrDatabases.Add(sDBName)
END
RESULT arrDatabases
END

// Gerar relatório de configuração
PROCEDURE GenerateConfigReport(): string
sReport is string = "=== CONFIGURAÇÃO DE SEGURANÇA POSTGRESQL ===" + CR + CR

FOR EACH config, sDBName OF m_mapDatabaseConfigs
sReport += "Banco: " + sDBName + CR
sReport += " Modo: "
SWITCH config.BlockMode
CASE 0: sReport += "Bloquear"
CASE 1: sReport += "Avisar"
CASE 2: sReport += "Log apenas"
END
sReport += CR
sReport += " Comandos permitidos: " + config.AllowedCommands + CR
sReport += " Comandos bloqueados: " + config.BlockedCommands + CR
sReport += " Arquivo de log: " + config.LogFile + CR + CR
END

RESULT sReport
END
END

// Estrutura de configuração
STRUCTURE DatabaseSecurityConfig
DatabaseName is string
BlockMode is int
AllowedCommands is string
BlockedCommands is string
LogFile is string
END
```




## 8. Interface de Configuração Multi-Banco
```wlanguage
// Window de configuração multi-banco
PROCEDURE WIN_MultiDBSecurity()
// Carregar gerenciador
gclSecurityManager is PostgreSQLMultiDBSecurity()

// Preencher tabela de bancos
TABLE_Databases.DeleteAll()
arrDatabases is array of string = gclSecurityManager.ListConfiguredDatabases()

FOR EACH sDB OF arrDatabases
config is DatabaseSecurityConfig = gclSecurityManager.GetDatabaseConfig(sDB)
TABLE_Databases.AddLine(sDB, config.BlockMode, config.AllowedCommands)
END
END

// Adicionar novo banco
PROCEDURE BTN_AddDatabase_Click()
sDBName is string = Input("Nome do banco de dados:")
IF sDBName = "" THEN RETURN

// Configuração padrão para novo banco
gclSecurityManager.AddDatabaseConfig(sDBName)

// Atualizar interface
WIN_MultiDBSecurity()
END

// Editar configuração selecionada
PROCEDURE BTN_EditDatabase_Click()
IF TABLE_Databases..Empty THEN RETURN

sDBName is string = TABLE_Databases.COL_Database
config is DatabaseSecurityConfig = gclSecurityManager.GetDatabaseConfig(sDBName)

// Abrir janela de edição
Open(WIN_EditDBConfig, config)

// Atualizar após edição
WIN_MultiDBSecurity()
END
```




**Principais vantagens desta implementação:**

1. **Modular**: DLL separada facilita atualizações sem recompilar PostgreSQL
2. **Multi-banco**: Cada banco pode ter suas próprias regras
3. **Performance**: Validação rápida em memória
4. **Flexível**: Fácil adicionar novos bancos via INI
5. **Compatível**: Funciona com libpq existente
6. **Auditoria**: Log separado por banco

A solução intercepta queries no nível da libpq, permitindo controle granular por banco de dados sem modificar o servidor PostgreSQL.

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,520 messages
Posted on June, 11 2025 - 3:58 PM
# Documentação Completa: Bloqueio de Comandos SQL PostgreSQL via DLL Customizada

## 1. Visão Geral do Projeto

### Objetivo
Implementar um sistema de segurança que intercepta e valida comandos SQL antes de serem executados no PostgreSQL, usando uma DLL customizada que lê configurações de um arquivo INI, permitindo diferentes níveis de acesso por banco de dados.

### Arquitetura da Solução

```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Aplicação │────▶│ libpq.dll │────▶│ PostgreSQL │
│ (WinDev) │ │ (modificada) │ │ Server │
└─────────────────┘ └────────┬─────────┘ └─────────────────┘


┌──────────────────┐
│ pgsql_security │
│ .dll │
└────────┬─────────┘


┌──────────────────┐
│ pgsql_security │
│ .ini │
└──────────────────┘
```

## 2. Ferramentas e Ambiente Necessários

### 2.1 Compiladores e IDEs

#### Visual Studio 2019/2022 (Recomendado)
```
- Download: https://visualstudio.microsoft.com/
- Workload necessário: "Desenvolvimento para desktop com C++"
- Componentes individuais:
* MSVC v142/v143 - Build Tools C++
* Windows 10 SDK
* CMake tools for Windows
```

#### MinGW-w64 (Alternativa)
```
- Download: https://www.mingw-w64.org/
- Versão recomendada: MinGW-W64 GCC-8.1.0
- Arquitetura: x86_64-posix-seh
```

### 2.2 PostgreSQL Source Code
```
- Download: https://www.postgresql.org/ftp/source/
- Versão: PostgreSQL 14.x ou 15.x
- Extrair em: C:\postgresql-source
```

### 2.3 Ferramentas Auxiliares

#### Dependency Walker
```
- Para analisar dependências de DLL
- Download: http://www.dependencywalker.com/
```

#### Process Monitor
```
- Para monitorar acesso a arquivos/DLLs
- Download: Microsoft Sysinternals
```

## 3. Preparação do Ambiente

### 3.1 Estrutura de Diretórios
```
C:\PostgreSQL-Security\

├── source\ # Código fonte
│ ├── pgsql_security.h
│ ├── pgsql_security.c
│ ├── pgsql_security.def
│ └── libpq_patch.c

├── build\ # Arquivos compilados
│ ├── pgsql_security.dll
│ ├── pgsql_security.lib
│ └── libpq.dll (modificada)

├── config\ # Configurações
│ └── pgsql_security.ini

├── tools\ # Scripts e ferramentas
│ ├── build.bat
│ ├── install.bat
│ └── test.bat

└── docs\ # Documentação
└── README.md
```

### 3.2 Configurar Variáveis de Ambiente

```batch
@echo off
REM setup_env.bat - Configurar ambiente de compilação

set PGSQL_SRC=C:\postgresql-source
set PGSQL_INSTALL=C:\Program Files\PostgreSQL\14
set PROJECT_ROOT=C:\PostgreSQL-Security

REM Visual Studio 2022
call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"

REM Adicionar PostgreSQL ao PATH
set PATH=%PGSQL_INSTALL%\bin;%PATH%
```

## 4. Implementação Detalhada

### 4.1 Código da DLL de Segurança

#### pgsql_security.h (Estendido)
```c
#ifndef PGSQL_SECURITY_H
#define PGSQL_SECURITY_H

#ifdef _WIN32
#ifdef PGSQL_SECURITY_EXPORTS
#define PGSQL_API __declspec(dllexport)
#else
#define PGSQL_API __declspec(dllimport)
#endif
#else
#define PGSQL_API
#endif

#include <windows.h>
#include <stdbool.h>
#include <stdint.h>

// Versão da DLL
#define PGSQL_SECURITY_VERSION "1.0.0"

// Modos de bloqueio
typedef enum {
BLOCK_MODE_DENY = 0, // Bloqueia execução
BLOCK_MODE_WARN = 1, // Permite mas avisa
BLOCK_MODE_LOG_ONLY = 2 // Apenas registra
} BlockMode;

// Estrutura de configuração por banco
typedef struct DatabaseConfig {
char dbname[128];
BlockMode block_mode;
char allowed_commands[1024];
char blocked_commands[1024];
char log_file[256];
bool use_whitelist;
struct DatabaseConfig *next;
} DatabaseConfig;

// Contexto de segurança global
typedef struct SecurityContext {
bool enabled;
char ini_path[MAX_PATH];
char log_path[MAX_PATH];
DatabaseConfig *db_configs;
CRITICAL_SECTION cs;
HANDLE hLogMutex;
uint32_t query_count;
uint32_t blocked_count;
} SecurityContext;

// API Pública
PGSQL_API bool InitializeSecurity(const char *ini_path);
PGSQL_API bool ValidateQuery(const char *dbname, const char *query, const char *user, const char *ip);
PGSQL_API void CleanupSecurity(void);
PGSQL_API void ReloadConfiguration(void);
PGSQL_API const char* GetSecurityVersion(void);
PGSQL_API bool GetSecurityStats(uint32_t *total_queries, uint32_t *blocked_queries);

#endif // PGSQL_SECURITY_H
```

### 4.2 Compilação com Visual Studio

#### build_vs2022.bat
```batch
@echo off
REM Script de compilação usando Visual Studio 2022
echo ========================================
echo Compilando PostgreSQL Security DLL
echo ========================================

REM Configurar ambiente
call setup_env.bat

cd /d %PROJECT_ROOT%\source

REM Limpar build anterior
if exist ..\build\*.* del /Q ..\build\*.*

REM Compilar DLL de segurança
echo.
echo Compilando pgsql_security.dll...
cl /nologo /MT /O2 /W3 /D_WINDOWS /DPGSQL_SECURITY_EXPORTS ^
/I"%PGSQL_SRC%\src\include" ^
/I"%PGSQL_SRC%\src\interfaces\libpq" ^
/I"%PGSQL_SRC%\src\port" ^
/c pgsql_security.c

REM Linkar DLL
link /nologo /DLL /DEF:pgsql_security.def ^
/OUT:..\build\pgsql_security.dll ^
/IMPLIB:..\build\pgsql_security.lib ^
pgsql_security.obj ^
kernel32.lib user32.lib advapi32.lib

REM Compilar libpq modificada
echo.
echo Compilando libpq modificada...
cd %PGSQL_SRC%\src\interfaces\libpq

REM Aplicar patch
copy /Y %PROJECT_ROOT%\source\libpq_patch.c fe-exec.c

REM Compilar
nmake /f win32.mak clean
nmake /f win32.mak

REM Copiar resultado
copy /Y Release\libpq.dll %PROJECT_ROOT%\build\

echo.
echo Compilação concluída!
echo Arquivos gerados em: %PROJECT_ROOT%\build\
pause
```

### 4.3 Compilação com MinGW

#### build_mingw.bat
```batch
@echo off
REM Script de compilação usando MinGW
echo ========================================
echo Compilando com MinGW-w64
echo ========================================

set PATH=C:\mingw64\bin;%PATH%
set CC=gcc
set PROJECT_ROOT=C:\PostgreSQL-Security

cd /d %PROJECT_ROOT%\source

REM Compilar DLL
echo Compilando pgsql_security.dll...
%CC% -shared -O2 -Wall -DPGSQL_SECURITY_EXPORTS ^
-I"%PGSQL_SRC%\src\include" ^
-I"%PGSQL_SRC%\src\interfaces\libpq" ^
pgsql_security.c ^
-o ..\build\pgsql_security.dll ^
-Wl,--out-implib,..\build\libpgsql_security.a ^
-lkernel32 -luser32 -ladvapi32

echo Compilação concluída!
pause
```

## 5. Modificação da libpq

### 5.1 Patch para fe-exec.c

```c
// libpq_patch.c - Adicionar ao início de fe-exec.c
#ifdef _WIN32
#include <windows.h>

// Tipos de função da DLL de segurança
typedef bool (*PFN_InitializeSecurity)(const char*);
typedef bool (*PFN_ValidateQuery)(const char*, const char*, const char*, const char*);
typedef void (*PFN_CleanupSecurity)(void);

// Variáveis globais para DLL
static HMODULE g_hSecurityDll = NULL;
static PFN_ValidateQuery g_pfnValidateQuery = NULL;
static bool g_bSecurityInitialized = false;

// Carregar DLL de segurança
static void LoadSecurityDLL(void)
{
if (g_bSecurityInitialized) return;

char szDllPath[MAX_PATH];
GetModuleFileNameA(NULL, szDllPath, MAX_PATH);

char *pLastSlash = strrchr(szDllPath, '\\');
if (pLastSlash) {
strcpy(pLastSlash + 1, "pgsql_security.dll");
}

// Tentar carregar DLL
g_hSecurityDll = LoadLibraryA(szDllPath);
if (g_hSecurityDll) {
PFN_InitializeSecurity pfnInit =
(PFN_InitializeSecurity)GetProcAddress(g_hSecurityDll, "InitializeSecurity");
g_pfnValidateQuery =
(PFN_ValidateQuery)GetProcAddress(g_hSecurityDll, "ValidateQuery");

if (pfnInit && g_pfnValidateQuery) {
// Inicializar com INI no mesmo diretório
strcpy(strrchr(szDllPath, '\\') + 1, "pgsql_security.ini");
pfnInit(szDllPath);
}
}

g_bSecurityInitialized = true;
}

// Função auxiliar para validar query
static bool ValidateQuerySecurity(PGconn *conn, const char *query)
{
LoadSecurityDLL();

if (g_pfnValidateQuery && conn && conn->dbName) {
const char *user = conn->pguser ? conn->pguser : "unknown";
const char *ip = conn->pghostaddr ? conn->pghostaddr : "127.0.0.1";

return g_pfnValidateQuery(conn->dbName, query, user, ip);
}

return true; // Permitir se DLL não carregada
}
#endif

// Modificar função PQexec original
PGresult *
PQexec(PGconn *conn, const char *query)
{
if (!PQexecStart(conn))
return NULL;

#ifdef _WIN32
// Validar query antes de enviar
if (!ValidateQuerySecurity(conn, query)) {
PGresult *res = PQmakeEmptyPGresult(conn, PGRES_FATAL_ERROR);
if (res) {
res->errMsg = strdup("ERROR: Query blocked by security policy");
res->errQuery = strdup(query);
}
return res;
}
#endif

if (!PQsendQuery(conn, query))
return NULL;

return PQexecFinish(conn);
}
```

## 6. Instalação e Configuração

### 6.1 Script de Instalação

#### install.bat
```batch
@echo off
REM Script de instalação
echo ========================================
echo Instalando PostgreSQL Security
echo ========================================

set PGSQL_BIN=C:\Program Files\PostgreSQL\14\bin
set BUILD_DIR=C:\PostgreSQL-Security\build

REM Verificar se PostgreSQL está instalado
if not exist "%PGSQL_BIN%\libpq.dll" (
echo ERRO: PostgreSQL não encontrado em %PGSQL_BIN%
pause
exit /b 1
)

REM Fazer backup da libpq original
echo Fazendo backup da libpq.dll original...
if not exist "%PGSQL_BIN%\libpq.dll.original" (
copy "%PGSQL_BIN%\libpq.dll" "%PGSQL_BIN%\libpq.dll.original"
)

REM Parar serviços que possam estar usando a DLL
echo Parando serviços...
net stop postgresql-x64-14 2>nul

REM Copiar arquivos
echo Copiando arquivos...
copy /Y "%BUILD_DIR%\pgsql_security.dll" "%PGSQL_BIN%\"
copy /Y "%BUILD_DIR%\libpq.dll" "%PGSQL_BIN%\"
copy /Y "..\config\pgsql_security.ini" "%PGSQL_BIN%\"

REM Reiniciar serviços
echo Reiniciando serviços...
net start postgresql-x64-14

echo.
echo Instalação concluída!
echo.
echo IMPORTANTE:
echo 1. Configure o arquivo: %PGSQL_BIN%\pgsql_security.ini
echo 2. Reinicie aplicações que usam PostgreSQL
echo.
pause
```

### 6.2 Arquivo de Configuração Exemplo

#### pgsql_security.ini (Completo)
```ini
; PostgreSQL Security Configuration
; Última atualização: 2024

[GLOBAL]
; Habilitar sistema de segurança (0=Desabilitado, 1=Habilitado)
EnableSecurity=1

; Modo padrão para bancos não configurados (0=Permitir, 1=Bloquear)
DefaultPolicy=0

; Diretório para arquivos de log
LogPath=C:\ProgramData\PostgreSQL\Security\Logs\

; Nível de log (0=Erros, 1=Avisos, 2=Info, 3=Debug)
LogLevel=2

; Tamanho máximo do arquivo de log em MB
MaxLogSize=100

; Número de arquivos de log para manter
LogRotation=10

; ========================================
; Configurações por Banco de Dados
; ========================================

[DATABASE:producao]
; Modo de bloqueio: 0=Bloquear, 1=Avisar, 2=Log apenas
BlockMode=0

; Comandos permitidos (separados por vírgula)
AllowedCommands=SELECT

; Comandos bloqueados (tem prioridade sobre permitidos)
BlockedCommands=DELETE,TRUNCATE,DROP,ALTER,UPDATE,GRANT,REVOKE,INSERT

; Arquivo de log específico
LogFile=producao_security.log

; Usar whitelist de queries (0=Não, 1=Sim)
UseWhitelist=1

[DATABASE:desenvolvimento]
BlockMode=2
AllowedCommands=ALL
LogFile=dev_security.log
UseWhitelist=0

[DATABASE:homologacao]
BlockMode=1
AllowedCommands=SELECT,INSERT,UPDATE
BlockedCommands=DELETE,TRUNCATE,DROP,ALTER,GRANT,REVOKE
LogFile=homolog_security.log
UseWhitelist=0

[DATABASE:relatorios]
BlockMode=0
AllowedCommands=SELECT,CREATE VIEW,DROP VIEW
BlockedCommands=DELETE,TRUNCATE,DROP TABLE,ALTER TABLE,UPDATE,INSERT
LogFile=relatorios_security.log
UseWhitelist=0

; ========================================
; Whitelist - Queries permitidas por hash MD5
; ========================================

[WHITELIST:producao]
; Hash MD5 da query normalizada (sem espaços extras, uppercase)
; Exemplo: UPDATE pedidos SET status = 'processado' WHERE id = $1
Query1=a1b2c3d4e5f6789012345678901234567
Query1_Desc=Atualização de status de pedidos

Query2=b2c3d4e5f6789012345678901234568
Query2_Desc=Inserção de logs de auditoria

[WHITELIST:homologacao]
Query1=c3d4e5f6789012345678901234569
Query1_Desc=Reset de dados de teste

; ========================================
; Exceções - Usuários e IPs com acesso total
; ========================================

[EXCEPTIONS]
; Usuários com permissão total (separados por vírgula)
AdminUsers=postgres,dba_master,backup_user

; IPs com permissão total
TrustedIPs=127.0.0.1,::1,192.168.1.10,192.168.1.11

; Aplicações com permissão total
TrustedApps=pg_dump.exe,pg_restore.exe,pgAdmin4.exe,maintenance.exe

; Horário de manutenção (formato HH:MM-HH:MM)
MaintenanceWindow=02:00-04:00

; Dias da semana para manutenção (0=Dom, 6=Sáb)
MaintenanceDays=0,6
```

## 7. Testes e Validação

### 7.1 Script de Teste

#### test_security.bat
```batch
@echo off
REM Script de teste da DLL de segurança
echo ========================================
echo Testando PostgreSQL Security
echo ========================================

set PGHOST=localhost
set PGPORT=5432
set PGUSER=test_user
set PGPASSWORD=test_pass

REM Teste 1: Query permitida
echo.
echo Teste 1: SELECT (deve funcionar)
psql -d producao -c "SELECT * FROM clientes LIMIT 1"

REM Teste 2: Query bloqueada
echo.
echo Teste 2: DELETE (deve ser bloqueado)
psql -d producao -c "DELETE FROM clientes WHERE id = -1"

REM Teste 3: Banco sem restrições
echo.
echo Teste 3: DELETE em desenvolvimento (deve funcionar)
psql -d desenvolvimento -c "DELETE FROM teste WHERE id = -1"

pause
```

### 7.2 Programa de Teste em C

```c
// test_security.c
#include <stdio.h>
#include <stdlib.h>
#include <libpq-fe.h>

void test_query(PGconn *conn, const char *query) {
printf("\nTestando: %s\n", query);

PGresult *res = PQexec(conn, query);
ExecStatusType status = PQresultStatus(res);

if (status == PGRES_TUPLES_OK || status == PGRES_COMMAND_OK) {
printf("✓ Query executada com sucesso\n");
} else {
printf("✗ Query bloqueada: %s\n", PQerrorMessage(conn));
}

PQclear(res);
}

int main() {
// Conectar ao banco producao
PGconn *conn = PQconnectdb("host=localhost dbname=producao user=test_user");

if (PQstatus(conn) != CONNECTION_OK) {
fprintf(stderr, "Erro de conexão: %s\n", PQerrorMessage(conn));
PQfinish(conn);
return 1;
}

printf("Conectado ao banco: producao\n");

// Testar diferentes queries
test_query(conn, "SELECT version()");
test_query(conn, "SELECT * FROM clientes LIMIT 1");
test_query(conn, "INSERT INTO clientes (nome) VALUES ('Teste')");
test_query(conn, "UPDATE clientes SET nome = 'Teste2' WHERE id = 1");
test_query(conn, "DELETE FROM clientes WHERE id = 1");
test_query(conn, "DROP TABLE teste");

PQfinish(conn);
return 0;
}
```

## 8. Monitoramento e Manutenção

### 8.1 Visualizador de Logs

```c
// log_viewer.c - Utilitário para visualizar logs
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

typedef struct {
char timestamp[20];
char database[64];
char user[64];
char ip[64];
char action[20];
char query[500];
} LogEntry;

void parse_log_line(const char *line, LogEntry *entry) {
sscanf(line, "%19[^|]|%63[^|]|%63[^|]|%63[^|]|%19[^|]|%499[^\n]",
entry->timestamp, entry->database, entry->user,
entry->ip, entry->action, entry->query);
}

int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Uso: %s <arquivo_de_log>\n", argv[0]);
return 1;
}

FILE *fp = fopen(argv[1], "r");
if (!fp) {
perror("Erro ao abrir arquivo");
return 1;
}

char line[1024];
int blocked_count = 0;
int total_count = 0;

printf("%-20s %-15s %-15s %-15s %-10s %s\n",
"TIMESTAMP", "DATABASE", "USER", "IP", "ACTION", "QUERY");
printf("%s\n", "=".repeat(100));

while (fgets(line, sizeof(line), fp)) {
LogEntry entry;
parse_log_line(line, &entry);

printf("%-20s %-15s %-15s %-15s %-10s %.50s...\n",
entry.timestamp, entry.database, entry.user,
entry.ip, entry.action, entry.query);

total_count++;
if (strcmp(entry.action, "BLOCKED") == 0) {
blocked_count++;
}
}

printf("\nTotal de queries: %d\n", total_count);
printf("Queries bloqueadas: %d (%.1f%%)\n",
blocked_count, (float)blocked_count/total_count * 100);

fclose(fp);
return 0;
}
```

### 8.2 Script de Rotação de Logs

```batch
@echo off
REM rotate_logs.bat - Rotacionar logs de segurança
set LOG_DIR=C:\ProgramData\PostgreSQL\Security\Logs

REM Obter data atual
for /f "tokens=1-3 delims=/" %%a in ('date /t') do (
set TODAY=%%c%%a%%b
)

REM Rotacionar logs
cd /d "%LOG_DIR%"
for %%f in (*.log) do (
if exist "%%f" (
move "%%f" "%%~nf_%TODAY%%%~xf"
)
)

REM Comprimir logs antigos
for %%f in (*_20*.log) do (
7z a -tzip "%%~nf.zip" "%%f" && del "%%f"
)

REM Limpar logs com mais de 30 dias
forfiles /p "%LOG_DIR%" /m "*.zip" /d -30 /c "cmd /c del @file"
```

## 9. Troubleshooting

### 9.1 Problemas Comuns e Soluções

#### DLL não carrega
```
Problema: A DLL de segurança não é carregada
Soluções:
1. Verificar se está no diretório correto (mesmo da libpq.dll)
2. Usar Dependency Walker para verificar dependências
3. Verificar permissões do arquivo
4. Checar Event Viewer do Windows para erros
```

#### Queries válidas sendo bloqueadas
```
Problema: Queries legítimas estão sendo bloqueadas
Soluções:
1. Verificar configuração do banco no INI
2. Adicionar query ao whitelist
3. Revisar logs para entender o motivo
4. Temporariamente mudar BlockMode para 2 (log only)
```

#### Performance degradada
```
Problema: Lentidão após instalar a DLL
Soluções:
1. Verificar se logs estão muito grandes
2. Desabilitar validação para bancos não críticos
3. Usar whitelist para queries frequentes
4. Verificar I/O do disco onde estão os logs
```

### 9.2 Debug e Diagnóstico

```c
// debug_security.c - Ferramenta de debug
#include <windows.h>
#include <stdio.h>
#include "pgsql_security.h"

int main() {
// Testar carregamento da DLL
HMODULE hDll = LoadLibrary("pgsql_security.dll");
if (!hDll) {
printf("Erro ao carregar DLL: %lu\n", GetLastError());
return 1;
}

// Obter ponteiros das funções
PFN_GetSecurityVersion pfnVersion =
(PFN_GetSecurityVersion)GetProcAddress(hDll, "GetSecurityVersion");

if (pfnVersion) {
printf("Versão da DLL: %s\n", pfnVersion());
}

// Testar inicialização
PFN_InitializeSecurity pfnInit =
(PFN_InitializeSecurity)GetProcAddress(hDll, "InitializeSecurity");

if (pfnInit) {
if (pfnInit("pgsql_security.ini")) {
printf("✓ Segurança inicializada com sucesso\n");
} else {
printf("✗ Falha ao inicializar segurança\n");
}
}

FreeLibrary(hDll);
return 0;
}
```

## 10. Integração com WinDev

### 10.1 Classe de Gerenciamento Completa

```wlanguage
// Classe completa para gerenciar segurança PostgreSQL
CLASS PostgreSQLSecurityManager
PRIVATE
m_sIniPath is string
m_hDLL is system int
m_bInitialized is boolean

PUBLIC
// Constructor
CONSTRUCTOR(sIniPath is string = "")
// Determinar caminho do INI
IF sIniPath = "" THEN
m_sIniPath = GetPostgreSQLBinPath() + "\pgsql_security.ini"
ELSE
m_sIniPath = sIniPath
END

// Carregar DLL
LoadSecurityDLL()
END

// Destructor
DESTRUCTOR()
IF m_hDLL <> 0 THEN
API("kernel32", "FreeLibrary", m_hDLL)
END
END

// Obter caminho bin do PostgreSQL
PROCEDURE PRIVATE GetPostgreSQLBinPath(): string
sPath is string

// Tentar registro 64-bit
sPath = RegistryQueryValue("HKEY_LOCAL_MACHINE\SOFTWARE\PostgreSQL\Installations\postgresql-x64-14", "Base Directory")
IF sPath <> "" THEN RESULT sPath + "\bin"

// Tentar registro 32-bit
sPath = RegistryQueryValue("HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\PostgreSQL\Installations\postgresql-14", "Base Directory")
IF sPath <> "" THEN RESULT sPath + "\bin"

// Caminhos padrão
IF fDirectoryExist("C:\Program Files\PostgreSQL\14\bin") THEN
RESULT "C:\Program Files\PostgreSQL\14\bin"
END

// Usar diretório da aplicação
RESULT fExeDir()
END

// Carregar DLL de segurança
PROCEDURE PRIVATE LoadSecurityDLL(): boolean
sDLLPath is string = GetPostgreSQLBinPath() + "\pgsql_security.dll"

IF NOT fFileExist(sDLLPath) THEN
Error("DLL de segurança não encontrada: " + sDLLPath)
RESULT False
END

// Carregar DLL
m_hDLL = API("kernel32", "LoadLibraryA", sDLLPath)
IF m_hDLL = 0 THEN
Error("Erro ao carregar DLL de segurança")
RESULT False
END

// Inicializar
nProcAddr is system int = API("kernel32", "GetProcAddress", m_hDLL, "InitializeSecurity")
IF nProcAddr <> 0 THEN
m_bInitialized = API(nProcAddr, m_sIniPath)
END

RESULT m_bInitialized
END

// Validar query manualmente
PROCEDURE ValidateQuery(sDatabase is string, sQuery is string, sUser is string = ""): boolean
IF NOT m_bInitialized THEN RESULT True

nProcAddr is system int = API("kernel32", "GetProcAddress", m_hDLL, "ValidateQuery")
IF nProcAddr = 0 THEN RESULT True

sIP is string = "127.0.0.1"
RESULT API(nProcAddr, sDatabase, sQuery, sUser, sIP)
END

// Obter estatísticas
PROCEDURE GetStatistics(): (nTotal is int, nBlocked is int)
nTotal = 0
nBlocked = 0

IF m_bInitialized THEN
nProcAddr is system int = API("kernel32", "GetProcAddress", m_hDLL, "GetSecurityStats")
IF nProcAddr <> 0 THEN
API(nProcAddr, &nTotal, &nBlocked)
END
END

RESULT (nTotal, nBlocked)
END

// Recarregar configuração
PROCEDURE ReloadConfiguration()
IF m_bInitialized THEN
nProcAddr is system int = API("kernel32", "GetProcAddress", m_hDLL, "ReloadConfiguration")
IF nProcAddr <> 0 THEN
API(nProcAddr)
END
END
END
END
```

## 11. Considerações de Segurança

### 11.1 Boas Práticas

1. **Permissões de Arquivo**
- INI: Leitura apenas para o serviço PostgreSQL
- DLL: Leitura e execução
- Logs: Escrita apenas para o serviço

2. **Validação de Entrada**
- Sempre validar e sanitizar queries
- Usar prepared statements quando possível
- Implementar rate limiting

3. **Monitoramento**
- Alertas para tentativas de bloqueio
- Análise regular de logs
- Métricas de performance

### 11.2 Checklist de Segurança

- [ ] DLL assinada digitalmente
- [ ] INI em local protegido
- [ ] Logs com rotação automática
- [ ] Backup da libpq original
- [ ] Documentação de emergência
- [ ] Plano de rollback

## 12. Conclusão

Esta solução fornece um sistema robusto e flexível para controlar acesso a comandos SQL no PostgreSQL, com as seguintes vantagens:

- **Transparente**: Aplicações não precisam ser modificadas
- **Granular**: Controle por banco de dados
- **Auditável**: Logs completos de todas as ações
- **Flexível**: Configuração via INI sem recompilação
- **Seguro**: Múltiplas camadas de validação

A implementação via DLL customizada permite atualizar as políticas de segurança sem afetar o servidor PostgreSQL ou as aplicações cliente.

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Registered member
4,520 messages
Posted on June, 11 2025 - 4:15 PM
Sim, você pode cadastrar comandos adicionais facilmente! A solução foi projetada para ser flexível. Vou mostrar como adicionar novos comandos bloqueados:

## 1. Adicionando Comandos via Arquivo INI

### 1.1 Método Simples - Direto no INI

```ini
[DATABASE:producao]
; Comandos bloqueados padrão
BlockedCommands=DELETE,TRUNCATE,DROP,ALTER,UPDATE,GRANT,REVOKE,INSERT

; Para adicionar novos comandos, simplesmente inclua na lista
BlockedCommands=DELETE,TRUNCATE,DROP,ALTER,UPDATE,GRANT,REVOKE,INSERT,MERGE,COPY,VACUUM,ANALYZE,CLUSTER,REINDEX,COMMENT,EXECUTE,CALL

; Ou bloqueie comandos específicos do PostgreSQL
BlockedCommands=DELETE,TRUNCATE,DROP,ALTER,UPDATE,GRANT,REVOKE,INSERT,CREATE FUNCTION,CREATE PROCEDURE,CREATE TRIGGER,CREATE RULE,CREATE TYPE,CREATE EXTENSION
```

### 1.2 Seção de Comandos Customizados

Vamos melhorar o INI para suportar comandos customizados:

```ini
[CUSTOM_COMMANDS]
; Definir comandos adicionais e suas regras
; Formato: COMANDO=tipo_bloqueio,mensagem_personalizada

; Bloquear comandos de backup/restore
COPY=BLOCK,Comando COPY não permitido por política de segurança
COPY TO=BLOCK,Exportação de dados não permitida
COPY FROM=BLOCK,Importação de dados não permitida

; Bloquear comandos administrativos
VACUUM=WARN,VACUUM deve ser executado apenas em horário de manutenção
ANALYZE=LOG,ANALYZE executado - verificar performance
CLUSTER=BLOCK,CLUSTER não permitido em produção
REINDEX=BLOCK,REINDEX não permitido - usar manutenção programada

; Bloquear comandos DDL específicos
CREATE FUNCTION=BLOCK,Criação de funções não permitida
CREATE PROCEDURE=BLOCK,Criação de procedures não permitida
CREATE TRIGGER=BLOCK,Criação de triggers não permitida
CREATE EXTENSION=BLOCK,Instalação de extensões não permitida

; Bloquear comandos perigosos
SET ROLE=BLOCK,Mudança de role não permitida
SET SESSION AUTHORIZATION=BLOCK,Mudança de autorização não permitida
LOAD=BLOCK,Carregamento de bibliotecas não permitido

; Comandos de transação
BEGIN ISOLATION LEVEL SERIALIZABLE=WARN,Nível de isolamento pode impactar performance
LOCK TABLE=WARN,Lock de tabela pode causar bloqueios

[DATABASE:producao]
; Referenciar comandos customizados
UseCustomCommands=1
BlockedCommands=DELETE,TRUNCATE,DROP,ALTER,UPDATE,GRANT,REVOKE,INSERT
; Adicionar categoria de comandos
BlockedCategories=DDL,DML,ADMIN,BACKUP
```

## 2. Código C Atualizado para Suportar Comandos Customizados

### 2.1 Estrutura Expandida

```c
// pgsql_security_extended.h
typedef struct CustomCommand {
char command[128];
char block_type[16]; // BLOCK, WARN, LOG
char message[256];
struct CustomCommand *next;
} CustomCommand;

typedef struct CommandCategory {
char name[32];
char commands[1024];
} CommandCategory;

typedef struct DatabaseConfig {
char dbname[128];
BlockMode block_mode;
char allowed_commands[1024];
char blocked_commands[1024];
char blocked_categories[256];
bool use_custom_commands;
char log_file[256];
struct DatabaseConfig *next;
} DatabaseConfig;

typedef struct SecurityContext {
bool enabled;
char ini_path[MAX_PATH];
char log_path[MAX_PATH];
DatabaseConfig *db_configs;
CustomCommand *custom_commands;
CommandCategory categories[10];
int category_count;
CRITICAL_SECTION cs;
} SecurityContext;
```

### 2.2 Função para Carregar Comandos Customizados

```c
// Carregar comandos customizados do INI
static void LoadCustomCommands(const char *ini_path) {
char buffer[32768];
char *line;

// Limpar comandos anteriores
CustomCommand *current = g_security.custom_commands;
while (current) {
CustomCommand *next = current->next;
free(current);
current = next;
}
g_security.custom_commands = NULL;

// Ler seção CUSTOM_COMMANDS
GetPrivateProfileSection("CUSTOM_COMMANDS", buffer, sizeof(buffer), ini_path);

line = buffer;
while (*line) {
char *equals = strchr(line, '=');
if (equals) {
*equals = '\0';
char *value = equals + 1;

// Parse: COMANDO=tipo,mensagem
char *comma = strchr(value, ',');
if (comma) {
*comma = '\0';
char *message = comma + 1;

// Criar novo comando customizado
CustomCommand *cmd = (CustomCommand*)calloc(1, sizeof(CustomCommand));
strncpy(cmd->command, line, sizeof(cmd->command) - 1);
strncpy(cmd->block_type, value, sizeof(cmd->block_type) - 1);
strncpy(cmd->message, message, sizeof(cmd->message) - 1);

// Adicionar à lista
cmd->next = g_security.custom_commands;
g_security.custom_commands = cmd;
}
}

line += strlen(line) + 1;
}

// Carregar categorias
LoadCommandCategories(ini_path);
}

// Carregar categorias de comandos
static void LoadCommandCategories(const char *ini_path) {
g_security.category_count = 0;

// DDL - Data Definition Language
GetPrivateProfileString("COMMAND_CATEGORIES", "DDL",
"CREATE,ALTER,DROP,RENAME,TRUNCATE,COMMENT",
g_security.categories[0].commands,
sizeof(g_security.categories[0].commands), ini_path);
strcpy(g_security.categories[0].name, "DDL");

// DML - Data Manipulation Language
GetPrivateProfileString("COMMAND_CATEGORIES", "DML",
"INSERT,UPDATE,DELETE,MERGE,COPY",
g_security.categories[1].commands,
sizeof(g_security.categories[1].commands), ini_path);
strcpy(g_security.categories[1].name, "DML");

// ADMIN - Administrative Commands
GetPrivateProfileString("COMMAND_CATEGORIES", "ADMIN",
"VACUUM,ANALYZE,CLUSTER,REINDEX,CHECKPOINT,LOAD",
g_security.categories[2].commands,
sizeof(g_security.categories[2].commands), ini_path);
strcpy(g_security.categories[2].name, "ADMIN");

// BACKUP - Backup/Restore Commands
GetPrivateProfileString("COMMAND_CATEGORIES", "BACKUP",
"COPY TO,COPY FROM,pg_dump,pg_restore",
g_security.categories[3].commands,
sizeof(g_security.categories[3].commands), ini_path);
strcpy(g_security.categories[3].name, "BACKUP");

g_security.category_count = 4;
}

// Verificar comando customizado
static CustomCommand* FindCustomCommand(const char *query) {
CustomCommand *current = g_security.custom_commands;

while (current) {
if (strncasecmp(query, current->command, strlen(current->command)) == 0) {
return current;
}
current = current->next;
}

return NULL;
}

// Verificar se comando está em categoria bloqueada
static bool IsCommandInBlockedCategory(const char *command, const char *blocked_categories) {
if (!blocked_categories || strlen(blocked_categories) == 0) return false;

// Para cada categoria bloqueada
char categories_copy[256];
strcpy(categories_copy, blocked_categories);

char *category = strtok(categories_copy, ",");
while (category) {
// Remover espaços
while (*category == ' ') category++;

// Procurar categoria
for (int i = 0; i < g_security.category_count; i++) {
if (strcasecmp(g_security.categories[i].name, category) == 0) {
// Verificar se comando está nesta categoria
if (IsCommandInList(command, g_security.categories[i].commands)) {
return true;
}
}
}

category = strtok(NULL, ",");
}

return false;
}
```

### 2.3 Função ValidateQuery Atualizada

```c
// Validar query com suporte a comandos customizados
bool ValidateQuery(const char *dbname, const char *query, const char *user, const char *ip) {
if (!g_security.enabled) return true;

EnterCriticalSection(&g_security.cs);

DatabaseConfig *config = FindDatabaseConfig(dbname);
if (!config) {
LeaveCriticalSection(&g_security.cs);
return true;
}

// Extrair comando principal
char command[64];
ExtractCommand(query, command, sizeof(command));

bool allowed = true;
char action[64] = "ALLOWED";
char message[256] = "";

// 1. Verificar comandos customizados primeiro
if (config->use_custom_commands) {
CustomCommand *custom = FindCustomCommand(query);
if (custom) {
strcpy(action, custom->block_type);
strcpy(message, custom->message);

if (strcasecmp(custom->block_type, "BLOCK") == 0) {
allowed = false;
}
}
}

// 2. Verificar categorias bloqueadas
if (allowed && strlen(config->blocked_categories) > 0) {
if (IsCommandInBlockedCategory(command, config->blocked_categories)) {
allowed = false;
strcpy(action, "BLOCKED");
sprintf(message, "Comando %s bloqueado por categoria", command);
}
}

// 3. Verificar lista de comandos bloqueados
if (allowed && strlen(config->blocked_commands) > 0) {
if (IsCommandInList(command, config->blocked_commands)) {
switch (config->block_mode) {
case 0: // Block
allowed = false;
strcpy(action, "BLOCKED");
sprintf(message, "Comando %s não permitido", command);
break;
case 1: // Warn
strcpy(action, "WARNING");
sprintf(message, "Aviso: Comando %s executado", command);
break;
case 2: // Log only
strcpy(action, "LOGGED");
break;
}
}
}

// Registrar evento
if (!allowed || config->block_mode > 0 || strlen(message) > 0) {
LogSecurityEventEx(dbname, query, user, ip, action, message, config->log_file);
}

LeaveCriticalSection(&g_security.cs);

// Se bloqueado, definir mensagem de erro
if (!allowed && strlen(message) > 0) {
SetLastError(ERROR_ACCESS_DENIED);
// Armazenar mensagem para ser recuperada pela aplicação
strcpy(g_last_error_message, message);
}

return allowed;
}
```

## 3. Interface WinDev para Gerenciar Comandos Customizados

### 3.1 Window de Gerenciamento de Comandos

```wlanguage
// WIN_CustomCommands - Janela para gerenciar comandos customizados
PROCEDURE WIN_CustomCommands()
// Carregar comandos existentes
LoadCustomCommands()

// Preencher categorias
COMBO_Category.Add("DDL - Data Definition Language")
COMBO_Category.Add("DML - Data Manipulation Language")
COMBO_Category.Add("ADMIN - Administrative")
COMBO_Category.Add("BACKUP - Backup/Restore")
COMBO_Category.Add("CUSTOM - Personalizado")
END

// Carregar comandos do INI
PROCEDURE LoadCustomCommands()
sIniPath is string = GetSecurityINIPath()
TABLE_Commands.DeleteAll()

// Ler seção CUSTOM_COMMANDS
sSection is string = INIRead("CUSTOM_COMMANDS", "", "", sIniPath)
IF sSection = "" THEN RETURN

// Parse cada linha
FOR EACH STRING sLine OF sSection SEPARATED BY CR
nPos is int = Position(sLine, "=")
IF nPos > 0 THEN
sCommand is string = sLine[[1 TO nPos-1]]
sValue is string = sLine[[nPos+1 TO]]

// Parse tipo e mensagem
nComma is int = Position(sValue, ",")
IF nComma > 0 THEN
sType is string = sValue[[1 TO nComma-1]]
sMessage is string = sValue[[nComma+1 TO]]

TABLE_Commands.AddLine(sCommand, sType, sMessage)
END
END
END
END

// Adicionar novo comando
PROCEDURE BTN_AddCommand_Click()
sCommand is string = EDT_Command
sType is string = COMBO_Type
sMessage is string = EDT_Message

IF sCommand = "" THEN
Error("Digite o comando SQL")
RETURN
END

// Adicionar à tabela
TABLE_Commands.AddLine(sCommand, sType, sMessage)

// Limpar campos
EDT_Command = ""
EDT_Message = ""
END

// Salvar comandos no INI
PROCEDURE BTN_Save_Click()
sIniPath is string = GetSecurityINIPath()

// Limpar seção existente
INIWrite("CUSTOM_COMMANDS", "", "", sIniPath)

// Salvar cada comando
FOR nRow = 1 TO TABLE_Commands.Count
sCommand is string = TABLE_Commands[nRow].COL_Command
sType is string = TABLE_Commands[nRow].COL_Type
sMessage is string = TABLE_Commands[nRow].COL_Message

sValue is string = sType + "," + sMessage
INIWrite("CUSTOM_COMMANDS", sCommand, sValue, sIniPath)
END

Info("Comandos salvos com sucesso!")

// Notificar DLL para recarregar
ReloadSecurityConfiguration()
END

// Importar comandos de arquivo
PROCEDURE BTN_Import_Click()
sFile is string = fSelect("", "", "Selecione arquivo de comandos", ...
"Arquivos texto (*.txt)" + TAB + "*.txt" + CR + ...
"Arquivos CSV (*.csv)" + TAB + "*.csv")

IF sFile = "" THEN RETURN

// Ler arquivo
sContent is string = fLoadText(sFile)

// Parse linhas (formato: COMANDO|TIPO|MENSAGEM)
FOR EACH STRING sLine OF sContent SEPARATED BY CR
arrParts is array of string
StringToArray(sLine, arrParts, "|")

IF arrParts.Count >= 3 THEN
TABLE_Commands.AddLine(arrParts[1], arrParts[2], arrParts[3])
END
END

Info("Comandos importados com sucesso!")
END
```

### 3.2 Classe para Gerenciar Comandos por Categoria

```wlanguage
// Classe para gerenciar comandos por categoria
CLASS CommandCategoryManager
PRIVATE
m_mapCategories is associative array of array of string
m_sIniPath is string

PUBLIC
// Constructor
CONSTRUCTOR(sIniPath is string = "")
m_sIniPath = sIniPath
LoadCategories()
END

// Carregar categorias padrão
PROCEDURE PRIVATE LoadCategories()
// DDL - Data Definition Language
m_mapCategories["DDL"].Add("CREATE")
m_mapCategories["DDL"].Add("ALTER")
m_mapCategories["DDL"].Add("DROP")
m_mapCategories["DDL"].Add("RENAME")
m_mapCategories["DDL"].Add("TRUNCATE")
m_mapCategories["DDL"].Add("COMMENT")

// DML - Data Manipulation Language
m_mapCategories["DML"].Add("INSERT")
m_mapCategories["DML"].Add("UPDATE")
m_mapCategories["DML"].Add("DELETE")
m_mapCategories["DML"].Add("MERGE")
m_mapCategories["DML"].Add("COPY")

// DCL - Data Control Language
m_mapCategories["DCL"].Add("GRANT")
m_mapCategories["DCL"].Add("REVOKE")
m_mapCategories["DCL"].Add("CREATE ROLE")
m_mapCategories["DCL"].Add("ALTER ROLE")
m_mapCategories["DCL"].Add("DROP ROLE")

// TCL - Transaction Control Language
m_mapCategories["TCL"].Add("BEGIN")
m_mapCategories["TCL"].Add("COMMIT")
m_mapCategories["TCL"].Add("ROLLBACK")
m_mapCategories["TCL"].Add("SAVEPOINT")
m_mapCategories["TCL"].Add("SET TRANSACTION")

// Administrative
m_mapCategories["ADMIN"].Add("VACUUM")
m_mapCategories["ADMIN"].Add("ANALYZE")
m_mapCategories["ADMIN"].Add("CLUSTER")
m_mapCategories["ADMIN"].Add("REINDEX")
m_mapCategories["ADMIN"].Add("CHECKPOINT")

// Perigosos
m_mapCategories["DANGEROUS"].Add("DROP DATABASE")
m_mapCategories["DANGEROUS"].Add("DROP SCHEMA")
m_mapCategories["DANGEROUS"].Add("TRUNCATE")
m_mapCategories["DANGEROUS"].Add("DELETE FROM")
m_mapCategories["DANGEROUS"].Add("UPDATE SET")

// Extensões e Funções
m_mapCategories["EXTENSIONS"].Add("CREATE EXTENSION")
m_mapCategories["EXTENSIONS"].Add("DROP EXTENSION")
m_mapCategories["EXTENSIONS"].Add("CREATE FUNCTION")
m_mapCategories["EXTENSIONS"].Add("CREATE PROCEDURE")
m_mapCategories["EXTENSIONS"].Add("CREATE TRIGGER")
END

// Adicionar comando a uma categoria
PROCEDURE AddCommandToCategory(sCategory is string, sCommand is string)
IF NOT m_mapCategories[sCategory]..Exists THEN
m_mapCategories[sCategory] = new array of string
END

m_mapCategories[sCategory].Add(sCommand)
SaveCategories()
END

// Obter comandos de uma categoria
PROCEDURE GetCategoryCommands(sCategory is string): array of string
IF m_mapCategories[sCategory]..Exists THEN
RESULT m_mapCategories[sCategory]
END

RESULT new array of string
END

// Salvar categorias no INI
PROCEDURE PRIVATE SaveCategories()
FOR EACH arrCommands, sCategory OF m_mapCategories
sCommands is string = ArrayToString(arrCommands, ",")
INIWrite("COMMAND_CATEGORIES", sCategory, sCommands, m_sIniPath)
END
END

// Verificar se comando pertence a categoria
PROCEDURE IsCommandInCategory(sCommand is string, sCategory is string): boolean
IF NOT m_mapCategories[sCategory]..Exists THEN
RESULT False
END

sCommandUpper is string = Upper(sCommand)
FOR EACH sCmd OF m_mapCategories[sCategory]
IF sCommandUpper.StartsWith(Upper(sCmd)) THEN
RESULT True
END
END

RESULT False
END

// Listar todas as categorias
PROCEDURE ListCategories(): array of string
arrCategories is array of string
FOR EACH arrCommands, sCategory OF m_mapCategories
arrCategories.Add(sCategory)
END
RESULT arrCategories
END
END
```

## 4. Exemplos de Uso

### 4.1 Configuração para Diferentes Cenários

#### Banco de Produção Ultra-Restritivo
```ini
[DATABASE:producao_critica]
BlockMode=0
; Permitir apenas SELECT básico
AllowedCommands=SELECT
; Bloquear categorias inteiras
BlockedCategories=DDL,DML,DCL,TCL,ADMIN,DANGEROUS,EXTENSIONS
; Usar comandos customizados
UseCustomCommands=1

[CUSTOM_COMMANDS]
; Bloquear SELECTs perigosos
SELECT INTO=BLOCK,SELECT INTO não permitido em produção
SELECT * FROM=WARN,Evite SELECT * - especifique colunas
SELECT WITHOUT WHERE=WARN,SELECT sem WHERE pode impactar performance

; Bloquear funções perigosas
pg_sleep=BLOCK,Função pg_sleep não permitida
pg_terminate_backend=BLOCK,Terminar conexões não permitido
pg_cancel_backend=BLOCK,Cancelar queries não permitido
```

#### Banco de Desenvolvimento com Avisos
```ini
[DATABASE:desenvolvimento]
BlockMode=1
AllowedCommands=ALL
UseCustomCommands=1

[CUSTOM_COMMANDS]
; Avisar sobre comandos perigosos
DROP TABLE=WARN,⚠️ CUIDADO: Você está deletando uma tabela!
TRUNCATE=WARN,⚠️ TRUNCATE remove TODOS os dados!
DELETE WITHOUT WHERE=WARN,⚠️ DELETE sem WHERE afeta todos registros!
UPDATE WITHOUT WHERE=WARN,⚠️ UPDATE sem WHERE afeta todos registros!
```

### 4.2 Script para Popular Comandos Comuns

```wlanguage
// Procedure para adicionar comandos PostgreSQL comuns
PROCEDURE PopulateCommonPostgreSQLCommands()
manager is CommandCategoryManager()

// Comandos de Sistema
manager.AddCommandToCategory("SYSTEM", "SHOW")
manager.AddCommandToCategory("SYSTEM", "SET")
manager.AddCommandToCategory("SYSTEM", "RESET")
manager.AddCommandToCategory("SYSTEM", "DISCARD")

// Comandos de Cópia e Backup
manager.AddCommandToCategory("BACKUP", "COPY TO STDOUT")
manager.AddCommandToCategory("BACKUP", "COPY TO PROGRAM")
manager.AddCommandToCategory("BACKUP", "\\copy")

// Comandos de Informação
manager.AddCommandToCategory("INFO", "EXPLAIN")
manager.AddCommandToCategory("INFO", "EXPLAIN ANALYZE")
manager.AddCommandToCategory("INFO", "\\d")
manager.AddCommandToCategory("INFO", "\\dt")
manager.AddCommandToCategory("INFO", "\\l")

// Comandos Perigosos Específicos
manager.AddCommandToCategory("DANGEROUS", "DROP DATABASE IF EXISTS")
manager.AddCommandToCategory("DANGEROUS", "DROP SCHEMA CASCADE")
manager.AddCommandToCategory("DANGEROUS", "DROP OWNED BY")
manager.AddCommandToCategory("DANGEROUS", "REASSIGN OWNED")

Info("Comandos PostgreSQL adicionados com sucesso!")
END
```

### 4.3 Validação em Tempo Real

```wlanguage
// Procedure para validar query em tempo real
PROCEDURE ValidateQueryRealTime(sQuery is string, sDatabase is string): (bValid is boolean, sMessage is string)
security is PostgreSQLSecurityManager()

// Validar query
IF NOT security.ValidateQuery(sDatabase, sQuery, CurrentUser()) THEN
// Obter mensagem de erro detalhada
sMessage = security.GetLastErrorMessage()
RESULT (False, sMessage)
END

// Query permitida
RESULT (True, "Query permitida")
END

// Exemplo de uso em campo de entrada
PROCEDURE EDT_Query_Exit()
bValid, sMessage = ValidateQueryRealTime(EDT_Query, COMBO_Database)

IF NOT bValid THEN
// Mostrar aviso visual
EDT_Query..BrushColor = LightRed
STC_Warning = sMessage
STC_Warning..Visible = True
ELSE
EDT_Query..BrushColor = White
STC_Warning..Visible = False
END
END
```

## 5. Monitoramento de Comandos Bloqueados

### 5.1 Dashboard de Comandos

```wlanguage
// Window para monitorar comandos bloqueados
PROCEDURE WIN_SecurityDashboard()
// Atualizar estatísticas
RefreshStatistics()

// Carregar comandos mais bloqueados
LoadTopBlockedCommands()

// Iniciar timer de atualização
Timer(RefreshStatistics, 5s)
END

// Carregar comandos mais bloqueados
PROCEDURE LoadTopBlockedCommands()
// Analisar logs
sLogPath is string = GetSecurityLogPath()
mapCommands is associative array of int

// Ler todos os logs
arrFiles is array of string = fListFile(sLogPath + "*.log")

FOR EACH sFile OF arrFiles
AnalyzeLogFile(sFile, mapCommands)
END

// Ordenar e mostrar top 10
CHART_BlockedCommands.DeleteAll()

arrSorted is array of CommandCount
FOR EACH nCount, sCommand OF mapCommands
cc is CommandCount
cc.Command = sCommand
cc.Count = nCount
arrSorted.Add(cc)
END

ArraySort(arrSorted, asMember, "-Count")

FOR i = 1 TO Min(10, arrSorted.Count)
CHART_BlockedCommands.AddData(1, arrSorted[i].Count, arrSorted[i].Command)
END
END
```

Com essas implementações, você tem total flexibilidade para:

1. **Adicionar novos comandos** via interface ou diretamente no INI
2. **Criar categorias** de comandos para facilitar gerenciamento
3. **Definir mensagens personalizadas** para cada comando
4. **Importar/Exportar** listas de comandos
5. **Monitorar** quais comandos são mais bloqueados
6. **Validar em tempo real** antes da execução

A solução é completamente extensível e permite adicionar qualquer comando SQL que você queira bloquear, avisar ou registrar.

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/