No arsenal de qualquer administrador de sistemas ou analista de segurança, poucas ferramentas são tão clássicas quanto o netstat e o console de Gerenciamento do Firewall do Windows. No entanto, usá-las em conjunto para uma resposta rápida a incidentes pode ser um processo manual e lento.
E se você pudesse ter a visibilidade do netstat, o poder de bloqueio do firewall e um sistema de alerta proativo, tudo em uma única interface interativa no seu terminal do PowerShell?
É exatamente isso que o projeto Connector Detector se propõe a fazer. Trata-se de um utilitário avançado, escrito inteiramente em PowerShell, projetado para monitorar, analisar e gerenciar ativamente suas conexões de rede TCP e UDP em tempo real.
O Problema: Visibilidade sem Ação
O desafio é clássico: você executa netstat -ano e vê um processo com um PID estranho se comunicando com um endereço IP que você não reconhece. Agora começa o trabalho manual:
- Abrir o Gerenciador de Tarefas para descobrir o nome do processo associado àquele PID.
- Copiar o endereço IP externo.
- Abrir o "Windows Defender Firewall com Segurança Avançada".
- Iniciar o assistente para criar uma nova regra de saída.
- Colar o IP e bloquear a conexão.
Em um cenário de resposta a incidentes, esses segundos (ou minutos) são preciosos. O Connector Detector consolida todo esse fluxo de trabalho em um único local.
Este script vai muito além de simplesmente listar conexões. Ele atua como um verdadeiro centro de controle de rede.
- Visibilidade Total: A ferramenta usa
netstat -a -n -omas, ao contrário da saída de texto pura, ela analisa e correlaciona o PID de cada conexão ao seu nome de processo (ex:svchost.exe), exibindo tudo em uma tabela limpa e estruturada. - Resposta Rápida a Incidentes: Viu uma conexão suspeita? O menu permite que você selecione a opção "Bloquear" e crie imediatamente uma regra de saída no Firewall do Windows (
New-NetFirewallRule) para cortar a comunicação. - Motor de Detecção Proativo: O script mantém listas personalizáveis de IPs maliciosos e portas incomuns. Ele pode comparar ativamente as conexões com essas listas para identificar ameaças.
- Monitoramento em Background: Esta é a funcionalidade mais poderosa. Você pode iniciar um "job" de monitoramento (Opção 4.5). O script usa
Start-ThreadJob(ouStart-Jobcomo fallback) para verificar continuamente as conexões em segundo plano, sem travar seu terminal. - Alertas Instantâneos (Toast Notifications): Quando o monitor em background detecta uma conexão que corresponde à sua lista de suspeitos, ele envia uma notificação "toast" (um alerta de desktop) , usando o popular módulo
BurntToast.
O projeto não é apenas um script monolítico; ele é dividido em três arquivos principais que demonstram um design de software robusto.
start.ps1(O Lançador): Um script de entrada simples que garante que o motor principal seja localizado e executado corretamente, tratando erros de caminho e encaminhando argumentos.connector-detector.ps1(O Motor): O coração da aplicação. Ele lida com a verificação de privilégios de administrador (essencial para o firewall) , a instalação de módulos (BurntToast) , a lógica denetstat, a criação dos jobs e todo o menu interativo.connector-language.ps1(O Módulo de Idioma): O aspecto mais elegante da arquitetura. Em vez de ter textos fixos, este módulo escaneia dinamicamente um subdiretóriolocales/em busca de arquivos*.json. Ele então constrói automaticamente um menu de seleção de idioma. Se você adicionar umfr.jsonna pasta, ele aparecerá na inicialização. Além disso, ele usa oen.jsoncomo "linha de base", garantindo que, se uma tradução estiver faltando em outro idioma, ele recorra ao inglês em vez de quebrar a interface.
O código a seguir é "lançador" (launcher) em PowerShell, cuja única finalidade é executar um segundo script, chamado connector-detector.ps1, de forma controlada e segura.
Seu funcionamento começa com uma lógica robusta para determinar o diretório onde o próprio script lançador está sendo executado. Ele primeiro tenta usar a variável $PSScriptRoot, que é a forma moderna de fazer isso. No entanto, se essa variável estiver vazia (o que pode acontecer em versões mais antigas do PowerShell ou se o script for "dot-sourced"), ele recorre a um método alternativo, inspecionando a variável $MyInvocation.MyCommand.Definition para extrair o caminho. Se até isso falhar, ele usa o local atual (Get-Location) como último recurso.
Uma vez que o diretório do script é determinado, ele constrói o caminho completo para o script-alvo (connector-detector.ps1), assumindo que ele esteja na mesma pasta. O lançador então verifica se esse arquivo-alvo realmente existe no local esperado. Se o arquivo não for encontrado, ele exibe uma mensagem de erro clara informando que o connector-detector.ps1 está ausente e encerra o script com o código de saída 2.
Se o arquivo-alvo for encontrado, o script se prepara para a execução principal. Ele faz isso dentro de um bloco try…catch…finally para garantir um tratamento de erros adequado. Antes de chamar o script, ele salva a preferência de ação de erro atual do PowerShell ($ErrorActionPreference) e, em seguida, a define como Stop. Isso é crucial: força o PowerShell a tratar qualquer erro no script-alvo como um erro de encerramento, permitindo que o bloco catch do lançador o capture.
Em seguida, ele executa o script-alvo (& $target) e usa a sintaxe @args para encaminhar (ou "splat") todos e quaisquer argumentos que foram originalmente passados para o script lançador, repassando-os de forma transparente para o connector-detector.ps1.
Se a execução for concluída sem erros, uma variável $succeeded é definida como true. Se ocorrer qualquer erro (capturado por causa do $ErrorActionPreference = 'Stop'), o bloco catch é ativado, exibe uma mensagem de erro informando que a execução falhou (incluindo o erro original) e define $succeeded como false.
Independentemente de sucesso ou falha, o bloco finally é executado para restaurar a preferência de ação de erro ($ErrorActionPreference) ao seu valor original, garantindo que o lançador não altere o ambiente do usuário após a execução.
Por fim, o script verifica a variável $succeeded: se for true, ele sai com o código 0 (indicando sucesso), e se for false, sai com o código 1 (indicando falha). O script inteiro também está envolvido em um try…catch externo para capturar qualquer falha no próprio lançador, garantindo que ele sempre saia com um código de erro apropriado em caso de problema.
[CmdletBinding()]
param()
try {
$scriptDir = $PSScriptRoot
if (-not $scriptDir -or [string]::IsNullOrWhiteSpace($scriptDir)) {
$def = $MyInvocation.MyCommand.Definition
if ([string]::IsNullOrWhiteSpace($def)) {
# Fallback to current location
$scriptDir = (Get-Location).ProviderPath
}
else {
$scriptDir = Split-Path -Path $def -Parent
}
}
$target = Join-Path -Path $scriptDir -ChildPath 'connector-detector.ps1'
if (-not (Test-Path -Path $target -PathType Leaf)) {
Write-Error "Could not find script: $target. Ensure 'connector-detector.ps1' is in the same folder as this script."
exit 2
}
$oldEAP = $ErrorActionPreference
$ErrorActionPreference = 'Stop'
try {
Write-Verbose "Invoking: $target with args: $($args -join ' ')"
& $target @args
$succeeded = $true
}
catch {
Write-Error "Execution of connector-detector.ps1 failed: $_"
$succeeded = $false
}
finally {
$ErrorActionPreference = $oldEAP
}
if ($succeeded) { exit 0 } else { exit 1 }
}
catch {
Write-Error "Launcher failure: $_"
exit 1
}Continuando, a seguir temos um script PowerShell robusto projetado para gerenciar a localização e internacionalização (tradução) de um script maior. Seu objetivo é carregar e fornecer texto em diferentes idiomas, que são armazenados em arquivos .json separados dentro de uma pasta locales.
O script começa determinando seu próprio diretório de execução usando uma sequência de métodos de fallback, garantindo que funcione em diferentes versões e modos de execução do PowerShell. Em seguida, ele inicializa três variáveis globais cruciais: __LanguageCode para rastrear o idioma atualmente selecionado (começando com 'en'), __LanguageData para armazenar o arquivo de idioma carregado e __LanguageBase para armazenar o idioma inglês (en.json), que serve como a "verdade absoluta" ou linha de base para todas as traduzidos.
O sistema gira em torno da função Initialize-LanguageBase, que carrega o arquivo en.json na memória uma única vez. Este arquivo base é fundamental, pois é usado pela função Test-LanguageData para validar outros arquivos de idioma. A validação verifica duas coisas: se o arquivo de tradução tem todas as chaves que existem no en.json (relatando MissingKeys) e se alguma dessas chaves tem um valor vazio ou nulo (relatando EmptyKeys).
A função Get-AvailableLanguages varre a pasta locales em busca de todos os arquivos .json, os lê, valida-os contra a linha de base inglesa e gera uma lista de objetos que descrevem cada idioma disponível, incluindo seu nome de exibição (que é obtido de forma inteligente pela Get-LanguageDisplayName), seu código e seu status de validade.
A função principal é Set-Language. Quando chamada com um código de idioma (por exemplo, 'pt-BR'), ela primeiro define o código global. Em seguida, ela procura o arquivo pt-BR.json. Se não o encontrar, ela demonstra uma lógica de fallback inteligente, procurando pelo idioma base sem a região (tentando encontrar pt.json).
Uma vez que um arquivo de idioma é encontrado, ele é lido, validado e, o mais importante, mesclado com a linha de base inglesa. Isso significa que se uma tradução em português estiver faltando para uma chave específica, o script usará automaticamente o valor da chave inglesa em seu lugar. Isso garante que o script nunca falhe por causa de uma tradução incompleta. O resultado mesclado e seguro é então armazenado na variável global __LanguageData.
Com um idioma carregado, o restante do script usa a função Get-Text para exibir o texto. Esta função pega uma chave (como 'WelcomeMessage') e a procura na __LanguageData carregada. Ela também lida com a formatação de strings (para inserir nomes ou números no texto traduzido) e possui um pequeno conjunto de fallbacks codificados para mensagens críticas, caso até mesmo o inglês falhe ao carregar.
Finalmente, a função Select-LanguageInteractive usa todas as outras funções para fornecer uma experiência de usuário: ela chama Get-AvailableLanguages para construir um menu dinâmico, exibe esse menu para o usuário e, em seguida, usa Set-Language para aplicar a escolha do usuário.
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
if ([string]::IsNullOrEmpty($scriptDir)) { $scriptDir = $PSScriptRoot }
if ([string]::IsNullOrEmpty($scriptDir)) { $scriptDir = (Get-Location).Path }
$Global:__LanguageCode = 'en'
$Global:__LanguageData = @{}
$Global:__LanguageBase = @{} # baseline (en) keys/values for validation/fallback
function Get-LocaleDirectory {
$basePath = Join-Path -Path $scriptDir -ChildPath 'locales'
return $basePath
}
function Initialize-LanguageBase {
if ($Global:__LanguageBase.Count -gt 0) { return }
$basePath = Get-LocaleDirectory
$enPath = Join-Path $basePath 'en.json'
if (Test-Path $enPath) {
try {
$json = Get-Content -Raw -Path $enPath | ConvertFrom-Json -ErrorAction Stop
$tmp = @{}
foreach ($p in $json.PSObject.Properties) { $tmp[$p.Name] = $p.Value }
$Global:__LanguageBase = $tmp
} catch {
# If en.json cannot be read, keep empty baseline; validation will be skipped
Write-Verbose "Failed to load baseline 'en.json': $_"
$Global:__LanguageBase = @{}
}
}
}
function Test-LanguageData {
param(
[Parameter(Mandatory=$true)][hashtable]$Data
)
Initialize-LanguageBase
$missing = @()
$empty = @()
if ($Global:__LanguageBase.Count -eq 0) {
# No baseline available; consider valid
return [PSCustomObject]@{ IsValid = $true; MissingKeys = @(); EmptyKeys = @() }
}
foreach ($k in $Global:__LanguageBase.Keys) {
if (-not $Data.ContainsKey($k)) { $missing += $k; continue }
$v = $Data[$k]
if (-not ($v -is [string]) -or [string]::IsNullOrWhiteSpace([string]$v)) { $empty += $k }
}
$isValid = ($missing.Count -eq 0 -and $empty.Count -eq 0)
return [PSCustomObject]@{
IsValid = $isValid
MissingKeys = $missing
EmptyKeys = $empty
}
}
function Get-LanguageDisplayName {
param(
[Parameter(Mandatory=$true)][string]$Code,
[hashtable]$RawData
)
# Try to read name metadata if present
if ($RawData) {
if ($RawData.ContainsKey('LanguageName') -and -not [string]::IsNullOrWhiteSpace($RawData['LanguageName'])) {
return $RawData['LanguageName']
}
if ($RawData.ContainsKey('_meta.name') -and -not [string]::IsNullOrWhiteSpace($RawData['_meta.name'])) {
return $RawData['_meta.name']
}
}
$map = @{
'en' = 'English'
'es' = 'Español'
'pt' = 'Português'
'pt-BR'= 'Português (Brasil)'
}
if ($map.ContainsKey($Code)) { return $map[$Code] }
return $Code
}
function Get-AvailableLanguages {
param(
[switch]$ValidOnly
)
Initialize-LanguageBase
$dir = Get-LocaleDirectory
if (-not (Test-Path $dir)) { return @() }
$items = Get-ChildItem -Path $dir -Filter '*.json' -File -ErrorAction SilentlyContinue
$list = @()
foreach ($it in $items) {
$code = [System.IO.Path]::GetFileNameWithoutExtension($it.FullName)
try {
$rawJson = Get-Content -Raw -Path $it.FullName | ConvertFrom-Json -ErrorAction Stop
$data = @{}
foreach ($p in $rawJson.PSObject.Properties) { $data[$p.Name] = $p.Value }
$test = Test-LanguageData -Data $data
if ($ValidOnly -and -not $test.IsValid) {
Write-Warning "Skipping locale '$code' due to validation errors. Missing: $($test.MissingKeys -join ', ') Empty: $($test.EmptyKeys -join ', ')"
continue
}
$name = Get-LanguageDisplayName -Code $code -RawData $data
$list += [PSCustomObject]@{
Code = $code
Name = $name
FilePath = $it.FullName
IsValid = $test.IsValid
MissingKeys = $test.MissingKeys
EmptyKeys = $test.EmptyKeys
}
} catch {
Write-Warning "Failed to read locale file '$($it.Name)': $_"
}
}
# Sort by Name, then Code for stable ordering
return ($list | Sort-Object Name, Code)
}
function Set-Language {
param(
[Parameter(Mandatory=$true)][string]$lang
)
$global:__LanguageCode = $lang
$basePath = Get-LocaleDirectory
$filePath = Join-Path $basePath ($lang + '.json')
if (-not (Test-Path $filePath)) {
# try language without region (e.g. 'pt-BR' -> 'pt')
if ($lang -match '^([a-z]{2})-') { $short = $Matches[1]; $f2 = Join-Path $basePath ($short + '.json') ; if (Test-Path $f2) { $filePath = $f2 } }
}
if (Test-Path $filePath) {
try {
Initialize-LanguageBase
$json = Get-Content -Raw -Path $filePath | ConvertFrom-Json -ErrorAction Stop
$data = @{}
foreach ($p in $json.PSObject.Properties) { $data[$p.Name] = $p.Value }
# validate and merge with baseline
$test = Test-LanguageData -Data $data
if (-not $test.IsValid) {
Write-Warning "Locale '$lang' has issues. Missing: $($test.MissingKeys -join ', ') Empty: $($test.EmptyKeys -join ', '). Falling back to English for those keys."
}
$merged = @{}
if ($Global:__LanguageBase.Count -gt 0) {
foreach ($k in $Global:__LanguageBase.Keys) {
$val = $null
if ($data.ContainsKey($k) -and -not [string]::IsNullOrWhiteSpace([string]$data[$k])) { $val = [string]$data[$k] }
else { $val = [string]$Global:__LanguageBase[$k] }
$merged[$k] = $val
}
# also include any extra keys present only in the translation
foreach ($k in $data.Keys) { if (-not $merged.ContainsKey($k)) { $merged[$k] = $data[$k] } }
$global:__LanguageData = $merged
} else {
# No baseline; use as-is
$global:__LanguageData = $data
}
return $true
} catch {
Write-Verbose "Failed to load language file '$filePath': $_"
}
}
# fallback to en built-in minimal set if file missing
if ($lang -ne 'en') { Set-Language -lang 'en' } else { $global:__LanguageData = @{} }
return $false
}
function Get-Text {
param(
[Parameter(Mandatory=$true)][string]$key,
[Parameter(Mandatory=$false)][object]$args
)
$val = $null
if ($global:__LanguageData.ContainsKey($key)) { $val = $global:__LanguageData[$key] }
else { $val = $null }
if (-not $val) {
# fallback messages for a small set of keys
switch ($key) {
'SelectLanguagePrompt' { $val = "Select language: 1) English 2) Portugues (Brasil)"; break }
default { $val = $key }
}
}
if ($args) {
try {
if ($args -is [hashtable]) {
foreach ($k in $args.Keys) { $val = $val -replace "\{${k}\}", [string]$args[$k] }
} elseif ($args -is [array]) {
$val = [string]::Format($val, $args)
} else {
$val = [string]::Format($val, $args)
}
} catch {
# ignore formatting errors and return raw value
}
}
return $val
}
# Show a dynamic language selection menu based on locales/*.json
function Select-LanguageInteractive {
param(
[switch]$ValidOnly
)
$langs = Get-AvailableLanguages -ValidOnly:$ValidOnly
if (-not $langs -or $langs.Count -eq 0) {
Write-Host "No locale files found. Using English."
Set-Language -lang 'en' | Out-Null
return 'en'
}
Write-Host "Select language (by number):"
for ($i = 0; $i -lt $langs.Count; $i++) {
$mark = if ($langs[$i].IsValid) { '' } else { ' (incomplete)' }
Write-Host ("{0}) {1}{2} [{3}]" -f ($i+1), $langs[$i].Name, $mark, $langs[$i].Code)
}
$selection = Read-Host "Enter your choice (1..$($langs.Count))"
$idx = 0
if ([int]::TryParse($selection, [ref]$idx)) {
if ($idx -ge 1 -and $idx -le $langs.Count) {
$chosen = $langs[$idx-1].Code
Set-Language -lang $chosen | Out-Null
return $chosen
}
}
Write-Host "Invalid selection. Defaulting to English."
Set-Language -lang 'en' | Out-Null
return 'en'
}Finalmente, temos um script de PowerShell complexo e completo, projetado para atuar como um detector e gerenciador de conexões de rede.
Seu funcionamento começa com uma fase de preparação e verificação rigorosa. O script primeiro verifica se está sendo executado com privilégios de Administrador. Se não estiver, ele exibe um aviso e, de forma inteligente, reinicia a si mesmo com privilégios elevados para garantir que tenha permissão para executar comandos de rede e gerenciar o firewall. Em seguida, ele verifica se o módulo BurntToast, necessário para enviar notificações de área de trabalho do Windows, está instalado. Se não estiver, o script o instala automaticamente, garantindo que suas dependências sejam resolvidas antes da execução principal.
O coração do script é a função Get-ActiveConnections. Esta função executa o comando netstat do Windows para obter uma lista de texto bruto de todas as conexões de rede ativas (TCP e UDP), seus endereços locais e externos, e os IDs de processo (PID) associados. O script então analisa (faz o "parsing") cuidadosamente essa saída de texto, linha por linha, convertendo-a em objetos PowerShell estruturados. Durante essa análise, ele identifica o protocolo e diferencia o TCP (que tem um "estado", como ESTABLISHED) do UDP (que não tem). Mais importante, ele usa o PID de cada conexão para consultar o sistema com Get-Process e descobrir o nome do programa (como chrome.exe ou svchost.exe) responsável por ela, fornecendo um contexto crucial.
Com essa capacidade de coleta de dados, o script implementa recursos de segurança. Ele define listas globais para rastrear IPs (ListMaliciousIPs) e portas (ListunusualPorts) considerados suspeitos. A função Check-SuspiciousConnections usa a lista de conexões ativas e as compara com essas listas de ameaças, utilizando expressões regulares (regex) para extrair e verificar corretamente os IPs (incluindo formatos IPv4 e IPv6) e as portas dos endereços de conexão. Funções auxiliares como Add-UserDefinedLists e Remove-UserDefinedLists permitem ao usuário personalizar essas listas de ameaças.
Para ação e resposta, o script inclui as funções Block-Connection e Unblock-Connection. Elas atuam como atalhos para criar e remover regras no Firewall do Windows, permitindo que o usuário bloqueie ou desbloqueie rapidamente o tráfego de e para um IP e porta específicos.
Uma das características mais avançadas é o monitoramento em segundo plano. A função Start-Monitoring inicia um "job" (um processo em segundo plano) que é executado de forma independente. Esse job (preferencialmente um Start-ThreadJob, mais leve) entra em um loop infinito, pausando por um intervalo definido (como 30 segundos) e, em seguida, executando sua própria verificação de netstat. Ele contém uma lógica de análise interna (para ser autossuficiente) e compara as conexões encontradas com as listas de suspeitos. Se uma conexão suspeita for detectada, e se as notificações estiverem habilitadas (Enable-ToastNotifications), o script usa o módulo BurntToast para enviar uma notificação de alerta na área de trabalho do usuário. As funções Stop-Monitoring e Get-MonitorStatus permitem ao usuário gerenciar esse "vigia" em segundo plano.
Finalmente, o script une tudo isso em uma interface de usuário interativa baseada em menu. Ele primeiro carrega um script de idioma (connector-language.ps1, que você me mostrou anteriormente) e exibe um menu para o usuário selecionar o idioma. Em seguida, ele entra em um loop while principal, exibindo um menu de opções traduzidas (usando Get-Text). O usuário pode então, usando um switch, escolher:
- Listar todas as conexões ativas.
- Entrar em um submenu para pesquisar conexões (por nome de programa ou por IP/Porta).
- Entrar em um submenu para bloquear ou desbloquear conexões.
- Entrar em um submenu de "recursos suspeitos" para habilitar/desabilitar notificações, ver conexões suspeitas, gerenciar as listas de IPs/Portas e iniciar/parar o monitor em segundo plano.
O script continua em execução até que o usuário escolha a opção "0" para sair, momento em que ele para o monitor em segundo plano e encerra.
# Connector Detector PowerShell Script
# This script detects and lists all IP and PORT connectors on a Windows system.
# It can detect both TCP and UDP connectors coming from outside of the system and inside the system.
# It provides details about each connector including its state and associated process.
# It also includes error handling to manage potential issues during execution and logging.
# Includes comments for clarity.
# System Requirements: Windows OS with PowerShell installed.
# Basic functionality: Lists all active TCP and UDP connections with their states and associated processes using netstat and lets the user select a specific connection for detailed information or search for specific program using name or ip/port.
# Extra: when detects an strange and suspected connection. send an toast alert (IF ENABLED) to the user and if clicked lets user to block/unblock the connection.
# Ensure script runs with administrative privileges
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Warning "You do not have Administrator rights to run this script! re-running this script as an Administrator!"
Start-Process powershell -Verb RunAs -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""
exit
}
# Ensure the BurntToast module is installed for toast notifications
if (-not (Get-Module -ListAvailable -Name BurntToast)) {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
Install-Module -Name BurntToast -Force
}
# Function to get all active connections
function Get-ActiveConnections {
try {
# Parse netstat output into structured objects so searches/filtering work correctly
$raw = netstat -a -n -o 2>$null | Select-String "TCP|UDP"
# initialize an array to hold results
$results = @()
foreach ($entry in $raw) {
# split the line into parts
$line = $entry.ToString().Trim()
$parts = $line -split "\s+"
# check if we have enough parts
if ($parts.Length -ge 3) {
# extract relevant fields
$protocol = $parts[0]
$localAddress = $parts[1]
$foreignAddress = $parts[2]
if ($protocol -eq 'TCP') {
# TCP protocol has a state
$state = if ($parts.Length -ge 4) { $parts[3] } else { 'N/A' }
$piid = if ($parts.Length -ge 5) { $parts[4] } else { $null }
} else {
# UDP protocol does not have a state
$state = 'N/A'
$piid = if ($parts.Length -ge 4) { $parts[3] } else { $null }
}
# get process name from PID
$processName = 'Unknown'
if ($piid -and ($piid -as [int])) {
try {
$p = Get-Process -Id $piid -ErrorAction Stop
$processName = $p.ProcessName
} catch {
# leave as Unknown
}
}
# create a custom object for each connection
$obj = [PSCustomObject]@{
Protocol = $protocol
LocalAddress = $localAddress
ForeignAddress = $foreignAddress
State = $state
PID = $piid
ProcessName = $processName
}
# add the object to results
$results += $obj
}
}
# return the results
return $results
} catch {
# error handling
Write-Error "Error retrieving active connections: $_"
}
}
# function to block a specific connection (placeholder, actual implementation may vary)
function Block-Connection {
# parameters
param (
[string]$ip,
[int]$port
)
try {
# Placeholder for blocking logic, e.g., using Windows Firewall commands
Write-Output "Blocking connection to $ip on port $port"
New-NetFirewallRule -DisplayName "Block $ip $port" -Direction Outbound -RemoteAddress $ip -RemotePort $port -Protocol TCP -Action Block
} catch {
# error handling
Write-Error "Error blocking connection: $_"
}
}
# function to unblock a specific connection (placeholder, actual implementation may vary)
function Unblock-Connection {
# parameters
param (
[string]$ip,
[int]$port
)
try {
# Placeholder for unblocking logic, e.g., using Windows Firewall commands
Write-Output "Unblocking connection to $ip on port $port"
Remove-NetFirewallRule -DisplayName "Block $ip $port"
} catch {
# error handling
Write-Error "Error unblocking connection: $_"
}
}
$ListMaliciousIPs = @("123.456.789.0", "987.654.321.0")
$ListunusualPorts = @(6666, 12345, 54321)
# Add User Defined Lists for Malicious IPs and Unusual Ports
function Add-UserDefinedLists {
param (
[string[]]$maliciousIPs,
[int[]]$unusualPorts
)
$global:ListMaliciousIPs += $maliciousIPs
$global:ListunusualPorts += $unusualPorts
}
# Remove User Defined Lists for Malicious IPs and Unusual Ports
function Remove-UserDefinedLists {
param (
[string[]]$maliciousIPs,
[int[]]$unusualPorts
)
$global:ListMaliciousIPs = $global:ListMaliciousIPs | Where-Object { $maliciousIPs -notcontains $_ }
$global:ListunusualPorts = $global:ListunusualPorts | Where-Object { $unusualPorts -notcontains $_ }
}
# Function to check for suspicious connections
function Check-SuspiciousConnections {
try {
$connections = Get-ActiveConnections
$suspiciousConnections = @()
# Define criteria for suspicious connections
# Example: connections to known malicious IPs or unusual ports
$maliciousIPs = $ListMaliciousIPs
$unusualPorts = $ListunusualPorts
foreach ($conn in $connections) {
$foreignIP = $null
$foreignPort = $null
$portStr = ''
if ($conn.ForeignAddress) {
$foreignAddress = $conn.ForeignAddress.ToString()
# Use a regex to capture a trailing :port (handles IPv4, IPv6 with brackets and wildcards like '*')
$m = [regex]::Match($foreignAddress, '(:\d+)$')
if ($m.Success) {
$portStr = $m.Groups[1].Value.TrimStart(':')
$parsedPort = 0
if ([int]::TryParse($portStr, [ref]$parsedPort)) {
$foreignPort = $parsedPort
}
}
# Extract IP part by removing the trailing :port if present
if ($portStr -ne '') {
$foreignIP = $foreignAddress.Substring(0, $foreignAddress.Length - ($portStr.Length + 1))
} else {
$foreignIP = $foreignAddress
}
# Normalize IPv6 bracket notation like [fe80::1]
if ($foreignIP -match '^\[(.*)\]$') {
$foreignIP = $Matches[1]
}
}
if (($foreignIP -and ($maliciousIPs -contains $foreignIP)) -or ($foreignPort -and ($unusualPorts -contains $foreignPort))) {
$suspiciousConnections += $conn
}
}
return $suspiciousConnections
} catch {
Write-Error "Error checking for suspicious connections: $_"
}
}
$enableToastNotifications = $false
# enable/disable toast notifications (placeholder, actual implementation may vary)
function Enable-ToastNotifications {
param (
[bool]$enable
)
try {
if ($enable) {
Write-Output "Toast notifications enabled."
$enableToastNotifications = $true
} else {
Write-Output "Toast notifications disabled."
$enableToastNotifications = $false
}
} catch {
Write-Error "Error setting toast notification preference: $_"
}
}
# Toast notification function (placeholder, actual implementation may vary)
function Show-ToastNotification {
param (
[string]$title,
[string]$message
)
try {
if (-not $enableToastNotifications) {
return
}
# Placeholder for toast notification logic using BurntToast module
New-BurntToastNotification -Text $title, $message
} catch {
Write-Error "Error showing toast notification: $_"
}
}
## Background monitor control functions
## Uses Start-ThreadJob with an inline ScriptBlock so the job does not depend on parent runspace functions.
function Start-Monitoring {
param(
[int]$IntervalSeconds = 30,
[string]$JobName = 'ConnectorDetectorMonitor'
)
# Check for existing job
$existing = Get-Job -Name $JobName -ErrorAction SilentlyContinue
if ($existing -and ($existing.State -eq 'Running' -or $existing.State -eq 'NotStarted')) {
Write-Output "Monitor already running as job '$JobName' (State: $($existing.State))."
return
}
# Prefer Start-ThreadJob when available (more efficient). Otherwise fall back to Start-Job.
if (Get-Command -Name Start-ThreadJob -ErrorAction SilentlyContinue) {
# Capture lists and notification preference at job start using $using:
$job = Start-ThreadJob -Name $JobName -ScriptBlock {
$malicious = $using:ListMaliciousIPs
$unusualPorts = $using:ListunusualPorts
$toastEnabled = $using:enableToastNotifications
while ($true) {
Start-Sleep -Seconds $using:IntervalSeconds
try {
$raw = netstat -a -n -o 2>$null | Select-String "TCP|UDP"
} catch {
continue
}
$suspicious = @()
foreach ($entry in $raw) {
$line = $entry.ToString().Trim()
$parts = $line -split "\s+"
if ($parts.Length -lt 3) { continue }
$protocol = $parts[0]
$local = $parts[1]
$foreign = $parts[2]
# Extract port
$m = [regex]::Match($foreign, '(:\d+)$')
$port = $null
if ($m.Success) {
$portStr = $m.Groups[1].Value.TrimStart(':')
$parsed = 0
if ([int]::TryParse($portStr, [ref]$parsed)) { $port = $parsed }
}
# Extract IP
if ($m.Success) {
$ip = $foreign.Substring(0, $foreign.Length - ($m.Groups[1].Value.Length))
} else {
$ip = $foreign
}
if ($ip -match '^\[(.*)\]$') { $ip = $Matches[1] }
$isMalicious = ($ip -and ($malicious -contains $ip))
$isUnusualPort = ($port -and ($unusualPorts -contains $port))
if ($isMalicious -or $isUnusualPort) {
$procName = 'Unknown'
# try to get PID if present
$procId = $null
if ($protocol -eq 'TCP') {
if ($parts.Length -ge 5) { $procId = $parts[4] }
} else {
if ($parts.Length -ge 4) { $procId = $parts[3] }
}
if ($procId -and ($procId -as [int])) {
try { $p = Get-Process -Id $procId -ErrorAction Stop; $procName = $p.ProcessName } catch {}
}
$suspicious += [PSCustomObject]@{
Protocol = $protocol
LocalAddress = $local
ForeignAddress = $foreign
Port = $port
IP = $ip
PID = $procId
ProcessName = $procName
}
}
}
if ($suspicious.Count -gt 0) {
foreach ($conn in $suspicious) {
$title = "Suspicious Connection Detected"
$message = "Protocol: $($conn.Protocol)`nLocal: $($conn.LocalAddress)`nForeign: $($conn.ForeignAddress)`nProcess: $($conn.ProcessName)"
if ($toastEnabled -and (Get-Command -Name New-BurntToastNotification -ErrorAction SilentlyContinue)) {
try { New-BurntToastNotification -Text $title, $message } catch {}
} else {
# write to job output (can be inspected with Receive-Job)
Write-Output $title
Write-Output $message
}
}
}
}
} -ArgumentList $IntervalSeconds
} else {
# Fallback to Start-Job (works on systems without ThreadJob). Pass required data via ArgumentList.
$job = Start-Job -Name $JobName -ScriptBlock {
param($malicious, $unusualPorts, $toastEnabled, $interval)
while ($true) {
Start-Sleep -Seconds $interval
try {
$raw = netstat -a -n -o 2>$null | Select-String "TCP|UDP"
} catch {
continue
}
$suspicious = @()
foreach ($entry in $raw) {
$line = $entry.ToString().Trim()
$parts = $line -split "\s+"
if ($parts.Length -lt 3) { continue }
$protocol = $parts[0]
$local = $parts[1]
$foreign = $parts[2]
$m = [regex]::Match($foreign, '(:\d+)$')
$port = $null
if ($m.Success) {
$portStr = $m.Groups[1].Value.TrimStart(':')
$parsed = 0
if ([int]::TryParse($portStr, [ref]$parsed)) { $port = $parsed }
}
if ($m.Success) {
$ip = $foreign.Substring(0, $foreign.Length - ($m.Groups[1].Value.Length))
} else {
$ip = $foreign
}
if ($ip -match '^\[(.*)\]$') { $ip = $Matches[1] }
$isMalicious = ($ip -and ($malicious -contains $ip))
$isUnusualPort = ($port -and ($unusualPorts -contains $port))
if ($isMalicious -or $isUnusualPort) {
$procName = 'Unknown'
$procId = $null
if ($protocol -eq 'TCP') {
if ($parts.Length -ge 5) { $procId = $parts[4] }
} else {
if ($parts.Length -ge 4) { $procId = $parts[3] }
}
if ($procId -and ($procId -as [int])) {
try { $p = Get-Process -Id $procId -ErrorAction Stop; $procName = $p.ProcessName } catch {}
}
$suspicious += [PSCustomObject]@{
Protocol = $protocol
LocalAddress = $local
ForeignAddress = $foreign
Port = $port
IP = $ip
PID = $procId
ProcessName = $procName
}
}
}
if ($suspicious.Count -gt 0) {
foreach ($conn in $suspicious) {
$title = "Suspicious Connection Detected"
$message = "Protocol: $($conn.Protocol)`nLocal: $($conn.LocalAddress)`nForeign: $($conn.ForeignAddress)`nProcess: $($conn.ProcessName)"
if ($toastEnabled -and (Get-Command -Name New-BurntToastNotification -ErrorAction SilentlyContinue)) {
try { New-BurntToastNotification -Text $title, $message } catch {}
} else {
Write-Output $title
Write-Output $message
}
}
}
}
} -ArgumentList ($ListMaliciousIPs, $ListunusualPorts, $enableToastNotifications, $IntervalSeconds)
}
if ($job) { Write-Output "Started monitor job '$($job.Name)' (Id: $($job.Id))." }
}
function Stop-Monitoring {
param([string]$JobName = 'ConnectorDetectorMonitor')
$job = Get-Job -Name $JobName -ErrorAction SilentlyContinue
if ($job) {
Stop-Job -Job $job -ErrorAction SilentlyContinue
Remove-Job -Job $job -ErrorAction SilentlyContinue
Write-Output "Stopped and removed monitor job '$JobName'."
} else {
Write-Output "No monitor job named '$JobName' was found."
}
}
function Get-MonitorStatus {
param([string]$JobName = 'ConnectorDetectorMonitor')
$job = Get-Job -Name $JobName -ErrorAction SilentlyContinue
if ($job) { return $job.State } else { return 'NotFound' }
}
function Monitor-SuspiciousConnections { Start-Monitoring }
# Main script execution (Let user choose an action)
# 0 - Exit
# 1 - List all active connections
# 2 - Get details of a specific connection
# 2.1 - Search for a specific program by name
# 2.2 - Search for a specific IP/Port
# 3 - Block/Unblock a specific connection
# 3.1 - Block a specific connection
# 3.2 - Unblock a specific connection
# 4 - Check for suspicious connections and alert user
# 4.1 - Activate toast notification for suspicious connections
# 4.2 - Let user block/unblock from notification
# 4.3 - Add User Defined Lists for Malicious IPs and Unusual Ports
# 4.4 - Remove User Defined Lists for Malicious IPs and Unusual Ports
. "$PSScriptRoot\connector-language.ps1"
# Dynamic language discovery and selection (lists locales/*.json). Only valid locales are shown.
Select-LanguageInteractive -ValidOnly | Out-Null
$exitScript = $false
while (-not $exitScript) {
Write-Output (Get-Text -key 'lineBreak')
Write-Output (Get-Text -key 'MenuLine')
Write-Output (Get-Text -key 'TitleHeader')
Write-Output (Get-Text -key 'MenuLine')
$connections = Get-ActiveConnections
if ($connections) {
# check for suspicious connections (on-demand)
$suspiciousConnections = Check-SuspiciousConnections
# show options to the user
Write-Output (Get-Text -key 'SelectOptionPrompt')
Write-Output (Get-Text -key 'Option1')
Write-Output (Get-Text -key 'Option2')
Write-Output (Get-Text -key 'Option3')
Write-Output (Get-Text -key 'Option4')
Write-Output (Get-Text -key 'Option0')
Write-Output (Get-Text -key 'lineBreak')
$userChoice = Read-Host (Get-Text -key 'EnterChoice')
switch ($userChoice) {
"1" {
# List all active connections as a table
if ($connections.Count -gt 0) {
# Display connections sorted by Protocol and LocalAddress
$connections | Sort-Object Protocol,LocalAddress | Format-Table -AutoSize
} else {
# No connections found
Write-Output (Get-Text -key 'NoActiveConnections')
}
}
"2" {
# show options 2.1 and 2.2 as 1 and 2 sub-options
Write-Output (Get-Text -key 'lineBreak')
Write-Output (Get-Text -key 'SubOptionPrompt')
Write-Output (Get-Text -key 'Sub2Option1')
Write-Output (Get-Text -key 'Sub2Option2')
$subChoice = Read-Host (Get-Text -key 'EnterSubChoice12')
# Handle sub-options
switch ($subChoice) {
"1" {
# Search for a specific program by name
$programName = Read-Host (Get-Text -key 'EnterProgramName')
if ([string]::IsNullOrWhiteSpace($programName)) {
# Empty input handling
Write-Output (Get-Text -key 'ProvideNonEmptyProgramName')
} else {
$matchingConnections = $connections | Where-Object { $_.ProcessName -match $programName }
if ($matchingConnections.Count -gt 0) {
# Matches found
Write-Output "Connections for program '$programName':"
$matchingConnections | Format-Table -AutoSize
} else {
# No matches found
Write-Output "No connections found for program '$programName'."
}
}
}
"2" {
# Search for a specific IP/Port
$ipOrPort = Read-Host (Get-Text -key 'EnterIPOrPort')
if ([string]::IsNullOrWhiteSpace($ipOrPort)) {
Write-Output (Get-Text -key 'ProvideNonEmptyIPPort')
} else {
$matchingConnections = $connections | Where-Object {
# Check if LocalAddress or ForeignAddress contains the IP/Port or if PID matches
($_.LocalAddress -match $ipOrPort) -or ($_.ForeignAddress -match $ipOrPort) -or ($_.PID -eq $ipOrPort)
}
if ($matchingConnections.Count -gt 0) {
# Matches found
Write-Output "Connections for IP/Port '$ipOrPort':"
$matchingConnections | Format-Table -AutoSize
} else {
# No matches found
Write-Output "No connections found for IP/Port '$ipOrPort'."
}
}
}
default {
# Invalid sub-choice
Write-Output (Get-Text -key 'InvalidChoice')
}
}
}
"3" {
# show options 3.1 and 3.2 as 1 and 2 sub-options
Write-Output (Get-Text -key 'lineBreak')
Write-Output (Get-Text -key 'SubOptionPrompt')
Write-Output (Get-Text -key 'Sub3Option1')
Write-Output (Get-Text -key 'Sub3Option2')
$subChoice = Read-Host (Get-Text -key 'EnterSubChoice12')
# Handle sub-options
switch ($subChoice) {
"1" {
# Block a specific connection
$ipToBlock = Read-Host (Get-Text -key 'EnterIPToBlock')
$portToBlock = Read-Host (Get-Text -key 'EnterPortToBlock')
if ([int]::TryParse($portToBlock, [ref]$null)) {
Block-Connection -ip $ipToBlock -port [int]$portToBlock
} else {
Write-Output (Get-Text -key 'InvalidPortNumber')
}
}
"2" {
# Unblock a specific connection
$ipToUnblock = Read-Host (Get-Text -key 'EnterIPToUnblock')
$portToUnblock = Read-Host (Get-Text -key 'EnterPortToUnblock')
if ([int]::TryParse($portToUnblock, [ref]$null)) {
Unblock-Connection -ip $ipToUnblock -port [int]$portToUnblock
} else {
Write-Output (Get-Text -key 'InvalidPortNumber')
}
}
default {
# Invalid sub-choice
Write-Output (Get-Text -key 'InvalidChoice')
}
}
}
"4" {
Write-Output (Get-Text -key 'lineBreak')
Write-Output (Get-Text -key 'Sub4Option1')
Write-Output (Get-Text -key 'Sub4Option2')
Write-Output (Get-Text -key 'Sub4Option3')
Write-Output (Get-Text -key 'Sub4Option4')
Write-Output (Get-Text -key 'Sub4Option5')
Write-Output (Get-Text -key 'Sub4Option6')
Write-Output (Get-Text -key 'Sub4Option7')
$suspiciousChoice = Read-Host (Get-Text -key 'EnterChoice1to7')
switch ($suspiciousChoice) {
"1" {
# Enable/Disable toast notifications
$toastChoice = Read-Host (Get-Text -key 'EnterEnableDisableToast')
if ($toastChoice -eq "enable") {
Enable-ToastNotifications -enable $true
} elseif ($toastChoice -eq "disable") {
Enable-ToastNotifications -enable $false
} else {
Write-Output (Get-Text -key 'InvalidEnableDisableChoice')
}
}
"2" {
# View suspicious connections
if ($suspiciousConnections.Count -gt 0) {
Write-Output (Get-Text -key 'SuspiciousConnectionsDetected')
$suspiciousConnections | Format-Table -AutoSize
} else {
Write-Output (Get-Text -key 'NoSuspiciousConnections')
}
}
"3" {
# Add User Defined Lists for Malicious IPs and Unusual Ports
$newMaliciousIPs = Read-Host (Get-Text -key 'EnterMaliciousIPsToAdd')
$newUnusualPorts = Read-Host (Get-Text -key 'EnterUnusualPortsToAdd')
$maliciousIPArray = $newMaliciousIPs -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
$unusualPortArray = $newUnusualPorts -split "," | ForEach-Object {
$portTrimmed = $_.Trim()
if ([int]::TryParse($portTrimmed, [ref]$null)) {
[int]$portTrimmed
}
} | Where-Object { $_ -ne $null }
Add-UserDefinedLists -maliciousIPs $maliciousIPArray -unusualPorts $unusualPortArray
Write-Output (Get-Text -key 'UserDefinedListsUpdated')
}
"4" {
# Remove User Defined Lists for Malicious IPs and Unusual Ports
$removeMaliciousIPs = Read-Host (Get-Text -key 'EnterMaliciousIPsToRemove')
$removeUnusualPorts = Read-Host (Get-Text -key 'EnterUnusualPortsToRemove')
$maliciousIPArray = $removeMaliciousIPs -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
$unusualPortArray = $removeUnusualPorts -split "," | ForEach-Object {
$portTrimmed = $_.Trim()
if ([int]::TryParse($portTrimmed, [ref]$null)) {
[int]$portTrimmed
}
} | Where-Object { $_ -ne $null }
Remove-UserDefinedLists -maliciousIPs $maliciousIPArray -unusualPorts $unusualPortArray
Write-Output (Get-Text -key 'UserDefinedListsUpdated')
}
"5" {
# Start background monitor
$intervalInput = Read-Host (Get-Text -key 'EnterMonitorInterval')
# default
$intervalNum = 30
if (-not [int]::TryParse($intervalInput, [ref]$intervalNum)) { $intervalNum = 30 }
Start-Monitoring -IntervalSeconds $intervalNum
}
"6" {
# Stop background monitor
Stop-Monitoring
}
"7" {
# Monitor status
$status = Get-MonitorStatus
Write-Output (Get-Text -key 'MonitorStatus' -args @($status))
}
default {
Write-Output (Get-Text -key 'InvalidChoice')
}
}
}
"0" {
# Exit the script
Write-Output "Exiting the script. Stopping monitor if running..."
Stop-Monitoring
$exitScript = $true
}
default {
# Invalid choice
Write-Output (Get-Text -key 'InvalidChoice')
}
}
} else {
# No active connections found
Write-Output (Get-Text -key 'NoActiveConnections')
}
}O Connector Detector nasceu de uma necessidade prática: fechar a lacuna clássica entre ver um problema com netstat e agir sobre ele com o Firewall do Windows. O tempo perdido alternando entre consoles é um luxo que, em um cenário de resposta a incidentes, simplesmente não podemos ter.
Este projeto é uma prova de como o PowerShell evoluiu de uma simples linguagem de script para uma plataforma robusta de desenvolvimento de ferramentas. Ao combinar análise de conexões em tempo real, um motor de detecção proativo com alertas e uma arquitetura de software pensada para ser modular e multilíngue, o connector-detector.ps1 transforma o terminal em um verdadeiro centro de controle de rede.
Para administradores de sistemas e analistas de segurança, esta ferramenta oferece o que mais precisamos: velocidade, controle e a capacidade de passar da detecção ao bloqueio em questão de segundos, não minutos.
Espero que este utilitário seja tão valioso para o seu fluxo de trabalho quanto foi desafiador e recompensador desenvolvê-lo. Sinta-se à vontade para explorar o projeto no GitHub, testá-lo em seu ambiente e, quem sabe, contribuir com novas ideias ou traduções.
