vamp-easm 1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vamp_easm.py ADDED
@@ -0,0 +1,563 @@
1
+ #!/usr/bin/env python3
2
+ # © VampSecure Studios — VampSecure Labs Security Research Division
3
+ """
4
+ vamp_easm.py — Motor de Attack Surface Management Continuo
5
+ ===========================================================
6
+ VampSecure Labs · VampSecure Studios
7
+ Para Uso Exclusivo en Pruebas de Penetración Autorizadas — v1.0
8
+
9
+ DESCRIPCIÓN GENERAL
10
+ -------------------
11
+ Motor de EASM (External Attack Surface Management) continuo que escanea
12
+ la superficie de ataque de un dominio objetivo, registra el historial en
13
+ SQLite y genera alertas cuando detecta cambios respecto al escaneo anterior.
14
+
15
+ El diferencial frente a escaneos puntuales es la **continuidad**: cada
16
+ ejecución mide deltas (diffs) contra el estado anterior, permitiendo
17
+ detectar nuevos activos, cambios de IP, puertos nuevos, certificados
18
+ expirados o cambiados.
19
+
20
+ ESCANEO EN TRES CAPAS
21
+ ----------------------
22
+ 1. DNS/Subdominios — crt.sh + HackerTarget (urllib stdlib)
23
+ 2. Puertos/Servicios — asyncio TCP connect (o nmap con --nmap)
24
+ 3. Certificados TLS — inspección con ssl stdlib (SANs, expiración, fingerprint)
25
+
26
+ HISTORIAL SQLite
27
+ ----------------
28
+ vamp_easm.db (directorio de trabajo) — tablas: scans, assets, certs
29
+
30
+ DIFFS DETECTADOS
31
+ ----------------
32
+ NUEVO_SUBDOMINIO HIGH CERT_EXPIRADO HIGH/CRITICAL
33
+ NUEVO_PUERTO MEDIUM CERT_CAMBIADO CRITICAL
34
+ SERVICIO_DESAPARECIDO LOW IP_CAMBIADA MEDIUM
35
+
36
+ USO
37
+ ---
38
+ python vamp_easm.py scan --target ejemplo.com [--ports top100|22,80,443]
39
+ python vamp_easm.py history --target ejemplo.com [--limit 10]
40
+ python vamp_easm.py assets --target ejemplo.com
41
+ python vamp_easm.py export --target ejemplo.com [--json] [--html]
42
+
43
+ EXIT CODES
44
+ ----------
45
+ 0 — Sin diffs o diffs de severidad MEDIUM/LOW
46
+ 1 — Hay diffs de severidad HIGH
47
+ 2 — Hay diffs de severidad CRITICAL
48
+
49
+ AUTORÍA
50
+ -------
51
+ © VampSecure Studios — VampSecure Labs Security Research Division
52
+ Todos los derechos reservados. Uso exclusivo en entornos autorizados.
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import argparse
58
+ import json
59
+ import sys
60
+ from datetime import datetime, timezone
61
+ from typing import List, Optional
62
+
63
+ from rich.console import Console
64
+ from rich.table import Table
65
+ from rich.panel import Panel
66
+ from rich import box
67
+
68
+ from easm import scanner, storage, differ, alerter
69
+ from vampsec_report import (
70
+ Finding, ReportMeta, VampSecReport,
71
+ add_report_args, meta_from_args,
72
+ )
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Constantes
76
+ # ---------------------------------------------------------------------------
77
+
78
+ VERSION = "1.1"
79
+ TOOL = "vamp-easm"
80
+ BRAND = "VampSecure Labs — EASM Continuo"
81
+
82
+ BANNER = (
83
+ "\n"
84
+ "__ ___ __ __ ___ ___ ___ ___ _ _ ___ ___ _ _ ___ ___ \n"
85
+ "\\ \\ / /_\\ | \\/ | _ \\/ __| __/ __| | | | _ \\ __| | /_\\ | _ ) __|\n"
86
+ " \\ V / _ \\| |\\/| | _/\\__ \\ _| (__| |_| | / _|| |__ / _ \\| _ \\__ \\\n"
87
+ " \\_/_/ \\_\\_| |_|_| |___/___\\___|\\___/|_|_\\___|____/_/ \\_\\___/___/\n"
88
+ ' by Antonio Hernandez "Belky" — VampSecure Studios\n'
89
+ " vamp-easm v1.1 · External Attack Surface Management\n"
90
+ " ────────────────────────────────────────────────────────────────────────\n"
91
+ " USO EXCLUSIVO EN AUDITORÍAS AUTORIZADAS · El uso no autorizado es ilegal\n"
92
+ )
93
+
94
+ # Mapa de colores de severidad para Rich
95
+ _COLORES = {
96
+ "CRITICAL": "bold red",
97
+ "HIGH": "bold orange3",
98
+ "MEDIUM": "yellow",
99
+ "LOW": "blue",
100
+ "INFO": "dim",
101
+ }
102
+
103
+ console = Console()
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # Helpers de presentación
108
+ # ---------------------------------------------------------------------------
109
+
110
+ def _badge_sev(sev: str) -> str:
111
+ """Devuelve el texto de severidad con estilo Rich."""
112
+ color = _COLORES.get(sev, "white")
113
+ return f"[{color}]{sev}[/{color}]"
114
+
115
+
116
+ def _ts_ahora() -> str:
117
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
118
+
119
+
120
+ def _tabla_diffs(diffs: List[differ.Diff]) -> Table:
121
+ """Construye una tabla Rich con los diffs del escaneo."""
122
+ tabla = Table(
123
+ title="[bold]Diffs detectados[/bold]",
124
+ box=box.ROUNDED,
125
+ show_header=True,
126
+ header_style="bold dim",
127
+ )
128
+ tabla.add_column("Finding", style="bold dim", width=12)
129
+ tabla.add_column("Categoría", style="cyan", width=24)
130
+ tabla.add_column("Severidad", width=12)
131
+ tabla.add_column("Activo", style="white", no_wrap=False)
132
+
133
+ for d in diffs:
134
+ fid = d.finding.id if d.finding else "—"
135
+ tabla.add_row(
136
+ fid,
137
+ d.categoria,
138
+ _badge_sev(d.severidad),
139
+ d.activo,
140
+ )
141
+ return tabla
142
+
143
+
144
+ def _tabla_assets(rows) -> Table:
145
+ """Construye una tabla Rich con los activos de un target."""
146
+ tabla = Table(
147
+ title="[bold]Activos conocidos[/bold]",
148
+ box=box.SIMPLE_HEAVY,
149
+ show_header=True,
150
+ header_style="bold dim",
151
+ )
152
+ tabla.add_column("Subdominio", style="cyan", no_wrap=False)
153
+ tabla.add_column("IP", style="white", width=18)
154
+ tabla.add_column("Puerto", style="yellow", width=8)
155
+ tabla.add_column("Protocolo", style="dim", width=8)
156
+ tabla.add_column("Servicio", style="green", width=14)
157
+ tabla.add_column("Primera vista", style="dim", width=20)
158
+ tabla.add_column("Última vista", style="dim", width=20)
159
+
160
+ for r in rows:
161
+ tabla.add_row(
162
+ r["subdominio"],
163
+ r["ip"] or "—",
164
+ str(r["puerto"]) if r["puerto"] else "—",
165
+ r["protocolo"] or "tcp",
166
+ r["servicio"] or "—",
167
+ r["primera_vista"][:16] if r["primera_vista"] else "—",
168
+ r["ultima_vista"][:16] if r["ultima_vista"] else "—",
169
+ )
170
+ return tabla
171
+
172
+
173
+ def _tabla_historial(rows) -> Table:
174
+ """Construye una tabla Rich con el historial de escaneos."""
175
+ tabla = Table(
176
+ title="[bold]Historial de escaneos[/bold]",
177
+ box=box.SIMPLE_HEAVY,
178
+ show_header=True,
179
+ header_style="bold dim",
180
+ )
181
+ tabla.add_column("Inicio", style="cyan", width=20)
182
+ tabla.add_column("Fin", style="dim", width=20)
183
+ tabla.add_column("Activos", style="green", width=10)
184
+ tabla.add_column("Diffs", style="yellow",width=8)
185
+ tabla.add_column("Scan ID", style="dim", width=38)
186
+
187
+ for r in rows:
188
+ tabla.add_row(
189
+ r["ts_start"][:16] if r["ts_start"] else "—",
190
+ r["ts_end"][:16] if r["ts_end"] else "en curso",
191
+ str(r["assets_found"]),
192
+ str(r["diffs_found"]),
193
+ r["id"],
194
+ )
195
+ return tabla
196
+
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # Comando: scan
200
+ # ---------------------------------------------------------------------------
201
+
202
+ def cmd_scan(args: argparse.Namespace) -> int:
203
+ """
204
+ Ejecuta un escaneo completo del target, guarda resultados en SQLite,
205
+ calcula diffs respecto al escaneo anterior y muestra los hallazgos.
206
+
207
+ Returns
208
+ -------
209
+ int : Código de salida (0 limpio, 1 HIGH, 2 CRITICAL)
210
+ """
211
+ target = args.target
212
+
213
+ # Inicializar la base de datos
214
+ storage.inicializar_db()
215
+
216
+ console.print(Panel(
217
+ f"[bold cyan]vamp-easm v{VERSION}[/bold cyan] · {BRAND}\n"
218
+ f"Target: [bold]{target}[/bold] · {_ts_ahora()}",
219
+ border_style="dim",
220
+ ))
221
+
222
+ # ── Registrar inicio del escaneo ──────────────────────────────────────
223
+ scan_id = storage.nuevo_scan(target)
224
+
225
+ # ── Determinar lista de puertos ───────────────────────────────────────
226
+ if args.ports and args.ports != "top100":
227
+ try:
228
+ puertos = [int(p.strip()) for p in args.ports.split(",") if p.strip()]
229
+ except ValueError:
230
+ console.print("[red]Error:[/red] --ports debe ser 'top100' o una lista de puertos (ej: 22,80,443)")
231
+ return 1
232
+ else:
233
+ puertos = scanner.TOP_100_PORTS
234
+
235
+ # ── Capa 1: subdominios ───────────────────────────────────────────────
236
+ console.print("\n[cyan]►[/cyan] [bold]Fase 1:[/bold] Enumeración de subdominios (crt.sh + HackerTarget)…")
237
+ resultado = scanner.ejecutar_escaneo(
238
+ target=target,
239
+ puertos=puertos,
240
+ usar_nmap=getattr(args, "nmap", False),
241
+ )
242
+
243
+ # Guardar subdominios en SQLite (sin puerto)
244
+ for sub in resultado.subdominios:
245
+ ip = sub.ips[0] if sub.ips else None
246
+ storage.upsert_asset(
247
+ target=target,
248
+ subdominio=sub.nombre,
249
+ scan_id=scan_id,
250
+ ip=ip,
251
+ )
252
+ console.print(f" {len(resultado.subdominios)} subdominios encontrados.")
253
+
254
+ # ── Capa 2: puertos ───────────────────────────────────────────────────
255
+ console.print("[cyan]►[/cyan] [bold]Fase 2:[/bold] Escaneo de puertos…")
256
+ for pa in resultado.puertos:
257
+ # Resolver IP del host si es posible
258
+ ip_pa = None
259
+ for sub in resultado.subdominios:
260
+ if sub.nombre == pa.host:
261
+ ip_pa = sub.ips[0] if sub.ips else None
262
+ break
263
+ storage.upsert_asset(
264
+ target=target,
265
+ subdominio=pa.host,
266
+ scan_id=scan_id,
267
+ ip=ip_pa,
268
+ puerto=pa.puerto,
269
+ protocolo=pa.protocolo,
270
+ servicio=pa.servicio,
271
+ )
272
+ console.print(f" {len(resultado.puertos)} puertos abiertos encontrados.")
273
+
274
+ # ── Capa 3: certificados ──────────────────────────────────────────────
275
+ console.print("[cyan]►[/cyan] [bold]Fase 3:[/bold] Inspección de certificados TLS…")
276
+ for cert in resultado.certs:
277
+ if cert.error:
278
+ continue
279
+ storage.insertar_cert(
280
+ target=target,
281
+ host=cert.host,
282
+ scan_id=scan_id,
283
+ issued_to=cert.issued_to,
284
+ issuer=cert.issuer,
285
+ not_after=cert.not_after,
286
+ sans=",".join(cert.sans) if cert.sans else None,
287
+ fingerprint=cert.fingerprint,
288
+ autofirmado=cert.autofirmado,
289
+ )
290
+ certs_ok = [c for c in resultado.certs if not c.error]
291
+ console.print(f" {len(certs_ok)} certificados inspeccionados.")
292
+
293
+ # ── Motor de diffs ────────────────────────────────────────────────────
294
+ console.print("\n[cyan]►[/cyan] [bold]Calculando diffs respecto al escaneo anterior…[/bold]")
295
+ scan_anterior = storage.ultimo_scan(target)
296
+ # El último scan completado puede ser el actual si ya acabó (no debería),
297
+ # así que buscamos el penúltimo para comparar
298
+ historial = storage.historial_scans(target, limit=5)
299
+ scan_id_prev: Optional[str] = None
300
+ for row in historial:
301
+ if row["id"] != scan_id:
302
+ scan_id_prev = row["id"]
303
+ break
304
+
305
+ motor = differ.MotorDiff(
306
+ target=target,
307
+ scan_id=scan_id,
308
+ scan_id_prev=scan_id_prev,
309
+ storage=storage,
310
+ )
311
+ diffs = motor.calcular()
312
+
313
+ # Contar totales de activos
314
+ assets_total = len(resultado.subdominios) + len(resultado.puertos)
315
+
316
+ # Cerrar escaneo en SQLite
317
+ storage.cerrar_scan(scan_id, assets_found=assets_total, diffs_found=len(diffs))
318
+
319
+ # ── Mostrar resultados ────────────────────────────────────────────────
320
+ if diffs:
321
+ console.print(_tabla_diffs(diffs))
322
+ else:
323
+ if scan_id_prev:
324
+ console.print("\n[green]✓ Sin diffs respecto al escaneo anterior. Superficie estable.[/green]")
325
+ else:
326
+ console.print("\n[blue]ℹ Primer escaneo del target — baseline registrado. No hay escaneo anterior para comparar.[/blue]")
327
+
328
+ # ── Alertas ───────────────────────────────────────────────────────────
329
+ webhook_url = alerter.resolver_webhook_url(getattr(args, "alert_webhook", None))
330
+ alerter.gestionar_alertas(webhook_url, target, diffs, scan_id)
331
+
332
+ # ── Exportar informes si se pidió ─────────────────────────────────────
333
+ findings = [d.finding for d in diffs if d.finding is not None]
334
+ if findings:
335
+ meta = meta_from_args(args, tool=TOOL, version=VERSION)
336
+ meta.scope = target
337
+ informe = VampSecReport(meta, findings)
338
+
339
+ if getattr(args, "json", False):
340
+ fname = f"easm_{target}_{scan_id[:8]}.json"
341
+ informe.to_json(fname)
342
+ console.print(f"[green]→ JSON:[/green] {fname}")
343
+
344
+ if getattr(args, "html", False):
345
+ fname = f"easm_{target}_{scan_id[:8]}.html"
346
+ informe.to_html_client(fname)
347
+ console.print(f"[green]→ HTML:[/green] {fname}")
348
+
349
+ # ── Código de salida ──────────────────────────────────────────────────
350
+ sevs = {d.severidad for d in diffs}
351
+ if "CRITICAL" in sevs:
352
+ return 2
353
+ if "HIGH" in sevs:
354
+ return 1
355
+ return 0
356
+
357
+
358
+ # ---------------------------------------------------------------------------
359
+ # Comando: history
360
+ # ---------------------------------------------------------------------------
361
+
362
+ def cmd_history(args: argparse.Namespace) -> int:
363
+ """Muestra el historial de escaneos para el target especificado."""
364
+ storage.inicializar_db()
365
+ limit = getattr(args, "limit", 10)
366
+ rows = storage.historial_scans(args.target, limit=limit)
367
+
368
+ if not rows:
369
+ console.print(f"[dim]Sin historial para el target:[/dim] {args.target}")
370
+ return 0
371
+
372
+ console.print(_tabla_historial(rows))
373
+ return 0
374
+
375
+
376
+ # ---------------------------------------------------------------------------
377
+ # Comando: assets
378
+ # ---------------------------------------------------------------------------
379
+
380
+ def cmd_assets(args: argparse.Namespace) -> int:
381
+ """Muestra todos los activos conocidos para el target especificado."""
382
+ storage.inicializar_db()
383
+ rows = storage.todos_los_assets(args.target)
384
+
385
+ if not rows:
386
+ console.print(f"[dim]Sin activos registrados para el target:[/dim] {args.target}")
387
+ return 0
388
+
389
+ console.print(_tabla_assets(rows))
390
+ return 0
391
+
392
+
393
+ # ---------------------------------------------------------------------------
394
+ # Comando: export
395
+ # ---------------------------------------------------------------------------
396
+
397
+ def cmd_export(args: argparse.Namespace) -> int:
398
+ """
399
+ Exporta un snapshot del último escaneo en JSON y/o HTML.
400
+ Útil para integración con vamp-penreport.
401
+ """
402
+ storage.inicializar_db()
403
+
404
+ # Buscar último escaneo
405
+ rows = storage.historial_scans(args.target, limit=1)
406
+ if not rows:
407
+ console.print(f"[red]Error:[/red] No hay escaneos registrados para {args.target}")
408
+ return 1
409
+
410
+ scan_id = rows[0]["id"]
411
+ assets = storage.assets_del_scan(args.target, scan_id)
412
+ certs = storage.certs_del_scan(args.target, scan_id)
413
+
414
+ # Construir findings de info a partir de los activos
415
+ findings: List[Finding] = []
416
+ n = 1
417
+ for a in assets:
418
+ if a["puerto"]:
419
+ findings.append(Finding(
420
+ id=f"EASM-{n:03d}",
421
+ title=f"Activo expuesto — {a['subdominio']}:{a['puerto']}",
422
+ severity="INFO",
423
+ description=f"Puerto {a['puerto']}/{a['protocolo']} abierto en {a['subdominio']}.",
424
+ evidence=f"Host: {a['subdominio']}\nIP: {a['ip'] or '—'}\nPuerto: {a['puerto']}/{a['protocolo']}\nServicio: {a['servicio'] or '—'}",
425
+ affected=f"{a['subdominio']}:{a['puerto']}",
426
+ remediation="Verificar que el servicio está correctamente asegurado y es intencionado.",
427
+ tags=["easm", "asset", "inventory"],
428
+ ))
429
+ n += 1
430
+
431
+ meta = meta_from_args(args, tool=TOOL, version=VERSION)
432
+ meta.scope = args.target
433
+ informe = VampSecReport(meta, findings)
434
+
435
+ if not getattr(args, "json", False) and not getattr(args, "html", False):
436
+ # Por defecto exportar ambos
437
+ args.json = True
438
+ args.html = True
439
+
440
+ exportados = []
441
+ if getattr(args, "json", False):
442
+ fname = f"easm_export_{args.target}_{scan_id[:8]}.json"
443
+ informe.to_json(fname)
444
+ exportados.append(fname)
445
+
446
+ if getattr(args, "html", False):
447
+ fname = f"easm_export_{args.target}_{scan_id[:8]}.html"
448
+ informe.to_html_client(fname)
449
+ exportados.append(fname)
450
+
451
+ for f in exportados:
452
+ console.print(f"[green]→ Exportado:[/green] {f}")
453
+
454
+ return 0
455
+
456
+
457
+ # ---------------------------------------------------------------------------
458
+ # CLI — definición de argumentos
459
+ # ---------------------------------------------------------------------------
460
+
461
+ def construir_parser() -> argparse.ArgumentParser:
462
+ """Construye el parser argparse con todos los subcomandos."""
463
+ parser = argparse.ArgumentParser(
464
+ prog="vamp-easm",
465
+ description=(
466
+ "vamp-easm — Motor de Attack Surface Management Continuo\n"
467
+ "VampSecure Labs · VampSecure Studios"
468
+ ),
469
+ formatter_class=argparse.RawDescriptionHelpFormatter,
470
+ epilog=(
471
+ "Ejemplos:\n"
472
+ " python vamp_easm.py scan --target ejemplo.com\n"
473
+ " python vamp_easm.py scan --target ejemplo.com --ports 22,80,443 --html\n"
474
+ " python vamp_easm.py history --target ejemplo.com --limit 5\n"
475
+ " python vamp_easm.py assets --target ejemplo.com\n"
476
+ " python vamp_easm.py export --target ejemplo.com --json\n"
477
+ ),
478
+ )
479
+ parser.add_argument(
480
+ "--version", action="version",
481
+ version=f"vamp-easm {VERSION} · VampSecure Labs",
482
+ )
483
+
484
+ subparsers = parser.add_subparsers(dest="comando", help="Subcomando a ejecutar")
485
+
486
+ # ── scan ─────────────────────────────────────────────────────────────
487
+ p_scan = subparsers.add_parser("scan", help="Escanear un target y calcular diffs")
488
+ p_scan.add_argument(
489
+ "--target", required=True, metavar="DOMINIO",
490
+ help="Dominio objetivo (ej: ejemplo.com)",
491
+ )
492
+ p_scan.add_argument(
493
+ "--ports", default="top100", metavar="PORTS",
494
+ help="Puertos a escanear: 'top100' (por defecto) o lista CSV (ej: 22,80,443)",
495
+ )
496
+ p_scan.add_argument(
497
+ "--nmap", action="store_true",
498
+ help="Usar nmap como backend de escaneo de puertos (requiere nmap en PATH)",
499
+ )
500
+ p_scan.add_argument(
501
+ "--json", action="store_true",
502
+ help="Exportar hallazgos en formato JSON (informe VSL estándar)",
503
+ )
504
+ p_scan.add_argument(
505
+ "--html", action="store_true",
506
+ help="Exportar hallazgos en formato HTML profesional (informe VSL)",
507
+ )
508
+ p_scan.add_argument(
509
+ "--alert-webhook", metavar="URL", dest="alert_webhook",
510
+ help="URL del webhook para alertas CRITICAL/HIGH (alternativa: env EASM_ALERT_WEBHOOK)",
511
+ )
512
+ add_report_args(p_scan)
513
+
514
+ # ── history ───────────────────────────────────────────────────────────
515
+ p_hist = subparsers.add_parser("history", help="Ver historial de escaneos de un target")
516
+ p_hist.add_argument("--target", required=True, metavar="DOMINIO", help="Dominio objetivo")
517
+ p_hist.add_argument("--limit", type=int, default=10, metavar="N", help="Número de entradas (por defecto: 10)")
518
+
519
+ # ── assets ────────────────────────────────────────────────────────────
520
+ p_assets = subparsers.add_parser("assets", help="Ver activos conocidos de un target")
521
+ p_assets.add_argument("--target", required=True, metavar="DOMINIO", help="Dominio objetivo")
522
+
523
+ # ── export ────────────────────────────────────────────────────────────
524
+ p_export = subparsers.add_parser("export", help="Exportar snapshot del último escaneo")
525
+ p_export.add_argument("--target", required=True, metavar="DOMINIO", help="Dominio objetivo")
526
+ p_export.add_argument("--json", action="store_true", help="Exportar como JSON")
527
+ p_export.add_argument("--html", action="store_true", help="Exportar como HTML")
528
+ add_report_args(p_export)
529
+
530
+ return parser
531
+
532
+
533
+ # ---------------------------------------------------------------------------
534
+ # Punto de entrada
535
+ # ---------------------------------------------------------------------------
536
+
537
+ def main() -> None:
538
+ """Punto de entrada principal. Parsea la CLI y despacha al subcomando."""
539
+ parser = construir_parser()
540
+ args = parser.parse_args()
541
+
542
+ if not args.comando:
543
+ parser.print_help()
544
+ sys.exit(0)
545
+
546
+ _dispatch = {
547
+ "scan": cmd_scan,
548
+ "history": cmd_history,
549
+ "assets": cmd_assets,
550
+ "export": cmd_export,
551
+ }
552
+
553
+ handler = _dispatch.get(args.comando)
554
+ if handler is None:
555
+ parser.print_help()
556
+ sys.exit(1)
557
+
558
+ exit_code = handler(args)
559
+ sys.exit(exit_code)
560
+
561
+
562
+ if __name__ == "__main__":
563
+ main()