#
============================================================================
OK-COMPUTER · Escaner de auditoria de seguridad de red (v2)
============================================================================
Auditoria DEFENSIVA y de DESCUBRIMIENTO a fondo. Inventaria la red, audita
en profundidad la postura de seguridad del equipo y correlaciona los
hallazgos con vulnerabilidades conocidas de Windows. NO explota, NO prueba
contrasenas, NO ataca: descubre, documenta y recomienda.
USO LEGITIMO UNICAMENTE: ejecutalo SOLO en redes donde el cliente te ha
autorizado por escrito. El script registra esa autorizacion en el informe.
Ejecutar (MEJOR como Administrador para la auditoria completa):
powershell -ExecutionPolicy Bypass -File .\scanner-red.ps1
Parametros:
-Target "192.168.1.0/24" Subred a escanear (por defecto autodetecta).
-Empresa "Nombre Cliente" Nombre del cliente para el informe.
-Quick Escaneo de puertos reducido (mas rapido).
-SkipDiscovery Solo auditar este equipo, sin barrer la red.
-Yes Salta la confirmacion (solo con autorizacion).
============================================================================
#>
[CmdletBinding()]
param(
[string]$Target,
[string]$Empresa = "",
[switch]$Quick,
[switch]$SkipDiscovery,
[switch]$Yes
)
$ErrorActionPreference = "SilentlyContinue"
$ProgressPreference = "SilentlyContinue"
Add-Type -AssemblyName System.Web | Out-Null
$script:Findings = New-Object System.Collections.ArrayList
$script:Hosts = New-Object System.Collections.ArrayList
$script:Attacker = New-Object System.Collections.ArrayList # "que podria hacer un atacante"
# --------------------------------------------------------------------------
# Salida y registro de hallazgos
# --------------------------------------------------------------------------
function Write-Title($t) { Write-Host "`n=== $t ===" -ForegroundColor Cyan }
function Add-Finding {
param(
[ValidateSet("Critico","Riesgo","Info","OK")] $Sev,
$Area, $Titulo, $Detalle = "", $Solucion = "", $Impacto = "", $Ref = ""
)
[void]$script:Findings.Add([pscustomobject]@{
Sev=$Sev; Area=$Area; Titulo=$Titulo; Detalle=$Detalle; Solucion=$Solucion; Impacto=$Impacto; Ref=$Ref
})
if ($Impacto) { [void]$script:Attacker.Add($Impacto) }
switch ($Sev) {
"Critico" { Write-Host " [!!!] $Area - $Titulo" -ForegroundColor Red }
"Riesgo" { Write-Host " [!] $Area - $Titulo" -ForegroundColor Yellow }
"OK" { Write-Host " [OK] $Area - $Titulo" -ForegroundColor Green }
default { Write-Host " [i] $Area - $Titulo" -ForegroundColor Gray }
}
if ($Impacto -and ($Sev -eq "Critico" -or $Sev -eq "Riesgo")) {
$rf = if ($Ref) { " [$Ref]" } else { "" }
Write-Host " Si no se arregla: $Impacto$rf" -ForegroundColor DarkGray
}
}
function Reg([string]$path, [string]$name) {
try { return (Get-ItemProperty -Path $path -Name $name -ErrorAction Stop).$name } catch { return $null }
}
# --------------------------------------------------------------------------
# Escaneo de puertos asincrono (rapido, no intrusivo: solo connect TCP)
# --------------------------------------------------------------------------
$PORTS = [ordered]@{
21="FTP"; 22="SSH"; 23="Telnet"; 25="SMTP"; 53="DNS"; 80="HTTP"; 110="POP3"
135="RPC/DCOM"; 139="NetBIOS"; 143="IMAP"; 389="LDAP"; 443="HTTPS"; 445="SMB"
465="SMTPS"; 587="SMTP"; 636="LDAPS"; 993="IMAPS"; 1433="SQL Server"
1434="SQL Browser"; 2049="NFS"; 3306="MySQL"; 3389="RDP"; 5432="PostgreSQL"
5900="VNC"; 5985="WinRM"; 5986="WinRM-S"; 6379="Redis"; 8000="HTTP/Camara"
8080="HTTP-alt"; 8443="HTTPS-alt"; 9100="Impresora"; 9200="Elasticsearch"
27017="MongoDB"; 37777="DVR Dahua"; 34567="DVR XMeye"; 49152="UPnP"
554="RTSP"; 5000="UPnP/DSM"; 5001="DSM-S"; 5060="SIP/VoIP"; 8554="RTSP-alt"; 8899="DVR"; 9000="Camara/NVR"
}
$QUICK_PORTS = 21,22,23,80,135,139,443,445,1433,3306,3389,5432,5900,5985,554,8000,8080,37777
$DB_PORTS = @{ 1433="SQL Server"; 3306="MySQL"; 5432="PostgreSQL"; 6379="Redis"; 27017="MongoDB"; 9200="Elasticsearch"; 2049="NFS" }
function Scan-HostPorts {
param([string]$IpAddress, $Ports, [int]$TimeoutMs = 700)
$tasks = @()
foreach ($pt in $Ports) {
$c = New-Object System.Net.Sockets.TcpClient
try { $iar = $c.BeginConnect($IpAddress, [int]$pt, $null, $null) } catch { continue }
$tasks += [pscustomobject]@{ Port=[int]$pt; Client=$c; Iar=$iar }
}
Start-Sleep -Milliseconds $TimeoutMs
$open = @()
foreach ($t in $tasks) {
if ($t.Iar.IsCompleted -and $t.Client.Connected) { $open += $t.Port }
try { $t.Client.Close() } catch {}
}
return ($open | Sort-Object)
}
# Banner / cabecera HTTP rapida (solo lectura)
function Get-HttpInfo {
param([string]$IpAddress, [int]$Port, [bool]$Https = $false)
try {
$scheme = if ($Https) { "https" } else { "http" }
$r = Invoke-WebRequest -Uri "$scheme`://$IpAddress`:$Port/" -TimeoutSec 3 -UseBasicParsing -ErrorAction Stop
$srv = $r.Headers["Server"]
$title = ""
if ($r.Content -match "
(.*?)") { $title = $matches[1].Trim() }
return ("$srv $title").Trim()
} catch { return "" }
}
# OUI -> fabricante (camaras, DVR, alarmas e IoT comunes)
$OUI_VENDOR = @{
# Hikvision
"BCAD28"="Hikvision"; "4419B7"="Hikvision"; "C056E3"="Hikvision"; "ACCB51"="Hikvision"
"28571C"="Hikvision"; "44A642"="Hikvision"; "18800C"="Hikvision"; "F84DFC"="Hikvision"
"542B8D"="Hikvision"; "E0CA94"="Hikvision"; "C40938"="Hikvision"
# Dahua
"001C27"="Dahua"; "9CA3A9"="Dahua"; "E03E44"="Dahua"; "3C1A89"="Dahua"
"14A78B"="Dahua"; "38AF29"="Dahua"; "4C11BF"="Dahua"; "90020A"="Dahua"; "08ED02"="Dahua"
# Axis
"00408C"="Axis"; "ACCC8E"="Axis"; "B8A44F"="Axis"; "E82725"="Axis"
# Uniview
"48EA63"="Uniview"; "5C5AEC"="Uniview"
# Reolink
"EC71DB"="Reolink"
# Hanwha / Samsung Techwin
"0009DC"="Hanwha"; "002342"="Hanwha"
# XiongMai (DVR/camaras baratas, carne de Mirai)
"00128A"="XiongMai"; "001210"="XiongMai"
# Paneles de alarma / accesos
"F4B85E"="Ajax"; "00184F"="Risco"; "0040C7"="Honeywell"; "B82410"="Honeywell"
# NAS (almacenamiento en red)
"001132"="Synology"; "245EBE"="QNAP"; "00089B"="QNAP"
"00903D"="WD"; "0014EE"="WD"; "00903A"="WD"
# Routers / equipo de red
"4C5E0C"="Mikrotik"; "B869F4"="Mikrotik"; "E48D8C"="Mikrotik"; "DC2C6E"="Mikrotik"
"50C7BF"="TP-Link"; "A42BB0"="TP-Link"; "F4F26D"="TP-Link"; "14CC20"="TP-Link"
"5404A6"="Asus"; "1C872C"="Asus"; "AC9E17"="Asus"; "088620"="Asus"
}
$ALARM_BRANDS = @("Ajax","Risco","Honeywell","Resideo","Visonic")
function Guess-Vendor($mac) {
if (-not $mac) { return "desconocido" }
$clean = ($mac -replace '[^0-9A-Fa-f]', '').ToUpper()
if ($clean.Length -lt 6) { return "MAC $mac" }
$p = $clean.Substring(0, 6)
if ($OUI_VENDOR.ContainsKey($p)) { return $OUI_VENDOR[$p] }
return "MAC $mac"
}
# Inteligencia de vulnerabilidades por marca de videovigilancia.
# Solo para REPORTAR: el escaner no explota ni prueba estas credenciales.
$CAM_INTEL = @{
"Hikvision" = @{ cve="CVE-2021-36260"; cred="admin / 12345";
impacto="Si el firmware no esta parcheado, ejecutar comandos en la camara SIN contrasena (RCE) y usarla para entrar en la red, espiar o pivotar a otros equipos." }
"Dahua" = @{ cve="CVE-2021-33044 / 33045"; cred="admin / admin";
impacto="Saltarse el login del DVR/camara y ver el video en directo y grabado sin credenciales si esta sin parchear." }
"Axis" = @{ cve="CVE-2018-10660 / 10661"; cred="root / (definida al instalar)";
impacto="En firmware antiguo, ejecutar comandos en la camara sin autenticarse." }
"Uniview" = @{ cve="CVE-2021-45039 (varios)"; cred="admin / 123456";
impacto="Acceso no autorizado a la camara/NVR si esta sin parchear." }
"Reolink" = @{ cve="exposicion de credenciales (varios)"; cred="admin / (vacia)";
impacto="Acceso al video y a la configuracion si las credenciales son las de fabrica." }
"Hanwha" = @{ cve="varios segun modelo"; cred="admin / 4321";
impacto="Acceso no autorizado en firmware antiguo o con la contrasena de fabrica." }
"XiongMai" = @{ cve="Mirai / CVE-2017-7577"; cred="root / xc3511 (oculta)";
impacto="DVR/camara barato muy explotado por botnets (Mirai): control remoto, espionaje y uso para ataques DDoS. La contrasena root suele estar oculta y no se puede cambiar." }
}
# Inteligencia por marca de NAS (almacenamiento en red).
$NAS_INTEL = @{
"Synology" = @{ cve="varios DSM + ransomware eCh0raix"; cred="admin / admin";
impacto="Cifrar el NAS entero con ransomware (eCh0raix) y perder a la vez los datos y las copias de seguridad; o acceso remoto al NAS si DSM no esta parcheado." }
"QNAP" = @{ cve="DeadBolt / Qlocker (varios CVE-2021/2022)"; cred="admin / admin";
impacto="Cifrar el NAS con ransomware (DeadBolt/Qlocker) y exigir rescate; o tomar control remoto si QTS no esta parcheado. Los NAS son uno de los blancos preferidos del ransomware." }
"WD" = @{ cve="CVE-2018-17153 (auth bypass a root)"; cred="admin / (vacia)";
impacto="Acceso remoto como root al NAS si el firmware MyCloud es antiguo: leer, borrar o cifrar todo lo guardado." }
}
# Inteligencia por marca de equipo de red (router, gateway).
$ROUTER_INTEL = @{
"Mikrotik" = @{ cve="CVE-2018-14847 (Winbox auth bypass)"; cred="admin / (vacia)";
impacto="Leer la contrasena del router si RouterOS no esta parcheado y entrar en toda la red; redirigir o espiar el trafico." }
"TP-Link" = @{ cve="varios (CVE-2017-13772, CVE-2023-1389...)"; cred="admin / admin";
impacto="Cambiar el DNS del router para redirigir el trafico a paginas falsas, o ejecutar comandos si el firmware es antiguo." }
"Asus" = @{ cve="varios AsusWRT"; cred="admin / admin";
impacto="Tomar control del router con las credenciales de fabrica y manipular toda la red." }
}
# --------------------------------------------------------------------------
# 0 · Autorizacion
# --------------------------------------------------------------------------
Clear-Host
Write-Host "============================================================" -ForegroundColor DarkCyan
Write-Host " OK-COMPUTER · Escaner de auditoria de seguridad (v2)" -ForegroundColor White
Write-Host "============================================================" -ForegroundColor DarkCyan
Write-Host ""
Write-Host " Esta herramienta SOLO debe ejecutarse en redes donde el" -ForegroundColor Yellow
Write-Host " cliente te ha AUTORIZADO POR ESCRITO la auditoria." -ForegroundColor Yellow
Write-Host ""
$autorizadoPor = ""
if (-not $Yes) {
$resp = Read-Host " Confirmas que tienes autorizacion para auditar esta red? (escribe SI)"
if ($resp -notmatch '^\s*SI\s*$') {
Write-Host "`n Cancelado. Sin autorizacion no se ejecuta.`n" -ForegroundColor Red
exit
}
if (-not $Empresa) { $Empresa = Read-Host " Nombre del cliente" }
$autorizadoPor = Read-Host " Quien autoriza (nombre y cargo)"
}
$inicio = Get-Date
if (-not $Empresa) { $Empresa = "Cliente" }
$esAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
# ==========================================================================
# 1 · AUDITORIA DEL EQUIPO
# ==========================================================================
Write-Title "1 - Auditoria del equipo"
if (-not $esAdmin) {
Add-Finding "Info" "Equipo" "Ejecutado SIN privilegios de administrador" `
"Varias comprobaciones (SMBv1, BitLocker, parches, servicios) quedan incompletas." `
"Para la auditoria completa, ejecutar PowerShell como Administrador."
}
# --- Sistema operativo y soporte ---
$os = Get-CimInstance Win32_OperatingSystem
$build = [int]($os.BuildNumber)
$osName = $os.Caption
$eol = $false
if ($osName -match "Windows 7|Windows 8|Vista|XP|Server 2003|Server 2008|Server 2012") { $eol = $true }
if ($eol) {
Add-Finding "Critico" "Equipo" "Sistema operativo SIN soporte: $osName" `
"Sin parches de seguridad. Puerta de entrada favorita del ransomware." `
"Renovar o actualizar a una version con soporte (Windows 10 22H2 / 11)." `
"Explotar fallos publicos sin parchear para tomar el control del equipo."
} else {
Add-Finding "OK" "Equipo" "Sistema operativo con soporte: $osName (build $build)"
}
# --- Ultima actualizacion ---
$ultHotfix = Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 1
if ($ultHotfix -and $ultHotfix.InstalledOn) {
$dias = (New-TimeSpan -Start $ultHotfix.InstalledOn -End (Get-Date)).Days
if ($dias -gt 45) {
Add-Finding "Riesgo" "Parches" "Sin actualizaciones desde hace $dias dias" `
"Ultima: $($ultHotfix.InstalledOn.ToString('yyyy-MM-dd')) ($($ultHotfix.HotFixID))." `
"Activar Windows Update automatico y aplicar parches pendientes." `
"Aprovechar vulnerabilidades parcheadas hace meses que este equipo aun no tiene."
} else {
Add-Finding "OK" "Parches" "Actualizado recientemente (hace $dias dias)"
}
}
# --- Reinicio pendiente ---
$pending = $false
if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending") { $pending = $true }
if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") { $pending = $true }
if ($pending) {
Add-Finding "Info" "Parches" "Hay un reinicio pendiente" `
"Algunos parches no se aplican del todo hasta reiniciar." "Reiniciar el equipo."
}
# --- Firewall ---
$fw = Get-NetFirewallProfile
$fwOff = $fw | Where-Object { -not $_.Enabled }
if ($fwOff) {
Add-Finding "Riesgo" "Equipo" "Firewall desactivado en: $($fwOff.Name -join ', ')" `
"" "Activar el firewall de Windows en todos los perfiles." `
"Conectarse libremente a servicios del equipo que el firewall deberia bloquear."
} elseif ($fw) { Add-Finding "OK" "Equipo" "Firewall activo en todos los perfiles" }
# --- Antivirus / Defender ---
$mp = Get-MpComputerStatus
if ($mp) {
if (-not $mp.RealTimeProtectionEnabled) {
Add-Finding "Critico" "Equipo" "Proteccion en tiempo real DESACTIVADA" `
"" "Activar la proteccion en tiempo real del antivirus." `
"Ejecutar malware sin que nada lo detenga."
} else { Add-Finding "OK" "Equipo" "Antivirus con proteccion en tiempo real activa" }
if ($mp.AntivirusSignatureLastUpdated) {
$antig = (New-TimeSpan -Start $mp.AntivirusSignatureLastUpdated -End (Get-Date)).Days
if ($antig -gt 7) { Add-Finding "Riesgo" "Equipo" "Firmas de antivirus desactualizadas ($antig dias)" "" "Forzar actualizacion de firmas y revisar conectividad." "El antivirus no reconoce el malware mas reciente y puede dejarlo pasar." }
}
# Exclusiones de Defender (pueden esconder malware)
$excl = (Get-MpPreference).ExclusionPath
if ($excl) {
Add-Finding "Riesgo" "Equipo" "Defender tiene rutas excluidas del analisis" `
("Excluido: " + (($excl | Select-Object -First 4) -join '; ')) `
"Revisar que esas exclusiones sean legitimas; el malware suele esconderse ahi." `
"Colocar malware en una carpeta excluida para que el antivirus lo ignore."
}
}
# --- SMBv1 ---
$smb1 = (Get-SmbServerConfiguration).EnableSMB1Protocol
if ($smb1 -eq $true) {
Add-Finding "Critico" "Equipo" "SMBv1 ACTIVADO (EternalBlue / MS17-010)" `
"Protocolo obsoleto explotado por WannaCry y NotPetya." `
"Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol" `
"Cifrar toda la red con ransomware sin necesidad de contrasena (EternalBlue)." `
"MS17-010 / CVE-2017-0144"
} elseif ($smb1 -eq $false) { Add-Finding "OK" "Equipo" "SMBv1 desactivado" }
# --- Firma SMB ---
$smbSign = (Get-SmbServerConfiguration).RequireSecuritySigning
if ($smbSign -eq $false) {
Add-Finding "Riesgo" "Red" "SMB sin exigir firma (relay / SMB relay attack)" `
"Permite ataques de retransmision de credenciales en la red." `
"Exigir la firma SMB por GPO (RequireSecuritySigning)." `
"Robar y reutilizar credenciales mediante SMB relay." `
"NTLM relay / CVE-2019-1040"
}
# --- WDigest (credenciales en texto plano en memoria) ---
$wdigest = Reg "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest" "UseLogonCredential"
if ($wdigest -eq 1) {
Add-Finding "Critico" "Credenciales" "WDigest guarda contrasenas en texto plano en memoria" `
"Permite extraerlas con herramientas tipo Mimikatz." `
"Poner UseLogonCredential=0 en el registro." `
"Volcar las contrasenas en claro de quien haya iniciado sesion." `
"Mimikatz / MITRE T1003"
}
# --- UAC ---
$uac = Reg "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" "EnableLUA"
if ($uac -eq 0) {
Add-Finding "Critico" "Equipo" "UAC (Control de cuentas) DESACTIVADO" `
"Sin UAC, cualquier proceso se ejecuta con permisos altos sin avisar." `
"Reactivar UAC (EnableLUA=1)." `
"Ejecutar acciones de administrador sin ninguna advertencia."
}
# --- AutoLogon con contrasena en claro ---
$alPwd = Reg "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" "DefaultPassword"
if ($alPwd) {
Add-Finding "Critico" "Credenciales" "Contrasena de inicio de sesion guardada en el registro (AutoLogon)" `
"La contrasena del usuario esta en texto plano en el registro." `
"Quitar el AutoLogon o usar el cifrado LSA secret." `
"Leer directamente la contrasena del usuario desde el registro."
}
# --- LLMNR / NBT-NS (envenenamiento - Responder) ---
$llmnr = Reg "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient" "EnableMulticast"
if ($llmnr -ne 0) {
Add-Finding "Riesgo" "Red" "LLMNR habilitado (envenenamiento de nombres)" `
"Permite a un atacante en la red robar hashes de credenciales (Responder)." `
"Desactivar LLMNR por GPO (EnableMulticast=0)." `
"Hacerse pasar por otro equipo y capturar credenciales de la red." `
"LLMNR/NBT-NS poisoning (Responder)"
}
# --- PowerShell v2 (downgrade) ---
try {
$psv2 = Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2 -ErrorAction Stop
if ($psv2.State -eq "Enabled") {
Add-Finding "Riesgo" "Equipo" "PowerShell v2 habilitado (ataque de downgrade)" `
"PS v2 no registra ni aplica las protecciones modernas." `
"Desinstalar la caracteristica PowerShell v2." `
"Bajar a PS v2 para ejecutar codigo sin que se registre."
}
} catch {}
# --- BitLocker ---
$bl = $null
try { $bl = Get-BitLockerVolume -MountPoint $env:SystemDrive -ErrorAction Stop } catch { $bl = $null }
if ($bl) {
if ($bl.ProtectionStatus -ne "On") {
Add-Finding "Riesgo" "Equipo" "Disco del sistema SIN cifrar (BitLocker off)" `
"Si roban el equipo, los datos se leen sin contrasena." `
"Activar BitLocker en discos con datos sensibles." `
"Sacar el disco o arrancar desde un USB y leer todos los datos."
} else { Add-Finding "OK" "Equipo" "Disco del sistema cifrado con BitLocker" }
}
# --- RDP ---
$rdpDeny = Reg "HKLM:\System\CurrentControlSet\Control\Terminal Server" "fDenyTSConnections"
if ($rdpDeny -eq 0) {
$nla = Reg "HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" "UserAuthentication"
$nlaTxt = if ($nla -eq 0) { " y SIN autenticacion de red (NLA)" } else { "" }
$sev = if ($nla -eq 0) { "Critico" } else { "Riesgo" }
Add-Finding $sev "Red" "Escritorio remoto (RDP) habilitado$nlaTxt" `
"RDP es uno de los vectores de intrusion mas usados (BlueKeep, fuerza bruta)." `
"Si no se usa, desactivar. Si se usa, solo por VPN, con NLA y doble factor." `
"Entrar al escritorio del equipo por RDP (fuerza bruta o BlueKeep)." `
"CVE-2019-0708 (BlueKeep)"
}
# --- Servicios con ruta sin comillas (privilege escalation clasico) ---
$unquoted = Get-CimInstance Win32_Service | Where-Object {
$_.PathName -and $_.PathName -notmatch '^"' -and $_.PathName -match ' ' -and
$_.PathName -notmatch '^[A-Za-z]:\\Windows\\' -and $_.StartMode -ne 'Disabled'
} | Select-Object -First 5
if ($unquoted) {
Add-Finding "Riesgo" "Equipo" "Servicios con ruta sin comillas (escalada de privilegios)" `
("Ej.: " + (($unquoted | ForEach-Object { $_.Name }) -join ', ')) `
"Poner comillas en el ImagePath de esos servicios." `
"Colocar un ejecutable en la ruta y que arranque como SYSTEM."
}
# ==========================================================================
# 2 · CUENTAS Y ACCESOS
# ==========================================================================
Write-Title "2 - Cuentas y accesos"
$guest = Get-LocalUser | Where-Object { $_.SID -like '*-501' }
if ($guest -and $guest.Enabled) {
Add-Finding "Riesgo" "Cuentas" "Cuenta Invitado habilitada" "" "Deshabilitar la cuenta de invitado." "Acceder al equipo sin credenciales."
}
$adminAcct = Get-LocalUser | Where-Object { $_.SID -like '*-500' }
if ($adminAcct -and $adminAcct.Enabled) {
Add-Finding "Info" "Cuentas" "Cuenta Administrador integrada activa" "" "Renombrarla y/o deshabilitarla; usar cuentas nominales."
}
$sinCaducar = Get-LocalUser | Where-Object { $_.Enabled -and $_.PasswordExpires -eq $null -and $_.Name -notmatch 'DefaultAccount|WDAGUtility' }
if ($sinCaducar) {
Add-Finding "Riesgo" "Cuentas" "Contrasenas que nunca caducan: $($sinCaducar.Name -join ', ')" `
"" "Aplicar politica de caducidad de contrasenas." `
"Reutilizar una contrasena filtrada que nunca cambia."
}
# PasswordRequired=false NO significa que la cuenta este sin contrasena: es la
# politica (si esta apagada, la cuenta PUEDE quedarse sin ella). Para detectar
# cuentas realmente sin contrasena, combinamos politica relajada + PasswordLastSet
# vacio (nunca se establecio una). Asi evitamos falsos positivos en cuentas que
# SI tienen contrasena pero tienen la politica relajada (caso habitual en
# cuentas Microsoft o cuentas locales convertidas).
$sinPwd = Get-LocalUser | Where-Object {
$_.Enabled -and $_.PasswordRequired -eq $false -and -not $_.PasswordLastSet
}
if ($sinPwd) {
Add-Finding "Critico" "Cuentas" "Cuentas SIN contrasena: $($sinPwd.Name -join ', ')" `
"Detectado por PasswordRequired=false Y PasswordLastSet vacio (no se ha establecido nunca una contrasena)." `
"Exigir una contrasena fuerte a esas cuentas o deshabilitarlas." `
"Iniciar sesion en el equipo sin necesidad de contrasena."
}
# Por separado: cuentas con politica relajada pero que SI tienen contrasena
# establecida -> solo informativo, no critico. Se ignora DefaultAccount y la
# WDAGUtility (cuentas de sistema gestionadas por Windows).
$pwdPolicyOff = Get-LocalUser | Where-Object {
$_.Enabled -and $_.PasswordRequired -eq $false -and $_.PasswordLastSet -and
$_.Name -notmatch 'DefaultAccount|WDAGUtility'
}
if ($pwdPolicyOff) {
Add-Finding "Info" "Cuentas" "Cuentas con politica de contrasena relajada: $($pwdPolicyOff.Name -join ', ')" `
"Tienen contrasena, pero la politica permite quedarse sin ella si se cambia (PasswordRequired=false). Riesgo bajo si la contrasena es fuerte." `
"Reforzar la politica: Set-LocalUser -Name 'NOMBRE' -PasswordRequired:`$true"
}
# Administradores locales (por SID, vale en Windows en espanol)
try {
$admins = Get-LocalGroupMember -SID 'S-1-5-32-544' -ErrorAction Stop | ForEach-Object { $_.Name }
if ($admins.Count -gt 3) {
Add-Finding "Riesgo" "Cuentas" "Demasiados administradores locales ($($admins.Count))" `
("Miembros: " + ($admins -join ', ')) `
"Reducir el grupo Administradores al minimo imprescindible." `
"Cualquiera de esas cuentas comprometida da control total del equipo."
} else {
Add-Finding "OK" "Cuentas" "Administradores locales acotados ($($admins.Count))"
}
} catch {}
# ==========================================================================
# 3 · EXPOSICION LOCAL (puertos a la escucha y recursos compartidos)
# ==========================================================================
Write-Title "3 - Exposicion local"
$listen = Get-NetTCPConnection -State Listen | Where-Object { $_.LocalAddress -in '0.0.0.0','::' } | Select-Object -ExpandProperty LocalPort -Unique
$riesgoLocal = $listen | Where-Object { $_ -in 445,3389,5985,5986,1433,3306,5432,139,135 }
if ($riesgoLocal) {
Add-Finding "Info" "Equipo" "Servicios sensibles a la escucha: $((($riesgoLocal | Sort-Object) -join ', '))" `
"Estos puertos abiertos al exterior amplian la superficie de ataque." `
"Cerrar o filtrar por firewall los que no se usen."
}
$shares = Get-SmbShare | Where-Object { $_.Name -notmatch '\$$' }
foreach ($sh in $shares) {
$acc = Get-SmbShareAccess -Name $sh.Name | Where-Object { $_.AccountName -match 'Everyone|Todos' -and $_.AccessRight -ne 'Deny' }
if ($acc) {
Add-Finding "Riesgo" "Red" "Recurso compartido abierto a 'Todos': $($sh.Name)" `
"Ruta: $($sh.Path)" `
"Restringir el acceso del recurso a usuarios concretos." `
"Leer o modificar los archivos de ese recurso desde cualquier equipo de la red."
}
}
# ==========================================================================
# 4 · WIFI
# ==========================================================================
Write-Title "4 - Redes wifi"
$wifi = netsh wlan show networks mode=bssid 2>$null
if ($wifi) {
$abiertas = ($wifi | Select-String "Autenticaci|Authentication" | Where-Object { $_ -match "Abrir|Open" }).Count
if ($abiertas -gt 0) {
Add-Finding "Riesgo" "Wifi" "$abiertas red(es) wifi abiertas en el entorno" `
"Una wifi abierta o WEP es una puerta a la red." `
"Usar WPA2/WPA3 y separar la wifi de invitados de la de trabajo." `
"Conectarse a la red sin contrasena."
} else { Add-Finding "OK" "Wifi" "Sin wifis abiertas en el entorno" }
}
# Claves wifi guardadas (texto plano)
$perfiles = (netsh wlan show profiles 2>$null | Select-String ":\s*(.+)$") | ForEach-Object { $_.Matches.Groups[1].Value.Trim() }
$conClave = 0
foreach ($pf in $perfiles) {
$det = netsh wlan show profile name="$pf" key=clear 2>$null
if ($det -match "Contenido de (la )?clave|Key Content") { $conClave++ }
}
if ($conClave -gt 0) {
Add-Finding "Riesgo" "Wifi" "$conClave clave(s) wifi guardadas en claro en este equipo" `
"Cualquiera con acceso al equipo puede leer las contrasenas wifi." `
"Limitar quien usa el equipo; las claves se ven con 'netsh wlan show profile key=clear'." `
"Leer las contrasenas wifi guardadas y entrar en la red."
}
# ==========================================================================
# 5 · DESCUBRIMIENTO DE RED
# ==========================================================================
if (-not $SkipDiscovery) {
Write-Title "5 - Descubrimiento de dispositivos en la red"
if (-not $Target) {
$ipcfg = Get-NetIPConfiguration | Where-Object { $_.IPv4DefaultGateway -ne $null -and $_.NetAdapter.Status -eq "Up" } | Select-Object -First 1
$miIp = $ipcfg.IPv4Address.IPAddress
if ($miIp) { $base = ($miIp -split '\.')[0..2] -join '.'; $Target = "$base.0/24" }
} else { $base = (($Target -split '/')[0] -split '\.')[0..2] -join '.' }
Write-Host " [i] Subred objetivo: $Target" -ForegroundColor Gray
# Barrido de hosts vivos (ping asincrono)
Write-Host " [i] Barriendo 254 direcciones..." -ForegroundColor Gray
$pings = 1..254 | ForEach-Object {
$p = New-Object System.Net.NetworkInformation.Ping
[pscustomobject]@{ Ip="$base.$_"; Task=$p.SendPingAsync("$base.$_", 500) }
}
[System.Threading.Tasks.Task]::WaitAll($pings.Task) | Out-Null
$vivos = $pings | Where-Object { $_.Task.Result.Status -eq "Success" } | Select-Object -ExpandProperty Ip
# Tabla ARP para MAC/fabricante
$arp = @{}
try { Get-NetNeighbor -AddressFamily IPv4 -ErrorAction Stop | Where-Object { $_.LinkLayerAddress } | ForEach-Object { $arp[$_.IPAddress] = $_.LinkLayerAddress } } catch {}
Write-Host " [i] $($vivos.Count) host(s) vivos. Escaneando puertos..." -ForegroundColor Gray
$portList = if ($Quick) { $QUICK_PORTS } else { $PORTS.Keys }
foreach ($ip in $vivos) {
$abiertos = Scan-HostPorts -IpAddress $ip -Ports $portList
$nombre = try { [System.Net.Dns]::GetHostEntry($ip).HostName } catch { "" }
$mac = $arp[$ip]
# --- Huella HTTP del panel web (para deducir marca) ---
$banner = ""
foreach ($wp in 80,8000,8080,5000) {
if (($abiertos -contains $wp) -and -not $banner) { $banner = Get-HttpInfo -IpAddress $ip -Port $wp }
}
# --- Marca: primero por MAC (OUI), si no por la huella del panel ---
$marca = Guess-Vendor $mac
$conocidas = @($CAM_INTEL.Keys) + @($NAS_INTEL.Keys) + @($ROUTER_INTEL.Keys) + $ALARM_BRANDS
if ($marca -notin $conocidas) {
if ($banner -match 'Hikvision|App-webs|DNVRS') { $marca = 'Hikvision' }
elseif ($banner -match 'Dahua') { $marca = 'Dahua' }
elseif ($banner -match 'Axis') { $marca = 'Axis' }
elseif ($banner -match 'Uniview') { $marca = 'Uniview' }
elseif ($banner -match 'Reolink') { $marca = 'Reolink' }
elseif ($banner -match 'uc-httpd|Hi3520|XiongMai|NETSurveillance') { $marca = 'XiongMai' }
elseif ($banner -match 'Synology|DiskStation|DSM/') { $marca = 'Synology' }
elseif ($banner -match 'QNAP|QTS|QuTS') { $marca = 'QNAP' }
elseif ($banner -match 'MyCloud|WDMyCloud') { $marca = 'WD' }
elseif ($banner -match 'RouterOS|MikroTik') { $marca = 'Mikrotik' }
elseif ($banner -match 'TP-Link|tplinkwifi') { $marca = 'TP-Link' }
elseif ($banner -match 'ASUSWRT|Asus Wireless') { $marca = 'Asus' }
}
$marcaConocida = ($marca -and $marca -notmatch '^MAC |^desconocido$')
# --- Clasificacion del dispositivo ---
$esVideo = ($abiertos -contains 554 -or $abiertos -contains 8000 -or $abiertos -contains 37777 -or $abiertos -contains 34567 -or $abiertos -contains 8899 -or $abiertos -contains 8554 -or $abiertos -contains 9000)
$esGrabador = ($abiertos -contains 37777 -or $abiertos -contains 34567 -or $abiertos -contains 8899 -or ($esVideo -and ($abiertos -contains 8000)))
$esAlarma = ($ALARM_BRANDS -contains $marca)
$esNas = $NAS_INTEL.ContainsKey($marca) -or (($abiertos -contains 5000) -and ($abiertos -contains 5001))
$esRouter = $ROUTER_INTEL.ContainsKey($marca)
$esVoIP = $abiertos -contains 5060
$tipo = "Equipo / desconocido"
if ($esAlarma) { $tipo = "Panel de alarma / accesos" }
elseif ($esNas) { $tipo = $(if ($marcaConocida) { "NAS $marca" } else { "NAS" }) }
elseif ($esGrabador) { $tipo = "Videograbador (DVR/NVR)" }
elseif ($esVideo) { $tipo = "Camara IP" }
elseif ($esRouter) { $tipo = "Router $marca" }
elseif ($esVoIP) { $tipo = "Centralita / VoIP (SIP)" }
elseif ($abiertos -contains 1433 -or $abiertos -contains 3306 -or $abiertos -contains 5432 -or $abiertos -contains 27017) { $tipo = "Servidor de base de datos" }
elseif ($abiertos -contains 3389 -or $abiertos -contains 5985) { $tipo = "Servidor / equipo Windows" }
elseif ($abiertos -contains 9100 -or $abiertos -contains 631) { $tipo = "Impresora" }
elseif (($abiertos -contains 80 -or $abiertos -contains 443) -and $abiertos.Count -le 3) { $tipo = "Router / dispositivo web" }
[void]$script:Hosts.Add([pscustomobject]@{
Ip=$ip; Nombre=$nombre; Mac=$mac; Tipo=$tipo
Marca=$(if ($marcaConocida) { $marca } else { "" })
Puertos=(($abiertos | ForEach-Object { "$_/$($PORTS[[string]$_])" }) -join ", ")
Banner=$banner
})
$etq = if ($nombre) { "$ip ($nombre)" } else { $ip }
$marcaTxt = if ($marcaConocida) { " · $marca" } else { "" }
Write-Host " $etq [$tipo$marcaTxt] -> $(($abiertos) -join ', ')" -ForegroundColor White
# --- Hallazgos por host ---
foreach ($dp in $DB_PORTS.Keys) {
if ($abiertos -contains [int]$dp) {
Add-Finding "Critico" "Bases de datos" "$($DB_PORTS[$dp]) accesible en red: $ip" `
"Una base de datos expuesta en la red es un objetivo directo de robo y ransomware." `
"Cerrar el puerto al exterior; acceso solo desde la app y con credenciales fuertes." `
"Conectarse a la base de datos para robarla, borrarla o cifrarla."
}
}
if ($abiertos -contains 23) {
Add-Finding "Critico" "Red" "Telnet abierto en $ip" `
"Telnet va sin cifrar; tipico en camaras/DVR antiguos." `
"Desactivar Telnet; usar SSH o panel seguro." `
"Capturar usuario y contrasena al vuelo (texto plano)."
}
if ($abiertos -contains 3389) {
Add-Finding "Riesgo" "Red" "RDP (3389) abierto en $ip" "" `
"Limitar RDP a VPN y doble factor; cerrar la exposicion directa." `
"Fuerza bruta o BlueKeep contra el escritorio remoto." `
"CVE-2019-0708 (BlueKeep)"
}
if ($abiertos -contains 445 -and $abiertos -contains 139) {
Add-Finding "Info" "Red" "Comparticion de archivos (SMB) en $ip" `
"Revisar que los recursos compartidos no esten abiertos a todos." "Auditar permisos de los recursos."
}
if ($esVideo -or $esGrabador) {
$tipoTxt = if ($esGrabador) { "Videograbador (DVR/NVR)" } else { "Camara IP" }
$como = if ($mac) { "MAC $mac" } else { "huella del panel web" }
if ($CAM_INTEL.ContainsKey($marca)) {
$intel = $CAM_INTEL[$marca]
Add-Finding "Riesgo" "Videovigilancia" "$tipoTxt $marca en red: $ip" `
("Detectado por $como. Credenciales de fabrica habituales de " + $marca + ": " + $intel.cred + ".") `
"Verificar que el firmware esta actualizado, cambiar las credenciales de fabrica, cerrar el acceso desde internet y aislar la videovigilancia en su propia VLAN." `
$intel.impacto `
$intel.cve
} else {
Add-Finding "Riesgo" "Videovigilancia" "$tipoTxt en red: $ip" `
"Detectado por $como. Marca no identificada con detalle." `
"Identificar marca/modelo, actualizar firmware, cambiar credenciales de fabrica y aislar en una VLAN propia." `
"Entrar a la camara/DVR con la contrasena de fabrica y ver o grabar imagenes; muchos modelos baratos son carne de botnet (Mirai)."
}
}
if ($esAlarma) {
Add-Finding "Riesgo" "Alarmas/Accesos" ("Posible panel de alarma o control de accesos (" + $marca + "): $ip") `
"Detectado por la MAC del fabricante." `
"Verificar credenciales y firmware, y que el panel no sea accesible desde fuera de la red." `
"Manipular o anular el sistema de alarma o de accesos si conserva las credenciales de fabrica."
}
if ($esNas) {
if ($NAS_INTEL.ContainsKey($marca)) {
$nasIntel = $NAS_INTEL[$marca]
Add-Finding "Critico" "NAS / Almacenamiento" "NAS $marca en red: $ip" `
("Credenciales de fabrica habituales de " + $marca + ": " + $nasIntel.cred + ". Los NAS son objetivo prioritario de ransomware.") `
"Actualizar firmware, cambiar credenciales de fabrica, deshabilitar el acceso del NAS desde internet y mantener una copia externa OFFLINE (no en el propio NAS)." `
$nasIntel.impacto `
$nasIntel.cve
} else {
Add-Finding "Riesgo" "NAS / Almacenamiento" "Posible NAS en red: $ip" `
"Puertos DSM/QTS abiertos pero marca no identificada con detalle." `
"Identificar el NAS, actualizar firmware, contrasenas fuertes y copia externa fuera del NAS." `
"Si el NAS guarda las copias de seguridad, un ataque borra a la vez los datos y las copias."
}
}
if ($esRouter -and $ROUTER_INTEL.ContainsKey($marca)) {
$rIntel = $ROUTER_INTEL[$marca]
Add-Finding "Riesgo" "Red / Router" "Router $marca en red: $ip" `
("Credenciales de fabrica habituales: " + $rIntel.cred + ". Cambiar el router compromete TODA la red.") `
"Actualizar firmware del router, cambiar la contrasena de administracion, deshabilitar el acceso desde WAN y revisar el DNS." `
$rIntel.impacto `
$rIntel.cve
}
if ($esVoIP) {
Add-Finding "Riesgo" "VoIP / SIP" "Servicio SIP (5060) accesible en $ip" `
"Las centralitas SIP mal protegidas son blanco habitual de fraude telefonico (toll fraud)." `
"Restringir SIP a IPs conocidas, exigir credenciales fuertes y bloquear llamadas internacionales innecesarias por defecto." `
"Realizar llamadas internacionales a costa del cliente en pocas horas (factura de miles de euros)."
}
if ($abiertos -contains 21) {
Add-Finding "Riesgo" "Red" "FTP (21) abierto en $ip" "FTP clasico no cifra credenciales." "Migrar a SFTP/FTPS." "Interceptar credenciales FTP."
}
if ($abiertos -contains 161) {
Add-Finding "Riesgo" "Red" "SNMP abierto en $ip" "SNMP con 'community' por defecto filtra informacion del dispositivo." "Cambiar la community y restringir SNMP." "Leer la configuracion del dispositivo (usuarios, interfaces, rutas) con la community 'public'."
}
}
if ($script:Hosts.Count -eq 0) { Write-Host " [i] Sin hosts (red aislada o firewall)." -ForegroundColor Gray }
} else { Write-Host " [i] Descubrimiento de red omitido (-SkipDiscovery)." -ForegroundColor Gray }
# ==========================================================================
# 5b · COMANDOS DE SOLUCION POR HALLAZGO
# Mapa "patron de titulo" -> comando PowerShell para arreglarlo. Se aplica
# por Add-Member para no tocar Add-Finding ni cada llamada. Solo para
# fallos del propio Windows con remedio por linea de comando. Para camaras,
# NAS, routers, etc., el arreglo se hace en su propio panel.
# ==========================================================================
$FIXES = [ordered]@{
"SMBv1 ACTIVADO" = 'Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -NoRestart'
"Firewall desactivado" = 'Set-NetFirewallProfile -All -Enabled True'
"Proteccion en tiempo real DESACTIVADA" = 'Set-MpPreference -DisableRealtimeMonitoring $false'
"Firmas de antivirus desactualizadas" = 'Update-MpSignature'
"SMB sin exigir firma" = 'Set-SmbServerConfiguration -RequireSecuritySigning $true -EnableSecuritySignature $true -Force'
"WDigest guarda contrasenas" = "New-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest' -Name UseLogonCredential -PropertyType DWord -Value 0 -Force"
"Hay un reinicio pendiente" = 'Restart-Computer'
"Defender tiene rutas excluidas" = @'
# Lista actual de exclusiones:
Get-MpPreference | Select-Object -ExpandProperty ExclusionPath
# Quitar una sospechosa (sustituye la ruta):
Remove-MpPreference -ExclusionPath 'C:\ruta\sospechosa'
'@
"UAC (Control de cuentas) DESACTIVADO" = @'
Set-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name EnableLUA -Value 1
Write-Host 'Reinicia el equipo para que se aplique.'
'@
"Contrasena de inicio de sesion guardada" = @'
Remove-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name DefaultPassword -Force
Remove-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name AutoAdminLogon -Force -ErrorAction SilentlyContinue
'@
"LLMNR habilitado" = @'
New-Item 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient' -Force | Out-Null
New-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient' -Name EnableMulticast -PropertyType DWord -Value 0 -Force
'@
"PowerShell v2 habilitado" = @'
Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2 -NoRestart
Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root -NoRestart
'@
"Disco del sistema SIN cifrar" = @'
# BitLocker con TPM (anota bien la clave de recuperacion que devuelve):
Enable-BitLocker -MountPoint $env:SystemDrive -EncryptionMethod XtsAes256 -UsedSpaceOnly -TpmProtector
Add-BitLockerKeyProtector -MountPoint $env:SystemDrive -RecoveryPasswordProtector
(Get-BitLockerVolume -MountPoint $env:SystemDrive).KeyProtector
'@
"Escritorio remoto (RDP) habilitado" = @'
# Desactivar RDP del todo:
Set-ItemProperty 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -Value 1
# O, si se mantiene, exigir NLA:
Set-ItemProperty 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name UserAuthentication -Value 1
'@
"Cuenta Invitado habilitada" = @'
Disable-LocalUser -SID (Get-LocalUser | Where-Object { $_.SID -like '*-501' }).SID
'@
"Contrasenas que nunca caducan" = @'
# Para cada cuenta listada en el titulo (sustituye NOMBRE):
Set-LocalUser -Name 'NOMBRE' -PasswordNeverExpires $false
'@
"Cuentas SIN contrasena" = @'
# Asigna una contrasena fuerte (sustituye NOMBRE):
$p = Read-Host 'Nueva contrasena' -AsSecureString
Set-LocalUser -Name 'NOMBRE' -Password $p
'@
"Recurso compartido abierto a 'Todos'" = @'
# Sustituye NOMBRE_RECURSO por el del titulo:
Revoke-SmbShareAccess -Name 'NOMBRE_RECURSO' -AccountName 'Todos' -Force
Revoke-SmbShareAccess -Name 'NOMBRE_RECURSO' -AccountName 'Everyone' -Force
'@
"Sin actualizaciones desde hace" = @'
# Instalar y usar PSWindowsUpdate:
Install-Module PSWindowsUpdate -Force -Scope AllUsers
Import-Module PSWindowsUpdate
Get-WindowsUpdate -Install -AcceptAll -AutoReboot
'@
"Servicios con ruta sin comillas" = @'
# Para cada servicio listado (sustituye NOMBRE_SERVICIO y la ruta correcta):
sc.exe config "NOMBRE_SERVICIO" binPath= "\"C:\Ruta Con Espacios\Servicio.exe\""
'@
}
foreach ($pat in $FIXES.Keys) {
foreach ($f in $script:Findings) {
if ($f.Titulo -like "*$pat*") {
$f | Add-Member -NotePropertyName Fix -NotePropertyValue $FIXES[$pat] -Force
}
}
}
# ==========================================================================
# 6 · NOTA Y RESUMEN
# ==========================================================================
$crit = @($script:Findings | Where-Object { $_.Sev -eq "Critico" }).Count
$ries = @($script:Findings | Where-Object { $_.Sev -eq "Riesgo" }).Count
$oks = @($script:Findings | Where-Object { $_.Sev -eq "OK" }).Count
$nota = [Math]::Max(0, 100 - ($crit * 15) - ($ries * 6))
Write-Title "Resumen"
$col = if ($nota -ge 75) { "Green" } elseif ($nota -ge 50) { "Yellow" } else { "Red" }
Write-Host " Nota de seguridad: $nota/100" -ForegroundColor $col
Write-Host " $crit criticos · $ries riesgos · $oks correctos · $($script:Hosts.Count) dispositivos" -ForegroundColor Gray
# ==========================================================================
# 7 · INFORME HTML (en el Escritorio del usuario)
# ==========================================================================
$desktop = [Environment]::GetFolderPath('DesktopDirectory')
if (-not $desktop -or -not (Test-Path $desktop)) { $desktop = Join-Path $env:USERPROFILE 'Desktop' }
if (-not (Test-Path $desktop)) { $desktop = $env:USERPROFILE }
$safe = ($Empresa -replace '[^\w\-]', '_')
$OutFile = Join-Path $desktop ("informe-seguridad-$safe-$(Get-Date -Format 'yyyyMMdd-HHmm').html")
$colNota = if ($nota -ge 75) { "#1f7a36" } elseif ($nota -ge 50) { "#c47a00" } else { "#b32424" }
$veredicto = if ($nota -ge 75) { "Situacion razonable, con mejoras puntuales." }
elseif ($nota -ge 50) { "Riesgos relevantes que conviene corregir pronto." }
else { "Exposicion seria. Hay que actuar cuanto antes." }
function HE($s) { if ($null -eq $s) { return "" }; [System.Web.HttpUtility]::HtmlEncode($s) }
$sb = New-Object System.Text.StringBuilder
[void]$sb.Append(@"
Informe de seguridad - $(HE $Empresa)
COMANDO PARA ARREGLARLO · PowerShell como Administrador
$(HE $f.Fix)
"
}
[void]$sb.Append("
$(HE $f.Area) - $(HE $f.Titulo)
$d$imp$s$cmd
")
}
}
}
# --- Inventario de dispositivos ---
if ($script:Hosts.Count -gt 0) {
[void]$sb.Append("
Inventario de dispositivos en red
IP
Nombre
Tipo
Marca
Puertos abiertos
Pista
")
foreach ($hh in $script:Hosts) {
[void]$sb.Append("
$(HE $hh.Ip)
$(HE $hh.Nombre)
$(HE $hh.Tipo)
$(HE $hh.Marca)
$(HE $hh.Puertos)
$(HE $hh.Banner)
")
}
[void]$sb.Append("
")
}
# --- Lo correcto ---
$oksF = $script:Findings | Where-Object Sev -eq "OK"
if ($oksF) {
[void]$sb.Append("
✓ Lo que esta correcto
")
foreach ($f in $oksF) { [void]$sb.Append("
$(HE $f.Titulo)
") }
[void]$sb.Append("
")
}
[void]$sb.Append(@"
Siguiente paso. Estos puntos se pueden dejar cerrados sin que tengas que llamar a varios proveedores. Reviso, te lo arreglo, y de paso te enseno como sacarle mas partido a tu sistema en el dia a dia. Con los primeros clientes la puesta en marcha la hago sin coste por adelantado: pagas cuando veas el resultado.
"@)
[System.IO.File]::WriteAllText($OutFile, $sb.ToString(), (New-Object System.Text.UTF8Encoding($true)))
Write-Host "`n Informe guardado en el Escritorio:" -ForegroundColor Cyan
Write-Host " $OutFile`n" -ForegroundColor White
$dur = [int]((Get-Date) - $inicio).TotalSeconds
Write-Host " Auditoria completada en $dur s.`n" -ForegroundColor DarkGray
try { Start-Process $OutFile } catch {}