-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscanner.py
More file actions
222 lines (192 loc) · 10.6 KB
/
Copy pathscanner.py
File metadata and controls
222 lines (192 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
"""Simulated port scan and mitigation report for a single host.
Educational simulation: no packet ever leaves the machine. The scan is drawn
from a fixed seed, so the same code always produces the same report -- before,
every run invented a different host and the committed report matched nothing.
Three findings that were wrong on their own terms have been fixed:
* The executive summary always claimed Telnet was the worst finding, even on
runs where port 23 came back closed. A report that names a risk the scan did
not find is worse than no report: it is the one line an ops team reads.
* The low-severity finding on 8080 was raised on a coin flip, whether or not
8080 was open. Nothing should be reported against a closed port.
* The rules were a chain of ifs. They are a table now, so adding a service is
a row and the report cannot drift from the catalogue.
"""
import json
import random
from datetime import datetime
# --- Configuration ---
TARGET_IP = "192.168.1.100"
# Fixed, for two reasons. A scan report has to be reproducible: with the seed
# floating, every run invented a different host and the committed report
# described none of them. And this particular value was picked so the example
# host exercises all four severities -- the neglected legacy box you actually
# find in a small company, with Telnet and RDP still listening. Any other seed
# gives another host; the analysis does not change.
SEED = 3078
COMMON_PORTS = {
21: "FTP (File Transfer Protocol)",
22: "SSH (Secure Shell)",
23: "Telnet",
25: "SMTP (Simple Mail Transfer Protocol)",
80: "HTTP (Web Server)",
443: "HTTPS (Secure Web Server)",
3389: "RDP (Remote Desktop Protocol)",
8080: "HTTP Proxy/Alternative Web Server",
}
SEVERITY_ORDER = ["Critical", "High", "Medium", "Low"]
# Each rule fires only on evidence from this scan. `requires` must be open and
# every port in `absent` must be closed.
RULES = [
{
"port": 23, "service": "Telnet", "severity": "Critical",
"requires": [23], "absent": [],
"description": "Telnet transmits data in plaintext, including passwords. MUST be disabled and replaced with SSH (Port 22).",
"description_es": "Telnet transmite los datos en claro, contraseñas incluidas. Debe deshabilitarse y sustituirse por SSH (puerto 22).",
"fix": "Disable the Telnet service and move remote administration to SSH on port 22.",
"fix_es": "Deshabilitar el servicio Telnet y mover la administración remota a SSH en el puerto 22.",
},
{
"port": 3389, "service": "RDP", "severity": "High",
"requires": [3389], "absent": [],
"description": "Remote Desktop reachable from the network is the most common entry point for ransomware. Exposed RDP is brute-forced continuously.",
"description_es": "Un Escritorio Remoto accesible desde la red es la vía de entrada más común del ransomware. El RDP expuesto recibe ataques de fuerza bruta de forma continua.",
"fix": "Put RDP behind a VPN or a bastion host, enforce network-level authentication and lock out repeated failures.",
"fix_es": "Poner el RDP detrás de una VPN o de un host bastión, exigir autenticación a nivel de red y bloquear los intentos repetidos.",
},
{
"port": 21, "service": "FTP", "severity": "High",
"requires": [21], "absent": [],
"description": "FTP is often unencrypted, risking credential exposure. Consider disabling or securing with SFTP/FTPS.",
"description_es": "FTP suele ir sin cifrar, con el riesgo de exponer las credenciales. Conviene deshabilitarlo o asegurarlo con SFTP o FTPS.",
"fix": "Replace FTP with SFTP over SSH, or enable FTPS and restrict access to trusted addresses.",
"fix_es": "Sustituir FTP por SFTP sobre SSH, o habilitar FTPS y restringir el acceso a direcciones de confianza.",
},
{
"port": 80, "service": "HTTP", "severity": "Medium",
"requires": [80], "absent": [443],
"description": "Web server running without HTTPS. All traffic is unencrypted. Implement SSL/TLS.",
"description_es": "Servidor web sirviendo sin HTTPS: todo el tráfico viaja sin cifrar. Hay que implantar SSL/TLS.",
"fix": "Issue a certificate, serve the site over 443 and redirect every request on 80 to HTTPS.",
"fix_es": "Emitir un certificado, servir el sitio por el 443 y redirigir al HTTPS toda petición que entre por el 80.",
},
{
"port": 25, "service": "SMTP", "severity": "Medium",
"requires": [25], "absent": [],
"description": "An SMTP service reachable from outside can be abused as an open relay to send mail in your name.",
"description_es": "Un servicio SMTP accesible desde fuera puede usarse como relay abierto para enviar correo en tu nombre.",
"fix": "Require authentication for relaying, restrict the listening interface and publish SPF and DKIM records.",
"fix_es": "Exigir autenticación para retransmitir, restringir la interfaz de escucha y publicar registros SPF y DKIM.",
},
{
"port": 8080, "service": "HTTP Proxy", "severity": "Low",
"requires": [8080], "absent": [],
"description": "Non-standard port 8080 is open. Ensure the service is properly configured and access is restricted.",
"description_es": "El puerto 8080, que no es estándar, está abierto. Hay que confirmar que el servicio está bien configurado y el acceso restringido.",
"fix": "Confirm what is listening on 8080 and firewall it off unless it has to be reachable.",
"fix_es": "Comprobar qué escucha en el 8080 y cerrarlo en el cortafuegos salvo que tenga que ser accesible.",
},
]
def simulate_nmap_scan(ip, seed=SEED):
"""Simulates an Nmap scan, returning a list of open ports and services."""
rng = random.Random(seed)
return {
port: service
for port, service in COMMON_PORTS.items()
if rng.random() < 0.4 # 40% chance of being open
}
def analyze_vulnerabilities(open_ports):
"""Analyzes open ports for potential vulnerabilities."""
abiertos = set(open_ports)
encontradas = [
{k: r[k] for k in ("port", "service", "severity",
"description", "description_es", "fix", "fix_es")}
for r in RULES
if set(r["requires"]) <= abiertos and not (set(r["absent"]) & abiertos)
]
encontradas.sort(key=lambda v: SEVERITY_ORDER.index(v["severity"]))
return encontradas
def _resumen(open_ports, vulnerabilities):
"""One honest sentence about what the scan actually found."""
n_p, n_v = len(open_ports), len(vulnerabilities)
if not vulnerabilities:
return (f"Se encontraron **{n_p}** puertos abiertos y **ninguna** vulnerabilidad "
"de las que este escáner reconoce.")
peor = vulnerabilities[0]
return (f"Se encontraron **{n_p}** puertos abiertos y **{n_v}** vulnerabilidades "
f"potenciales. La más grave es {peor['service']} en el puerto {peor['port']} "
f"(severidad {peor['severity']}).")
def generate_report(ip, open_ports, vulnerabilities, scanned_at=None):
"""Generates a detailed security report in Markdown format."""
cuando = (scanned_at or datetime.now()).strftime("%Y-%m-%d %H:%M:%S")
report = f"# 🛡️ Reporte de Escaneo de Vulnerabilidades - {ip}\n\n"
report += f"**Fecha del Escaneo:** {cuando}\n"
report += f"**Objetivo (IP):** `{ip}`\n"
report += "**Herramienta de Simulación:** Nmap (Simulado)\n\n"
report += "## 1. Resumen Ejecutivo\n"
report += _resumen(open_ports, vulnerabilities) + "\n\n"
report += "## 2. Puertos Abiertos Encontrados\n"
if open_ports:
report += "| Puerto | Servicio | Estado |\n"
report += "| :--- | :--- | :--- |\n"
for port, service in sorted(open_ports.items()):
report += f"| {port} | {service} | Abierto |\n"
else:
report += "No se encontraron puertos comunes abiertos.\n"
report += "\n## 3. Vulnerabilidades Detectadas\n"
if vulnerabilities:
for vul in vulnerabilities:
report += f"### 🚨 Puerto {vul['port']} - {vul['service']} (Severidad: {vul['severity']})\n"
report += f"**Descripción:** {vul['description_es']}\n\n"
else:
report += "No se detectaron vulnerabilidades de alta prioridad.\n"
# The mitigations are the ones for the findings of THIS scan, in the order
# they should be tackled. A fixed list used to recommend disabling Telnet
# on hosts where Telnet was closed.
report += "\n## 4. Recomendaciones de Mitigación\n"
if vulnerabilities:
for vul in vulnerabilities:
report += f"* **{vul['service']} (puerto {vul['port']}):** {vul['fix_es']}\n"
else:
report += "* Mantener cerrado todo puerto que no preste un servicio necesario.\n"
return report
def scan_payload(ip=TARGET_IP, seed=SEED, scanned_at=None):
"""Everything the scan knows, ready to serialise."""
open_ports = simulate_nmap_scan(ip, seed)
vulnerabilities = analyze_vulnerabilities(open_ports)
conteo = {s: sum(1 for v in vulnerabilities if v["severity"] == s) for s in SEVERITY_ORDER}
return {
"target_ip": ip,
"scanned_at": (scanned_at or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"),
# Every common port with its verdict: a closed port is a result too,
# and without them the report only shows half of what was checked.
"ports": [
{"port": p, "service": s, "open": p in open_ports}
for p, s in sorted(COMMON_PORTS.items())
],
"open_ports": open_ports,
"vulnerabilities": vulnerabilities,
"counts": conteo,
"checked": len(COMMON_PORTS),
}
if __name__ == "__main__":
import os
import sys
datos = scan_payload()
open_ports = datos["open_ports"]
vulnerabilities = datos["vulnerabilities"]
report_filename = "vulnerability_report.md"
with open(report_filename, "w", encoding="utf-8") as f:
f.write(generate_report(TARGET_IP, open_ports, vulnerabilities))
# Save the raw data for transparency
with open("raw_scan_data.json", "w", encoding="utf-8") as f:
json.dump(
{"target_ip": TARGET_IP, "open_ports": open_ports, "vulnerabilities": vulnerabilities},
f, indent=4, ensure_ascii=False,
)
print(f"Vulnerability scan complete. Report saved to {report_filename}")
# Optional argument: a directory to drop the dashboard payload into.
if len(sys.argv) > 1:
os.makedirs(sys.argv[1], exist_ok=True)
with open(os.path.join(sys.argv[1], "escaneo.json"), "w", encoding="utf-8") as f:
json.dump(datos, f, indent=1, ensure_ascii=False)
print(" escaneo.json")