diff --git a/AGENTS.md b/AGENTS.md
index aa2ab51..58df95f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -132,7 +132,7 @@ O programa roda no notebook do técnico, ligado à rede do cliente. Isso molda o
## Dados de cliente
-O relatório traz dado da rede do cliente e pode trazer dado pessoal (nome de celular com nome de pessoa, MAC de aparelho pessoal). Ele fica só na máquina do técnico, em `Documentos\MapNet - MT`, e nunca entra no repositório. As regras de aviso, guarda e descarte saem da análise da `legal-br` (ver `docs/superpowers/pendencias.md`).
+O relatório traz dado da rede do cliente e pode trazer dado pessoal (nome de celular com nome de pessoa, MAC de aparelho pessoal). Ele fica só na máquina do técnico, na pasta que ele escolhe ao salvar (a janela sugere `Documentos\MapNet - MT`), e nunca entra no repositório. As regras de aviso, guarda e descarte saem da análise da `legal-br` (ver `docs/superpowers/pendencias.md`).
## Portões antes de cada commit
diff --git a/Directory.Build.props b/Directory.Build.props
index 49a979c..08bf641 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,7 +4,7 @@
enableenabletrue
- 0.2.5
+ 0.3.0Manfred Heil JuniorMT - Manfred TecnologiaMapNet - MT
diff --git a/README.md b/README.md
index 9b0d9d0..d017869 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ Página do programa: **https://mapnet.manfred.com.br**. Download da versão mais
Software livre, sob licença [GPL-3.0](LICENSE).
-Não é scanner de vulnerabilidade: o programa não testa senha, não contorna autenticação e não explora vulnerabilidade. Para achar os equipamentos, ele manda ping, pedidos ARP e consultas de nome (DNS, NetBIOS e mDNS) aos endereços da sub-rede escolhida.
+Não é scanner de vulnerabilidade: o programa não testa senha, não contorna autenticação e não explora vulnerabilidade. Para achar os equipamentos, ele manda ping, pedidos ARP e consultas de nome (DNS, NetBIOS e mDNS) aos endereços da sub-rede escolhida. Com a opção **Verificar portas** ligada, ele também abre e fecha uma conexão TCP em cada porta da lista, nos equipamentos que encontrou, sem enviar dados.
**Uso autorizado.** O MapNet - MT serve para inventariar equipamentos em redes nas quais você tem autorização para fazer a verificação. Confira o intervalo de endereços antes de iniciar e respeite as regras da rede e dos dispositivos examinados.
@@ -24,7 +24,8 @@ As regras do projeto estão no [`AGENTS.md`](AGENTS.md) e o desenho em [`docs/su
| 4 | Console de diagnóstico: ping, tracert, DNS, ARP, conexões e rotas (sai como v0.2.3) | `diagnostico` | [#17](https://github.com/manfredjr/mapnet/pull/17) | Publicada na v0.2.5 |
| 5 | Ações de manutenção com elevação sob demanda, na aba Manutenção do console (sai como v0.2.4) | `manutencao` | [#18](https://github.com/manfredjr/mapnet/pull/18) | Publicada na v0.2.5 |
| 6 | Painel de detalhe do host, com ping, tracert, navegador, área de trabalho remota e pasta compartilhada (sai como v0.2.5) | `detalhe-host` | [#19](https://github.com/manfredjr/mapnet/pull/19) | Publicada na v0.2.5 |
-| 7 em diante | Portas TCP, identificação leve, classificação e relatório completo em HTML, XML e CSV | a definir | - | A fazer |
+| 7 | Portas TCP comuns abertas, com lista configurável, desligadas por padrão e com confirmação por rede (sai como v0.3.0) | `portas` | Plano em [#26](https://github.com/manfredjr/mapnet/pull/26), código em [#31](https://github.com/manfredjr/mapnet/pull/31) | Em revisão |
+| 8 a 10 | Identificação leve, classificação por tipo de equipamento e relatório também em XML e CSV (até a v1.0.0) | a definir | - | A fazer |
A versão 0.2 segue o desenho em [`docs/superpowers/specs/2026-09-25-mapnet-0.2-painel-do-tecnico-design.md`](docs/superpowers/specs/2026-09-25-mapnet-0.2-painel-do-tecnico-design.md).
@@ -38,11 +39,12 @@ O programa é um arquivo só, `mapnet.exe`. Não precisa instalar nada no comput
1. Abra o `mapnet.exe`.
2. Na faixa verde do topo, escolha a interface de rede. A primeira da lista é a que tem gateway, quase sempre a certa. A coluna **Minha máquina** mostra o computador (nome, grupo de trabalho ou domínio, usuário), a placa (tipo, MAC, velocidade, MTU), os endereços (IPv4, IPv6, gateway, DNS, sufixo e a sub-rede que vai ser varrida) e o DHCP com a validade da concessão. No Wi-Fi, mostra também a rede, a banda, o canal e o sinal.
-3. O botão **Consultar IP público** pergunta o IP ao serviço `1.1.1.1`, do Cloudflare, e só roda com o clique. Se o relatório for gravado depois da consulta, o IP público vai junto.
-4. Clique em **Iniciar varredura**. Clicar numa linha da tabela abre, ao lado, o detalhe do host: MAC, fabricante, os nomes pelo DNS reverso, NetBIOS e mDNS, o grupo de trabalho, o ping com o TTL e uma pista do sistema pelo TTL. Os botões do detalhe fazem ping contínuo e tracert no console, abrem http e https no navegador, a área de trabalho remota e a pasta compartilhada, e copiam os dados. Um novo clique na mesma linha, a tecla Esc ou o botão **Fechar** fecham o detalhe. A tabela vai se enchendo enquanto os hosts respondem, e o mesmo botão vira **Cancelar**. O campo **Filtrar** procura por IP, nome, MAC, fabricante ou observação, e cada coluna ordena com um clique no título.
-5. O console embaixo registra o andamento e os avisos, e a barra de estado mostra o resumo. O console tem também uma aba para cada ferramenta: Ping (normal ou contínuo), Tracert, DNS com escolha do servidor, ARP, Conexões e Rotas. Os campos de host e de servidor DNS já vêm com o gateway e o DNS da interface. Enquanto a ferramenta roda, **Executar** vira **Parar**. **Copiar** e **Limpar** valem para a aba aberta.
-6. No fim, o relatório é gravado em `Documentos\MapNet - MT`. O botão **Relatório** abre o relatório no navegador, salva em outro lugar ou abre a pasta.
-7. A aba **Manutenção** tem quatro botões, que rodam os comandos oficiais do Windows e mostram a saída ali mesmo: **Limpar cache DNS** (`ipconfig /flushdns`), **Liberar e renovar IP** (`ipconfig /release` e `/renew`), **Limpar tabela ARP** (`netsh interface ip delete arpcache`) e **Resetar Winsock e TCP/IP** (`netsh winsock reset` e `netsh int ip reset`). O programa continua abrindo como usuário comum. Quando a ação pede administrador, o Windows mostra a tela do UAC só para aquele comando. Renovar o IP e resetar o Winsock derrubam a rede por alguns segundos e pedem confirmação antes. O reset só vale depois de reiniciar o computador.
+3. O botão **Consultar IP público**, na faixa do topo, pergunta o IP ao serviço `1.1.1.1`, do Cloudflare, e só roda com o clique. Se o relatório for gravado depois da consulta, o IP público vai junto.
+4. Para saber também as portas abertas de cada equipamento, marque **Verificar portas** acima da tabela. A **Lista** vem com `padrão`, as 24 portas comuns (a dica do campo mostra quais são), ou aceita até 100 portas separadas por vírgula, como `80,443,9100`. Os aparelhos com MAC aleatório, quase sempre celulares e notebooks pessoais, ficam de fora, a não ser que você marque **Incluir aparelhos com MAC aleatório**. Na primeira varredura com portas em cada rede, o programa diz o que vai enviar e pede confirmação. Use só em rede que você tem autorização para verificar.
+5. Clique em **Iniciar varredura**. Clicar numa linha da tabela abre, ao lado, o detalhe do host: MAC, fabricante, os nomes pelo DNS reverso, NetBIOS e mDNS, o grupo de trabalho, o ping com o TTL, uma pista do sistema pelo TTL e as portas abertas, com o serviço de cada uma. Os botões do detalhe fazem ping contínuo e tracert no console, abrem http e https no navegador (na 8080, 8000 ou 8443 quando só ela está aberta, e desligados quando as portas web foram verificadas e estão fechadas), a área de trabalho remota e a pasta compartilhada, e copiam os dados. Um novo clique na mesma linha, a tecla Esc ou o botão **Fechar** fecham o detalhe. A tabela vai se enchendo enquanto os hosts respondem, e o mesmo botão vira **Cancelar**. O campo **Filtrar** procura por IP, nome, MAC, fabricante, observação ou número de porta aberta (`9100` lista as impressoras), e cada coluna ordena com um clique no título.
+6. O console embaixo registra o andamento e os avisos, e a barra de estado mostra o resumo. O console tem também uma aba para cada ferramenta: Ping (normal ou contínuo), Tracert, DNS com escolha do servidor, ARP, Conexões e Rotas. Os campos de host e de servidor DNS já vêm com o gateway e o DNS da interface. Enquanto a ferramenta roda, **Executar** vira **Parar**. **Copiar** e **Limpar** valem para a aba aberta.
+7. No fim, clique em **Salvar relatório** e escolha onde gravar o arquivo HTML. A janela abre em `Documentos\MapNet - MT` com um nome pronto, e da segunda vez abre na pasta usada por último. O programa não grava sozinho: fechar a janela com a varredura sem salvar pergunta antes. O botão **Abrir** abre o relatório salvo no navegador ou a pasta dele.
+8. A aba **Manutenção** tem quatro botões, que rodam os comandos oficiais do Windows e mostram a saída ali mesmo: **Limpar cache DNS** (`ipconfig /flushdns`), **Liberar e renovar IP** (`ipconfig /release` e `/renew`), **Limpar tabela ARP** (`netsh interface ip delete arpcache`) e **Resetar Winsock e TCP/IP** (`netsh winsock reset` e `netsh int ip reset`). O programa continua abrindo como usuário comum. Quando a ação pede administrador, o Windows mostra a tela do UAC só para aquele comando. Renovar o IP e resetar o Winsock derrubam a rede por alguns segundos e pedem confirmação antes. O reset só vale depois de reiniciar o computador.
### Pela linha de comando
@@ -50,6 +52,7 @@ O programa é um arquivo só, `mapnet.exe`. Não precisa instalar nada no comput
mapnet --interfaces
mapnet --varrer
mapnet --varrer --interface 2 --saida C:\Relatorios --abrir
+mapnet --varrer --portas 80,443,9100
mapnet --ajuda
```
@@ -63,6 +66,8 @@ mapnet --ajuda
| `--tempo-ping ` | Espera de cada ping, de 100 a 10000. Padrão: 1000 |
| `--paralelo ` | Endereços sondados ao mesmo tempo, de 1 a 256. Padrão: 64 |
| `--sem-arp` | Descobre hosts só pelo ping |
+| `--portas [lista]` | Verifica as portas TCP dos hosts encontrados, só abrindo e fechando a conexão. Sem lista, usa as 24 portas comuns; com lista, até 100 portas, como `22,80,443`. Desligado sem esta opção |
+| `--incluir-mac-aleatorio` | Com `--portas`, inclui os aparelhos com MAC aleatório, que ficam de fora por padrão |
O `.exe` é de janela, então o Prompt de Comando devolve o cursor antes de o programa terminar. Para esperar o fim, use `start /wait mapnet --varrer` no Prompt de Comando ou termine a linha com `| Out-Host` no PowerShell. Redirecionar para arquivo (`> saida.txt`) funciona direto, em UTF-8.
@@ -104,6 +109,8 @@ Se o valor bater com o do `.txt`, o executável é exatamente o que foi publicad
- **Nome:** DNS reverso, NetBIOS (o mesmo do `nbtstat -A`, que traz também o grupo de trabalho ou domínio) e mDNS (nomes `.local` de celulares, Macs, impressoras e aparelhos Linux).
- **Relatório HTML:** arquivo único, com resumo da varredura, cartões de contagem, tabela filtrável e ordenável, detalhe de cada host ao clicar na linha e a lista de fabricantes.
+- **Portas (desde a v0.3.0, desligadas por padrão):** para cada host encontrado, abre e fecha a conexão TCP em cada porta da lista, sem enviar nenhum dado, com até 128 conexões ao mesmo tempo, 8 por host e 800 ms de espera por porta. Porta que aceita a conexão aparece como aberta. Este computador e os aparelhos com MAC aleatório ficam de fora, e o motivo aparece no detalhe. O relatório ganha a coluna Portas e diz quais portas foram verificadas. A identificação do serviço (título da página, banner, certificado) fica para a v0.4.0.
+
Sub-rede maior que /22 (1022 endereços) é varrida só no bloco /22 em volta do IP do computador, com aviso no relatório. A faixa manual fica para uma próxima versão.
## Primeira vez na máquina
diff --git a/docs/superpowers/pendencias.md b/docs/superpowers/pendencias.md
index d6bdc80..31cb2d2 100644
--- a/docs/superpowers/pendencias.md
+++ b/docs/superpowers/pendencias.md
@@ -54,9 +54,17 @@ Fechado em parte na fatia 3 (0.2.2). No tamanho padrão da janela, IP, Origem, M
| Item | Motivo | O que fecha |
|---|---|---|
-| Portas abertas no detalhe | O desenho prevê as portas no detalhe quando a fatia de portas existir | Versão 1, fatia de portas TCP |
+| Portas abertas no detalhe | Fechado na fatia 7 (0.3.0): o detalhe mostra as portas abertas com o serviço, ou o motivo de não terem sido verificadas | Fechado |
| Esc e novo clique na linha testados só no modelo | O agente não teve acesso ao teclado e ao mouse na janela de teste. A lógica de fechar está coberta por teste, e a ligação na janela é curta | O Manfred abrir e fechar o detalhe com Esc e com novo clique |
+## Fatia 7: portas TCP
+
+| Item | Motivo | O que fecha |
+|---|---|---|
+| Portas desligadas por padrão | O plano da versão 1.0 previa as portas ligadas, com `--sem-portas` para desligar. A análise jurídica de 26/09/2026 recomendou pedir o escopo antes da sondagem e deixar aparelhos pessoais de fora por padrão. A fatia seguiu a recomendação: a caixa começa desmarcada, a primeira varredura com portas em cada rede pede confirmação e a linha de comando liga com `--portas` | Fechado em 26/09/2026: o Manfred confirmou as portas desligadas ao abrir, com confirmação por rede |
+| Portas numa rede de verdade | A etapa foi testada com sonda simulada e no modo de demonstração. Não rodou numa rede de verdade no teste do agente | O Manfred varrer a rede da MT com as portas ligadas e conferir, por exemplo, a 80 ou 443 no roteador, a 9100 numa impressora e a 445 num Windows com compartilhamento |
+| Confirmação vale para a sessão | A pergunta aparece uma vez por sub-rede enquanto o programa está aberto. Fechar e abrir de novo pergunta outra vez | Guardar a confirmação, se o Manfred achar a pergunta repetitiva no uso |
+
## Página de privacidade
| Item | Motivo | O que fecha |
diff --git a/public/index.html b/public/index.html
index a4dd126..905581a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -48,7 +48,7 @@
O que ele faz
Todos os equipamentos da sub-rede
Por ping e por ARP. Quem bloqueia ping também aparece.
Fabricante e nome
Pelo MAC, na tabela do IEEE, e por DNS, NetBIOS e mDNS.
De onde vem a rede
DHCP, gateway, DNS, Wi-Fi e, com um clique, o IP público.
-
Detalhe de cada equipamento
Nomes, TTL e atalhos para ping, navegador e área de trabalho remota.
+
Detalhe de cada equipamento
Nomes, TTL, portas abertas e atalhos para ping, navegador e área de trabalho remota.
Ferramentas de rede
Ping, tracert, DNS, ARP, conexões e rotas, com a saída em português.
Manutenção
Limpar o cache DNS, renovar o IP, limpar o ARP e resetar o Winsock.
Relatório em HTML
Arquivo único para guardar com a documentação do cliente.
@@ -61,7 +61,7 @@
Como usar
Baixe o mapnet.exe e copie para qualquer pasta, ou para um pendrive.
Dê um duplo clique nele e escolha a interface de rede. A primeira da lista costuma ser a certa.
-
Clique em Iniciar varredura. No fim, o relatório fica em Documentos\MapNet - MT.
+
Clique em Iniciar varredura. No fim, clique em Salvar relatório e escolha onde guardar.
Na primeira vez, o Windows pode exibir "O Windows protegeu o seu computador". O motivo é a falta de assinatura digital, que é um certificado pago. Clique em Mais informações e depois em Executar assim mesmo.
Pela linha de comando:
@@ -74,7 +74,7 @@
Como usar
Uso autorizado
O MapNet serve para inventariar equipamentos em redes nas quais você tem autorização para fazer a verificação.
-
O que ele envia: ping, pedidos ARP e consultas de nome (DNS, NetBIOS e mDNS) aos endereços da sub-rede escolhida, as mesmas mensagens que os computadores de uma rede trocam entre si.
+
O que ele envia: ping, pedidos ARP e consultas de nome (DNS, NetBIOS e mDNS) aos endereços da sub-rede escolhida, as mesmas mensagens que os computadores de uma rede trocam entre si. Se você ligar a verificação de portas, ele também abre e fecha uma conexão TCP em cada porta da lista, só nos equipamentos encontrados e sem enviar dados.
O que ele não faz: não testa senhas, não contorna autenticação, não explora vulnerabilidades e não muda configuração de nada na rede. A manutenção mexe só no computador de quem usa, e só com o clique.
Sua parte: confira o intervalo de endereços antes de iniciar e respeite as regras da rede e dos dispositivos examinados.
O MapNet roda no seu computador e não manda nenhum dado para a MT. Não tem telemetria, não pede cadastro e não precisa de conta.
-
O relatório fica só no seu computador, em Documentos\MapNet - MT. Ele pode trazer dados pessoais, como o nome de um celular. Guarde com cuidado e apague quando não precisar mais.
+
O relatório fica só no seu computador, na pasta que você escolher ao salvar. Ele pode trazer dados pessoais, como o nome de um celular. Guarde com cuidado e apague quando não precisar mais.
O programa só sai da rede local quando você pede. Ao consultar o IP público, ele pergunta ao serviço 1.1.1.1, da Cloudflare. Ao usar o ping, o tracert ou o DNS com um endereço de fora, ele fala só com esse endereço.
diff --git a/src/mapnet.nucleo/linha-de-comando/argumentos.cs b/src/mapnet.nucleo/linha-de-comando/argumentos.cs
index 2cbeada..919d405 100644
--- a/src/mapnet.nucleo/linha-de-comando/argumentos.cs
+++ b/src/mapnet.nucleo/linha-de-comando/argumentos.cs
@@ -33,9 +33,17 @@ mapnet abre a janela
--tempo-ping espera de cada ping, de 100 a 10000 (padrão: 1000)
--paralelo endereços sondados ao mesmo tempo, de 1 a 256 (padrão: 64)
--sem-arp usa só o ping para descobrir hosts
+ --portas [lista] verifica as portas TCP dos hosts encontrados, só abrindo
+ e fechando a conexão. Sem lista, usa as 24 portas comuns;
+ com lista, por exemplo 22,80,443 (até 100 portas)
+ --incluir-mac-aleatorio inclui nas portas os aparelhos com MAC aleatório, quase
+ sempre pessoais, que ficam de fora por padrão
- Exemplo:
+ Exemplos:
mapnet --varrer --interface Wi-Fi --saida C:\Relatorios --abrir
+ mapnet --varrer --portas 80,443,9100
+
+ Use a verificação de portas só em rede que você tem autorização para verificar.
""";
public ComandoCli Comando { get; private set; } = ComandoCli.Janela;
@@ -91,6 +99,22 @@ public static ArgumentosCli Interpretar(IReadOnlyList args)
case "--sem-arp":
a.Opcoes.UsarArp = false;
break;
+ case "--portas":
+ a.Opcoes.OlharPortas = true;
+ var lista = i + 1 < args.Count && !args[i + 1].StartsWith("--", StringComparison.Ordinal) ? args[++i] : null;
+ if (ListaPortas.Interpretar(lista, out var erroPortas) is { } portas)
+ {
+ a.Opcoes.Portas = portas;
+ }
+ else
+ {
+ a.Erros.Add(erroPortas!);
+ }
+
+ break;
+ case "--incluir-mac-aleatorio":
+ a.Opcoes.PortasEmMacAleatorio = true;
+ break;
case "--tempo-ping":
if (Numero(Valor(args, ref i, arg, a.Erros), 100, 10000, arg, a.Erros) is int tempo)
{
@@ -116,10 +140,15 @@ public static ArgumentosCli Interpretar(IReadOnlyList args)
a.Erros.Add("Use só um comando por vez: --varrer, --interfaces, --ajuda ou --versao.");
}
- var opcoesDeVarredura = a.Interface != null || a.Saida != null || a.Abrir;
+ var opcoesDeVarredura = a.Interface != null || a.Saida != null || a.Abrir || a.Opcoes.OlharPortas;
if (opcoesDeVarredura && a.Comando == ComandoCli.Janela)
{
- a.Erros.Add("As opções --interface, --saida e --abrir pedem o comando --varrer.");
+ a.Erros.Add("As opções --interface, --saida, --abrir e --portas pedem o comando --varrer.");
+ }
+
+ if (a.Opcoes.PortasEmMacAleatorio && !a.Opcoes.OlharPortas)
+ {
+ a.Erros.Add("A opção --incluir-mac-aleatorio só vale junto com --portas.");
}
return a;
diff --git a/src/mapnet.nucleo/painel/demonstracao.cs b/src/mapnet.nucleo/painel/demonstracao.cs
index 1e8c583..02ab1f1 100644
--- a/src/mapnet.nucleo/painel/demonstracao.cs
+++ b/src/mapnet.nucleo/painel/demonstracao.cs
@@ -41,10 +41,12 @@ public static DependenciasPainel Dependencias()
VelocidadeBps = 1_000_000_000,
};
+ var opcoes = new OpcoesVarredura();
return new DependenciasPainel
{
+ Opcoes = opcoes,
ListarInterfaces = () => [wifi, cabo],
- Varrer = VarrerAsync,
+ Varrer = (i, p, c) => VarrerAsync(i, opcoes, p, c),
SalvarEm = (r, caminho) => RelatorioHtml.SalvarAsync(r, caminho),
// Documentos Públicos: o caminho não traz o nome do usuário, que apareceria nas imagens.
PastaRelatorios = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonDocuments), "MapNet - MT", "demonstracao"),
@@ -235,8 +237,25 @@ HostEncontrado Host(byte final, string mac, long? ping, Action?
];
}
- /// Varredura de mentira: anda pelos 254 endereços em uns 4 segundos e relata os hosts de exemplo.
- private static async Task VarrerAsync(InterfaceRede i, IProgress progresso, CancellationToken cancelamento)
+ /// Portas abertas de exemplo, pelo último número do IP dos .
+ private static readonly Dictionary _portasExemplo = new()
+ {
+ [1] = [53, 80, 443],
+ [5] = [80, 443, 515, 631, 9100],
+ [12] = [22, 80],
+ [31] = [8080],
+ [40] = [80, 443],
+ [57] = [135, 139, 445, 3389],
+ [101] = [5000],
+ [142] = [554, 8000],
+ };
+
+ ///
+ /// Varredura de mentira: anda pelos 254 endereços em uns 4 segundos e relata os hosts de
+ /// exemplo. Com as portas ligadas, preenche as portas de exemplo, com a mesma regra de quem
+ /// fica de fora que a varredura de verdade usa.
+ ///
+ private static async Task VarrerAsync(InterfaceRede i, OpcoesVarredura opcoes, IProgress progresso, CancellationToken cancelamento)
{
var inicio = DateTimeOffset.Now;
var subRede = Varredor.SubRedeAVarrer(i, new OpcoesVarredura().PrefixoMinimo, out _);
@@ -261,6 +280,21 @@ private static async Task VarrerAsync(InterfaceRede i, IProg
await Task.Delay(15, CancellationToken.None).ConfigureAwait(true);
}
+ if (!resultado.Cancelada && opcoes.OlharPortas)
+ {
+ resultado.PortasVerificadas = opcoes.Portas;
+ var alvos = EtapaPortas.Escolher(resultado.Hosts, opcoes.PortasEmMacAleatorio);
+ for (var n = 0; n < alvos.Count; n++)
+ {
+ var h = alvos[n];
+ h.PortasAbertas = _portasExemplo.TryGetValue(h.Ip.GetAddressBytes()[3], out var p) ? p.Where(opcoes.Portas.Contains).ToList() : [];
+ h.PortasTestadas = opcoes.Portas;
+ h.PortasVerificadas = true;
+ progresso.Report(new ProgressoVarredura(EtapaPortas.Nome, n + 1, alvos.Count, h));
+ await Task.Delay(120, CancellationToken.None).ConfigureAwait(true);
+ }
+ }
+
resultado.Fim = DateTimeOffset.Now;
return resultado;
}
diff --git a/src/mapnet.nucleo/painel/detalhe-host.cs b/src/mapnet.nucleo/painel/detalhe-host.cs
index 9dd1e6d..d6acd7c 100644
--- a/src/mapnet.nucleo/painel/detalhe-host.cs
+++ b/src/mapnet.nucleo/painel/detalhe-host.cs
@@ -20,7 +20,7 @@ public enum AcaoHost
public static class DetalheHost
{
/// Arquivo (ou endereço) e argumentos para abrir no Windows. Só IPv4.
- public static (string Arquivo, string? Argumentos) Destino(AcaoHost acao, IPAddress ip)
+ public static (string Arquivo, string? Argumentos) Destino(AcaoHost acao, IPAddress ip, int? porta = null)
{
if (ip.AddressFamily != AddressFamily.InterNetwork)
{
@@ -28,16 +28,63 @@ public static (string Arquivo, string? Argumentos) Destino(AcaoHost acao, IPAddr
}
var texto = ip.ToString();
+ var sufixo = porta is int p ? ":" + p.ToString(System.Globalization.CultureInfo.InvariantCulture) : string.Empty;
return acao switch
{
- AcaoHost.AbrirHttp => ($"http://{texto}/", null),
- AcaoHost.AbrirHttps => ($"https://{texto}/", null),
+ AcaoHost.AbrirHttp => ($"http://{texto}{sufixo}/", null),
+ AcaoHost.AbrirHttps => ($"https://{texto}{sufixo}/", null),
AcaoHost.AreaDeTrabalhoRemota => ("mstsc.exe", $"/v:{texto}"),
AcaoHost.PastaCompartilhada => ($@"\\{texto}", null),
_ => throw new ArgumentOutOfRangeException(nameof(acao)),
};
}
+ ///
+ /// Porta que o atalho http ou https deve usar. Quando a porta comum (80 ou 443) está fechada
+ /// e uma alternativa está aberta, como a 8080 de muito painel de câmera, vai a alternativa.
+ /// Null é a porta comum.
+ ///
+ public static int? PortaWeb(HostEncontrado h, bool https)
+ {
+ if (!h.PortasVerificadas)
+ {
+ return null;
+ }
+
+ var (comum, alternativas) = https ? (443, new[] { 8443 }) : (80, new[] { 8080, 8000 });
+ if (h.PortasAbertas.Contains(comum))
+ {
+ return null;
+ }
+
+ return alternativas.FirstOrDefault(h.PortasAbertas.Contains) is var a and > 0 ? a : null;
+ }
+
+ ///
+ /// Diz se vale oferecer a ação. Sem verificação de portas, ou quando nenhuma porta do serviço
+ /// entrou na lista, não há como saber, e a ação fica ligada. Quando as portas do serviço foram
+ /// verificadas e todas estão fechadas, a ação desliga: abrir daria só erro.
+ ///
+ public static bool Disponivel(HostEncontrado h, AcaoHost acao)
+ {
+ int[] portas = acao switch
+ {
+ AcaoHost.AbrirHttp => [80, 8080, 8000],
+ AcaoHost.AbrirHttps => [443, 8443],
+ AcaoHost.AreaDeTrabalhoRemota => [3389],
+ AcaoHost.PastaCompartilhada => [445, 139],
+ _ => [],
+ };
+ var testadas = portas.Where(h.PortasTestadas.Contains).ToList();
+ return testadas.Count == 0 || testadas.Any(h.PortasAbertas.Contains);
+ }
+
+ /// Texto das portas para o detalhe e o relatório.
+ public static string TextoPortas(HostEncontrado h) =>
+ h.PortasVerificadas
+ ? (h.PortasAbertas.Count > 0 ? ListaPortas.Texto(h.PortasAbertas) : "nenhuma das portas verificadas está aberta")
+ : h.MotivoSemPortas ?? "não verificadas nesta varredura";
+
/// Pista do sistema pelo TTL da resposta ao ping.
public static string? PistaDoTtl(int? ttl) => ttl switch
{
@@ -68,6 +115,7 @@ public static IReadOnlyList Itens(HostEncontrado h)
}
itens.Add(new("ARP", h.RespondeuArp ? "respondeu" : h.EhEsteComputador ? "não se aplica (este computador)" : "não respondeu"));
+ itens.Add(new("Portas abertas", TextoPortas(h)));
if (h.Marcas.Count > 0)
{
itens.Add(new("Observação", string.Join(", ", h.Marcas)));
diff --git a/src/mapnet.nucleo/painel/linha-host.cs b/src/mapnet.nucleo/painel/linha-host.cs
index dd3ae38..63e7189 100644
--- a/src/mapnet.nucleo/painel/linha-host.cs
+++ b/src/mapnet.nucleo/painel/linha-host.cs
@@ -39,6 +39,15 @@ public LinhaHost(HostEncontrado host)
public string Observacao => string.Join(", ", _host.Marcas);
+ /// Portas abertas, só os números: "22, 80, 443".
+ public string Portas => _host.PortasTexto;
+
+ /// Dica da coluna: cada porta com o serviço, ou o motivo de não ter sido verificada.
+ public string? PortasDica =>
+ _host.PortasAbertas.Count > 0 ? ListaPortas.Texto(_host.PortasAbertas)
+ : _host.MotivoSemPortas is { } motivo ? char.ToUpperInvariant(motivo[0]) + motivo[1..]
+ : null;
+
public bool EhGateway => _host.EhGateway;
public bool EhEsteComputador => _host.EhEsteComputador;
@@ -50,7 +59,10 @@ public void Atualizar(HostEncontrado host)
Avisar(string.Empty);
}
- /// O filtro procura o texto no IP, no nome, no MAC, no fabricante e na observação.
+ ///
+ /// O filtro procura o texto no IP, no nome, no MAC, no fabricante e na observação. Um número
+ /// que é porta aberta também acha o host: "9100" lista as impressoras.
+ ///
public bool Contem(string texto)
{
if (string.IsNullOrWhiteSpace(texto))
@@ -67,7 +79,8 @@ public bool Contem(string texto)
|| Mac.Contains(t, StringComparison.OrdinalIgnoreCase)
|| (macBusca.Length > 0 && Mac.Replace(":", string.Empty).Contains(macBusca, StringComparison.OrdinalIgnoreCase))
|| Fabricante.Contains(t, StringComparison.OrdinalIgnoreCase)
- || Observacao.Contains(t, StringComparison.OrdinalIgnoreCase);
+ || Observacao.Contains(t, StringComparison.OrdinalIgnoreCase)
+ || _host.PortasAbertas.Any(p => p.ToString(System.Globalization.CultureInfo.InvariantCulture) == t);
}
private void Avisar([CallerMemberName] string? propriedade = null) =>
diff --git a/src/mapnet.nucleo/painel/painel-varredura.cs b/src/mapnet.nucleo/painel/painel-varredura.cs
index 4dcc8e6..0fed974 100644
--- a/src/mapnet.nucleo/painel/painel-varredura.cs
+++ b/src/mapnet.nucleo/painel/painel-varredura.cs
@@ -30,6 +30,12 @@ public sealed class DependenciasPainel
public int PrefixoMinimo { get; init; } = new OpcoesVarredura().PrefixoMinimo;
+ ///
+ /// Opções que a varredura usa. O painel liga e desliga as portas aqui antes de chamar
+ /// , que precisa ler este mesmo objeto.
+ ///
+ public OpcoesVarredura Opcoes { get; init; } = new();
+
public string PastaRelatorios { get; init; } =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "MapNet - MT");
@@ -78,11 +84,13 @@ public static IReadOnlyList FerramentasPadrao()
/// As partes de verdade: interfaces do Windows, varredor e relatório HTML.
public static DependenciasPainel Padrao()
{
- var prefixo = new OpcoesVarredura().PrefixoMinimo;
+ var opcoes = new OpcoesVarredura();
+ var prefixo = opcoes.PrefixoMinimo;
return new()
{
+ Opcoes = opcoes,
ListarInterfaces = LeitorInterfaces.Listar,
- Varrer = (i, p, c) => new Varredor().VarrerAsync(i, p, c),
+ Varrer = (i, p, c) => new Varredor(opcoes).VarrerAsync(i, p, c),
SalvarEm = (r, caminho) => RelatorioHtml.SalvarAsync(r, caminho),
LerMaquina = i => Task.Run(() => LeitorMaquina.Ler(i, FontesMaquina.Padrao(), prefixo)),
IpPublico = new IpPublicoCloudflare(),
@@ -119,6 +127,8 @@ public sealed class PainelVarredura : INotifyPropertyChanged
private AbaConsole? _abaManutencao;
private bool _manutencaoRodando;
private LinhaHost? _hostSelecionado;
+ private string _textoPortas = "padrão";
+ private readonly HashSet _redesConfirmadas = [];
public PainelVarredura(DependenciasPainel dependencias)
{
@@ -259,28 +269,54 @@ public async Task FerramentaNoHostAsync(string titulo, bool continuo)
await aba.ExecutarAsync();
}
- /// Abre o host no navegador, na área de trabalho remota ou no Explorer.
- public void AcaoNoHost(AcaoHost acao)
+ ///
+ /// Abre o host no navegador, na área de trabalho remota ou no Explorer. O Windows pode levar
+ /// segundos para desistir de um endereço que não responde, então a abertura roda fora da
+ /// linha da tela, e os botões ficam desligados até ela terminar: clique repetido não vira
+ /// fila de tentativas.
+ ///
+ public async Task AcaoNoHostAsync(AcaoHost acao)
{
- if (_hostSelecionado is not { } h || _dep.Abrir is not { } abrir)
+ if (_abrindo || _hostSelecionado is not { } h || _dep.Abrir is not { } abrir)
{
return;
}
- var (arquivo, argumentos) = DetalheHost.Destino(acao, h.Host.Ip);
+ var porta = acao switch
+ {
+ AcaoHost.AbrirHttp => DetalheHost.PortaWeb(h.Host, https: false),
+ AcaoHost.AbrirHttps => DetalheHost.PortaWeb(h.Host, https: true),
+ _ => null,
+ };
+ var (arquivo, argumentos) = DetalheHost.Destino(acao, h.Host.Ip, porta);
+ _abrindo = true;
+ AvisarDetalhe();
+ Console.Escrever($"Abrindo {arquivo}{(argumentos is null ? "" : " " + argumentos)}...");
try
{
- abrir(arquivo, argumentos);
- Console.Escrever($"Abrindo {arquivo}{(argumentos is null ? "" : " " + argumentos)}.");
+ await Task.Run(() => abrir(arquivo, argumentos));
+ }
+ catch (Win32Exception e)
+ {
+ // A mensagem do .NET vem em inglês e com a pasta do programa. A do Windows, pelo
+ // código do erro, vem no idioma do sistema e diz só o motivo.
+ Console.Escrever($"Não foi possível abrir {arquivo}: {new Win32Exception(e.NativeErrorCode).Message}");
}
catch (Exception e)
{
Console.Escrever($"Não foi possível abrir {arquivo}: {e.Message}");
}
+ finally
+ {
+ _abrindo = false;
+ AvisarDetalhe();
+ }
}
+ private bool _abrindo;
+
private Comando ComandoDe(AcaoHost acao) =>
- new(() => AcaoNoHost(acao), () => _hostSelecionado != null && _dep.Abrir != null);
+ new(() => _ = AcaoNoHostAsync(acao), () => !_abrindo && _hostSelecionado is { } h && _dep.Abrir != null && DetalheHost.Disponivel(h.Host, acao));
private void AoMudarHost(object? sender, System.ComponentModel.PropertyChangedEventArgs e) => AvisarDetalhe();
@@ -552,6 +588,70 @@ public string Filtro
public bool PodeTrocarInterface => _estado == EstadoPainel.Parado;
+ /// Verifica as portas TCP dos hosts encontrados. Desligado ao abrir: quem usa liga.
+ public bool OlharPortas
+ {
+ get => _dep.Opcoes.OlharPortas;
+ set
+ {
+ if (_dep.Opcoes.OlharPortas == value || !PodeTrocarInterface)
+ {
+ return;
+ }
+
+ _dep.Opcoes.OlharPortas = value;
+ Avisar();
+ }
+ }
+
+ /// "padrão" ou a lista digitada, como "22,80,443". Só é conferida ao iniciar.
+ public string TextoPortas
+ {
+ get => _textoPortas;
+ set
+ {
+ var novo = value ?? string.Empty;
+ if (_textoPortas == novo || !PodeTrocarInterface)
+ {
+ return;
+ }
+
+ _textoPortas = novo;
+ Avisar();
+ }
+ }
+
+ /// Inclui na verificação de portas os aparelhos com MAC aleatório, quase sempre pessoais.
+ public bool PortasEmMacAleatorio
+ {
+ get => _dep.Opcoes.PortasEmMacAleatorio;
+ set
+ {
+ if (_dep.Opcoes.PortasEmMacAleatorio == value || !PodeTrocarInterface)
+ {
+ return;
+ }
+
+ _dep.Opcoes.PortasEmMacAleatorio = value;
+ Avisar();
+ }
+ }
+
+ public string DicaPortasPadrao => "Portas da lista padrão: " + ListaPortas.Texto(ListaPortas.Padrao);
+
+ ///
+ /// Pergunta feita antes da primeira verificação de portas em cada sub-rede. Diz o que vai ser
+ /// enviado e lembra da autorização, como recomenda a análise jurídica de 26/09/2026.
+ ///
+ public static string PerguntaPortas(SubRede subRede, int quantidade, bool incluiMacAleatorio) =>
+ $"Verificar portas em {subRede}?\n\n"
+ + $"Para cada equipamento encontrado, o MapNet abre e fecha uma conexão TCP em {quantidade} porta(s), sem enviar dados. "
+ + "Não testa senha nem explora falha.\n\n"
+ + (incluiMacAleatorio
+ ? "Os aparelhos com MAC aleatório, quase sempre celulares e notebooks pessoais, também entram.\n\n"
+ : "Os aparelhos com MAC aleatório, quase sempre pessoais, ficam de fora.\n\n")
+ + "Faça isso só em rede que você tem autorização para verificar.";
+
public bool PodeAcionarPrincipal =>
(_estado == EstadoPainel.Parado && _interfaceSelecionada != null) || _estado == EstadoPainel.Varrendo;
@@ -669,6 +769,11 @@ public async Task VarrerAsync()
return;
}
+ if (_dep.Opcoes.OlharPortas && !PrepararPortas(interfaceRede))
+ {
+ return;
+ }
+
_cancelamento = new CancellationTokenSource();
HostSelecionado = null;
Hosts.Clear();
@@ -684,6 +789,11 @@ public async Task VarrerAsync()
var subRede = Varredor.SubRedeAVarrer(interfaceRede, _dep.PrefixoMinimo, out var aviso);
Console.Escrever($"Varredura de {subRede} ({subRede.QuantidadeHosts} endereços) pela interface {interfaceRede.Nome}.");
+ if (_dep.Opcoes.OlharPortas)
+ {
+ Console.Escrever($"Portas: {_dep.Opcoes.Portas.Count} porta(s) TCP em cada host encontrado, só abrindo e fechando a conexão.");
+ }
+
if (aviso != null)
{
EscreverAviso(aviso);
@@ -727,11 +837,56 @@ public async Task VarrerAsync()
EscreverAviso(a);
}
+ // O relatório não é gravado sozinho: quem usa escolhe onde, no botão Salvar relatório.
+ TextoRelatorio = "Relatório ainda não salvo.";
+ Console.Escrever("Para guardar o relatório, clique em Salvar relatório e escolha onde.");
AvisarBotoes();
- await SalvarAsync(resultado, Path.Combine(PastaRelatorios, RelatorioHtml.NomeArquivo(resultado)));
}
- /// Grava o último resultado em outro lugar, escolhido pelo técnico.
+ /// A última varredura ainda não foi salva em nenhum lugar.
+ public bool RelatorioNaoSalvo => UltimoResultado != null && UltimoRelatorio == null;
+
+ /// Nome sugerido na janela de salvar, como mapnet-20260926-1430-192-0-2-0-24.html.
+ public string? NomeSugerido => UltimoResultado is { } r ? RelatorioHtml.NomeArquivo(r) : null;
+
+ /// Pasta em que a janela de salvar abre: a do último relatório salvo ou Documentos\MapNet - MT.
+ public string PastaInicial => _ultimaPasta is { } ultimo ? Path.GetDirectoryName(ultimo) ?? PastaRelatorios : PastaRelatorios;
+
+ private string? _ultimaPasta;
+
+ ///
+ /// Confere a lista de portas e, na primeira vez em cada sub-rede, pede a confirmação. Sem
+ /// lista válida ou sem o sim do técnico, a varredura não começa.
+ ///
+ private bool PrepararPortas(InterfaceRede interfaceRede)
+ {
+ var portas = ListaPortas.Interpretar(_textoPortas, out var erro);
+ if (portas is null)
+ {
+ Console.Escrever($"Lista de portas: {erro}");
+ Falhou?.Invoke(erro!);
+ return false;
+ }
+
+ _dep.Opcoes.Portas = portas;
+ var subRede = Varredor.SubRedeAVarrer(interfaceRede, _dep.PrefixoMinimo, out _);
+ var chave = $"{subRede}|{_dep.Opcoes.PortasEmMacAleatorio}";
+ if (_redesConfirmadas.Contains(chave))
+ {
+ return true;
+ }
+
+ if (_dep.Confirmar?.Invoke(PerguntaPortas(subRede, portas.Count, _dep.Opcoes.PortasEmMacAleatorio)) != true)
+ {
+ Console.Escrever("Varredura não iniciada: a verificação de portas não foi confirmada.");
+ return false;
+ }
+
+ _redesConfirmadas.Add(chave);
+ return true;
+ }
+
+ /// Grava o último resultado no lugar escolhido pelo técnico.
public Task SalvarComoAsync(string caminho) =>
UltimoResultado is { } r && _estado == EstadoPainel.Parado ? SalvarAsync(r, caminho) : Task.CompletedTask;
@@ -753,6 +908,7 @@ private async Task SalvarAsync(ResultadoVarredura resultado, string caminho)
resultado.IpPublico = _ipPublico?.ToString();
UltimoRelatorio = await _dep.SalvarEm(resultado, caminho);
+ _ultimaPasta = UltimoRelatorio;
TextoRelatorio = $"Relatório em {UltimoRelatorio}";
Console.Escrever($"Relatório gravado em {UltimoRelatorio}");
}
@@ -846,10 +1002,12 @@ private void AvisarBotoes()
{
Avisar(nameof(PodeTrocarInterface));
Avisar(nameof(PodeAcionarPrincipal));
+ Avisar(nameof(OlharPortas));
Avisar(nameof(TextoBotaoPrincipal));
Avisar(nameof(Varrendo));
Avisar(nameof(PodeAbrirRelatorio));
Avisar(nameof(PodeSalvarComo));
+ Avisar(nameof(RelatorioNaoSalvo));
ComandoPrincipal.Reavaliar();
ComandoAtualizar.Reavaliar();
}
diff --git a/src/mapnet.nucleo/portas/portas.cs b/src/mapnet.nucleo/portas/portas.cs
new file mode 100644
index 0000000..da5da0a
--- /dev/null
+++ b/src/mapnet.nucleo/portas/portas.cs
@@ -0,0 +1,204 @@
+using System.Collections.Concurrent;
+using System.Globalization;
+using System.Net;
+using System.Net.Sockets;
+
+namespace MapNet.Nucleo;
+
+/// Portas TCP comuns, com o serviço de cada uma, e a leitura da lista digitada.
+public static class ListaPortas
+{
+ public const int MaximoDePortas = 100;
+
+ private static readonly SortedDictionary _servicos = new()
+ {
+ [21] = "FTP",
+ [22] = "SSH",
+ [23] = "Telnet",
+ [25] = "SMTP",
+ [53] = "DNS",
+ [80] = "HTTP",
+ [135] = "RPC do Windows",
+ [139] = "NetBIOS",
+ [443] = "HTTPS",
+ [445] = "Compartilhamento do Windows",
+ [515] = "Impressão LPD",
+ [554] = "Câmera RTSP",
+ [631] = "Impressão IPP",
+ [1433] = "SQL Server",
+ [3306] = "MySQL",
+ [3389] = "Área de trabalho remota",
+ [5000] = "NAS e UPnP",
+ [5900] = "VNC",
+ [8000] = "HTTP alternativo",
+ [8080] = "HTTP alternativo",
+ [8291] = "MikroTik Winbox",
+ [8443] = "HTTPS alternativo",
+ [9100] = "Impressão direta",
+ [37777] = "NVR Dahua",
+ };
+
+ public static IReadOnlyList Padrao { get; } = _servicos.Keys.ToList();
+
+ public static string Servico(int porta) => _servicos.TryGetValue(porta, out var s) ? s : string.Empty;
+
+ /// "80 (HTTP), 443 (HTTPS), 8123".
+ public static string Texto(IEnumerable portas) =>
+ string.Join(", ", portas.Select(p => Servico(p) is { Length: > 0 } s ? $"{p} ({s})" : p.ToString(CultureInfo.InvariantCulture)));
+
+ ///
+ /// Lê "padrao" ou uma lista como "22,80,443". Porta fora de 1 a 65535, texto que não é
+ /// número e lista com mais de portas são recusados.
+ ///
+ public static IReadOnlyList? Interpretar(string? texto, out string? erro)
+ {
+ erro = null;
+ var t = texto?.Trim() ?? string.Empty;
+ if (t.Length == 0 || t.Equals("padrao", StringComparison.OrdinalIgnoreCase) || t.Equals("padrão", StringComparison.OrdinalIgnoreCase))
+ {
+ return Padrao;
+ }
+
+ var portas = new SortedSet();
+ foreach (var parte in t.Split([',', ';', ' '], StringSplitOptions.RemoveEmptyEntries))
+ {
+ if (!int.TryParse(parte, NumberStyles.None, CultureInfo.InvariantCulture, out var p) || p < 1 || p > 65535)
+ {
+ erro = $"Porta inválida: {parte}. Use números de 1 a 65535, separados por vírgula.";
+ return null;
+ }
+
+ portas.Add(p);
+ }
+
+ if (portas.Count > MaximoDePortas)
+ {
+ erro = $"São {portas.Count} portas. O limite é {MaximoDePortas}.";
+ return null;
+ }
+
+ return portas.ToList();
+ }
+}
+
+/// Diz se a porta aceita conexão. Os testes trocam por uma sonda simulada.
+public interface ISondaPorta
+{
+ Task AbertaAsync(IPAddress ip, int porta, int tempoMs, CancellationToken cancelamento);
+}
+
+///
+/// Abre a conexão TCP e fecha em seguida, sem enviar nenhum dado. Porta que recusa ou não
+/// responde no tempo limite conta como fechada. Não pede administrador.
+///
+public sealed class SondaPortaTcp : ISondaPorta
+{
+ public async Task AbertaAsync(IPAddress ip, int porta, int tempoMs, CancellationToken cancelamento)
+ {
+ using var limite = CancellationTokenSource.CreateLinkedTokenSource(cancelamento);
+ limite.CancelAfter(tempoMs);
+ using var socket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
+ try
+ {
+ await socket.ConnectAsync(new IPEndPoint(ip, porta), limite.Token).ConfigureAwait(false);
+ return true;
+ }
+ catch (OperationCanceledException) when (!cancelamento.IsCancellationRequested)
+ {
+ return false;
+ }
+ catch (SocketException)
+ {
+ return false;
+ }
+ }
+}
+
+///
+/// Etapa de portas da varredura. Só verifica os hosts que a descoberta achou, nunca sai
+/// deles. Por padrão, deixa de fora este computador e os aparelhos com MAC aleatório, que são
+/// quase sempre pessoais: o MAC aleatório serve de sinal, não de prova.
+///
+public static class EtapaPortas
+{
+ public const string Nome = "Verificação de portas";
+
+ public const string MotivoMacAleatorio = "fora da verificação por padrão: MAC aleatório, aparelho provavelmente pessoal";
+
+ public const string MotivoEsteComputador = "não se aplica (este computador)";
+
+ /// Hosts que entram na verificação. Os outros recebem o motivo.
+ public static IReadOnlyList Escolher(IReadOnlyList hosts, bool incluirMacAleatorio)
+ {
+ var escolhidos = new List();
+ foreach (var h in hosts)
+ {
+ if (h.EhEsteComputador)
+ {
+ h.MotivoSemPortas = MotivoEsteComputador;
+ }
+ else if (h.MacAleatorio && !incluirMacAleatorio)
+ {
+ h.MotivoSemPortas = MotivoMacAleatorio;
+ }
+ else
+ {
+ escolhidos.Add(h);
+ }
+ }
+
+ return escolhidos;
+ }
+
+ public static async Task VerificarAsync(
+ IReadOnlyList hosts,
+ OpcoesVarredura opcoes,
+ ISondaPorta sonda,
+ IProgress? progresso,
+ CancellationToken cancelamento)
+ {
+ var alvos = Escolher(hosts, opcoes.PortasEmMacAleatorio);
+ var total = alvos.Count;
+ var concluidos = 0;
+ using var global = new SemaphoreSlim(Math.Max(1, opcoes.ConexoesPortas));
+ progresso?.Report(new ProgressoVarredura(Nome, 0, total));
+
+ var tarefas = alvos.Select(async host =>
+ {
+ using var porHost = new SemaphoreSlim(Math.Max(1, opcoes.ConexoesPorHost));
+ var abertas = new ConcurrentBag();
+ var portas = opcoes.Portas.Select(async porta =>
+ {
+ await porHost.WaitAsync(cancelamento).ConfigureAwait(false);
+ try
+ {
+ await global.WaitAsync(cancelamento).ConfigureAwait(false);
+ try
+ {
+ if (await sonda.AbertaAsync(host.Ip, porta, opcoes.TempoPortaMs, cancelamento).ConfigureAwait(false))
+ {
+ abertas.Add(porta);
+ }
+ }
+ finally
+ {
+ global.Release();
+ }
+ }
+ finally
+ {
+ porHost.Release();
+ }
+ });
+ await Task.WhenAll(portas).ConfigureAwait(false);
+
+ host.PortasAbertas = abertas.Order().ToList();
+ host.PortasTestadas = opcoes.Portas;
+ host.PortasVerificadas = true;
+ var feitos = Interlocked.Increment(ref concluidos);
+ progresso?.Report(new ProgressoVarredura(Nome, feitos, total, host));
+ });
+
+ await Task.WhenAll(tarefas).ConfigureAwait(false);
+ }
+}
diff --git a/src/mapnet.nucleo/relatorios/relatorio-html.cs b/src/mapnet.nucleo/relatorios/relatorio-html.cs
index 0d54975..78f3f55 100644
--- a/src/mapnet.nucleo/relatorios/relatorio-html.cs
+++ b/src/mapnet.nucleo/relatorios/relatorio-html.cs
@@ -99,6 +99,7 @@ public static string Gerar(ResultadoVarredura r)
{{C(r.PortasVerificadas is { } portas ? $"{portas.Count} porta(s) TCP, só abrindo e fechando a conexão: {ListaPortas.Texto(portas)}" : "não verificadas nesta varredura")}}