vamp-secrets-scanner 2.2__tar.gz → 2.4__tar.gz

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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vamp-secrets-scanner
3
- Version: 2.2
4
- Summary: Static secrets and credentials scanner with git history analysis, Shannon entropy detection and SARIF 2.1.0 output
3
+ Version: 2.4
4
+ Summary: Static secrets and credentials scanner with git history analysis, Shannon entropy detection, SARIF 2.1.0 output, Docker runtime scanning and Kubernetes Secrets scanning
5
5
  Author-email: VampSecure Studios <contact@vampsecurestudios.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/Vampsecure-Labs/vamp-secrets-scanner
@@ -26,6 +26,7 @@ License-File: LICENSE
26
26
  Requires-Dist: rich>=13.7.0
27
27
  Dynamic: license-file
28
28
 
29
+ <!-- © VampSecure Studios — VampSecure Labs Security Research Division -->
29
30
  <h1 align="center">vamp-secrets-scanner</h1>
30
31
  <p align="center">
31
32
  <strong>Static secrets and credential scanner with Git history analysis and SARIF export</strong><br>
@@ -77,6 +78,13 @@ Standard library only beyond `rich`: `re`, `os`, `math`, `pathlib`, `hashlib`, `
77
78
 
78
79
  ## Installation
79
80
 
81
+
82
+ ```bash
83
+ pip install vamp-secrets-scanner
84
+ # o con Homebrew:
85
+ brew install vampsecure-labs/labs/vamp-secrets-scanner
86
+ ```
87
+
80
88
  ```bash
81
89
  git clone https://github.com/belky-me/vamp-secrets-scanner.git
82
90
  cd vamp-secrets-scanner
@@ -177,3 +185,8 @@ This tool is part of the **VampSecure Labs Security Toolkit** — a collection o
177
185
 
178
186
  © VampSecure Studios — VampSecure Labs Security Research Division
179
187
  For authorized security testing only.
188
+
189
+ ---
190
+
191
+ ## Versión
192
+ v2.2 — VampSecure Labs Security Research Division
@@ -1,3 +1,4 @@
1
+ <!-- © VampSecure Studios — VampSecure Labs Security Research Division -->
1
2
  <h1 align="center">vamp-secrets-scanner</h1>
2
3
  <p align="center">
3
4
  <strong>Static secrets and credential scanner with Git history analysis and SARIF export</strong><br>
@@ -49,6 +50,13 @@ Standard library only beyond `rich`: `re`, `os`, `math`, `pathlib`, `hashlib`, `
49
50
 
50
51
  ## Installation
51
52
 
53
+
54
+ ```bash
55
+ pip install vamp-secrets-scanner
56
+ # o con Homebrew:
57
+ brew install vampsecure-labs/labs/vamp-secrets-scanner
58
+ ```
59
+
52
60
  ```bash
53
61
  git clone https://github.com/belky-me/vamp-secrets-scanner.git
54
62
  cd vamp-secrets-scanner
@@ -149,3 +157,8 @@ This tool is part of the **VampSecure Labs Security Toolkit** — a collection o
149
157
 
150
158
  © VampSecure Studios — VampSecure Labs Security Research Division
151
159
  For authorized security testing only.
160
+
161
+ ---
162
+
163
+ ## Versión
164
+ v2.2 — VampSecure Labs Security Research Division
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vamp-secrets-scanner"
7
- version = "2.2"
8
- description = "Static secrets and credentials scanner with git history analysis, Shannon entropy detection and SARIF 2.1.0 output"
7
+ version = "2.4"
8
+ description = "Static secrets and credentials scanner with git history analysis, Shannon entropy detection, SARIF 2.1.0 output, Docker runtime scanning and Kubernetes Secrets scanning"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.10"
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vamp-secrets-scanner
3
- Version: 2.2
4
- Summary: Static secrets and credentials scanner with git history analysis, Shannon entropy detection and SARIF 2.1.0 output
3
+ Version: 2.4
4
+ Summary: Static secrets and credentials scanner with git history analysis, Shannon entropy detection, SARIF 2.1.0 output, Docker runtime scanning and Kubernetes Secrets scanning
5
5
  Author-email: VampSecure Studios <contact@vampsecurestudios.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/Vampsecure-Labs/vamp-secrets-scanner
@@ -26,6 +26,7 @@ License-File: LICENSE
26
26
  Requires-Dist: rich>=13.7.0
27
27
  Dynamic: license-file
28
28
 
29
+ <!-- © VampSecure Studios — VampSecure Labs Security Research Division -->
29
30
  <h1 align="center">vamp-secrets-scanner</h1>
30
31
  <p align="center">
31
32
  <strong>Static secrets and credential scanner with Git history analysis and SARIF export</strong><br>
@@ -77,6 +78,13 @@ Standard library only beyond `rich`: `re`, `os`, `math`, `pathlib`, `hashlib`, `
77
78
 
78
79
  ## Installation
79
80
 
81
+
82
+ ```bash
83
+ pip install vamp-secrets-scanner
84
+ # o con Homebrew:
85
+ brew install vampsecure-labs/labs/vamp-secrets-scanner
86
+ ```
87
+
80
88
  ```bash
81
89
  git clone https://github.com/belky-me/vamp-secrets-scanner.git
82
90
  cd vamp-secrets-scanner
@@ -177,3 +185,8 @@ This tool is part of the **VampSecure Labs Security Toolkit** — a collection o
177
185
 
178
186
  © VampSecure Studios — VampSecure Labs Security Research Division
179
187
  For authorized security testing only.
188
+
189
+ ---
190
+
191
+ ## Versión
192
+ v2.2 — VampSecure Labs Security Research Division
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env python3
2
+ # © VampSecure Studios — VampSecure Labs Security Research Division
2
3
  """
3
4
  vamp_secrets_scanner.py — Escáner Estático de Secretos y Credenciales
4
5
  ======================================================================
@@ -85,6 +86,7 @@ AUTORÍA
85
86
  from __future__ import annotations
86
87
 
87
88
  import argparse
89
+ import base64
88
90
  import hashlib
89
91
  import json
90
92
  import math
@@ -109,7 +111,7 @@ from rich.text import Text
109
111
  # Constantes
110
112
  # ─────────────────────────────────────────────────────────────────────────────
111
113
 
112
- VERSION = "2.2"
114
+ VERSION = "2.4"
113
115
  TOOL_NAME = "vamp-secrets-scanner"
114
116
 
115
117
  BANNER = r"""
@@ -118,7 +120,7 @@ __ ___ __ __ ___ ___ ___ ___ _ _ ___ ___ _ _ ___ ___
118
120
  \ V / _ \| |\/| | _/\__ \ _| (__| |_| | / _|| |__ / _ \| _ \__ \
119
121
  \_/_/ \_\_| |_|_| |___/___\___|\___/|_|_\___|____/_/ \_\___/___/
120
122
  by Antonio Hernandez "Belky" — VampSecure Studios
121
- vamp-secrets-scanner v2.2 · Static Secrets & Git History Scanner
123
+ vamp-secrets-scanner v2.4 · Static Secrets & Git History Scanner
122
124
  ────────────────────────────────────────────────────────────────────────
123
125
  USO EXCLUSIVO EN AUDITORÍAS AUTORIZADAS · El uso no autorizado es ilegal
124
126
  """
@@ -1491,6 +1493,335 @@ function toggle(row){{
1491
1493
  console.print(f"[dim] HTML → {path}[/]")
1492
1494
 
1493
1495
 
1496
+ # ─────────────────────────────────────────────────────────────────────────────
1497
+ # Escaneo de contenedores Docker en runtime (v2.3)
1498
+ # ─────────────────────────────────────────────────────────────────────────────
1499
+
1500
+ def _docker_disponible() -> bool:
1501
+ """
1502
+ Comprueba si el binario docker está disponible en el PATH.
1503
+ Retorna True si está disponible, False en caso contrario.
1504
+ """
1505
+ import shutil as _shutil
1506
+ return _shutil.which("docker") is not None
1507
+
1508
+
1509
+ def _obtener_envs_contenedor(container_id: str) -> Optional[List[str]]:
1510
+ """
1511
+ Obtiene las variables de entorno de un contenedor Docker.
1512
+
1513
+ Intenta primero con 'docker inspect' para leer la configuración estática;
1514
+ si no devuelve variables, recurre a 'docker exec env' como alternativa.
1515
+
1516
+ Parámetros
1517
+ ----------
1518
+ container_id : nombre o ID del contenedor en ejecución
1519
+
1520
+ Retorna lista de strings con formato 'CLAVE=valor', o None si falla.
1521
+ """
1522
+ envs: List[str] = []
1523
+
1524
+ # Método 1: docker inspect (más rápido, no requiere shell en el contenedor)
1525
+ try:
1526
+ result = subprocess.run(
1527
+ ["docker", "inspect", "--format", "{{range .Config.Env}}{{println .}}{{end}}", container_id],
1528
+ capture_output=True,
1529
+ text=True,
1530
+ timeout=10,
1531
+ )
1532
+ if result.returncode == 0 and result.stdout.strip():
1533
+ envs = [l for l in result.stdout.splitlines() if l.strip() and "=" in l]
1534
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
1535
+ pass
1536
+
1537
+ # Método 2: docker exec env (alternativa si inspect no devolvió variables)
1538
+ if not envs:
1539
+ try:
1540
+ result = subprocess.run(
1541
+ ["docker", "exec", container_id, "env"],
1542
+ capture_output=True,
1543
+ text=True,
1544
+ timeout=10,
1545
+ )
1546
+ if result.returncode == 0 and result.stdout.strip():
1547
+ envs = [l for l in result.stdout.splitlines() if l.strip() and "=" in l]
1548
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
1549
+ pass
1550
+
1551
+ return envs if envs else None
1552
+
1553
+
1554
+ def scan_docker_container(container_id: str) -> List[Finding]:
1555
+ """
1556
+ Escanea las variables de entorno de un contenedor Docker en ejecución.
1557
+
1558
+ Aplica los mismos 77 patrones de SECRET_PATTERNS del scanner estático
1559
+ a cada variable de entorno del contenedor. Los hallazgos se marcan
1560
+ como categoría 'Docker · Runtime Env' con severidad CRITICAL.
1561
+
1562
+ Parámetros
1563
+ ----------
1564
+ container_id : nombre o ID del contenedor Docker a escanear
1565
+
1566
+ Retorna lista de Finding con los secretos encontrados en las envs del contenedor.
1567
+ """
1568
+ if not _docker_disponible():
1569
+ console.print("[yellow] Aviso: 'docker' no encontrado en PATH — omitiendo escaneo de contenedor[/]")
1570
+ return []
1571
+
1572
+ console.print(f"[cyan] Escaneando contenedor Docker: {container_id}[/]")
1573
+
1574
+ envs = _obtener_envs_contenedor(container_id)
1575
+ if envs is None:
1576
+ console.print(f"[yellow] Aviso: no se pudieron obtener variables de entorno del contenedor '{container_id}'[/]")
1577
+ return []
1578
+
1579
+ hallazgos: List[Finding] = []
1580
+
1581
+ for linea in envs:
1582
+ if not linea.strip() or "=" not in linea:
1583
+ continue
1584
+
1585
+ # Escanear la línea de variable de entorno con los patrones habituales
1586
+ for pat in SECRET_PATTERNS:
1587
+ match = pat["compiled"].search(linea)
1588
+ if not match:
1589
+ continue
1590
+
1591
+ valor_raw = match.group(0)
1592
+ extracto = _censor(valor_raw)
1593
+ fp = _fingerprint(valor_raw, "DOCKER_ENV_SECRET")
1594
+
1595
+ # Nombre de la variable de entorno (parte antes del '=')
1596
+ nombre_var = linea.split("=", 1)[0]
1597
+
1598
+ hallazgos.append(Finding(
1599
+ file = f"docker://{container_id}",
1600
+ line_no = 0,
1601
+ pattern = f"DOCKER_ENV_SECRET: {pat['name']}",
1602
+ category = "Docker · Runtime Env",
1603
+ severity = Severity.CRITICAL,
1604
+ preview = extracto,
1605
+ context = [
1606
+ f" Contenedor: {container_id}",
1607
+ f" Variable: {nombre_var}",
1608
+ f" Patrón: {pat['name']}",
1609
+ f" Extracto: {extracto}",
1610
+ ],
1611
+ fingerprint = fp,
1612
+ ))
1613
+
1614
+ if hallazgos:
1615
+ console.print(
1616
+ f" [bold red]⚠ {len(hallazgos)} secreto(s) encontrado(s) en "
1617
+ f"variables de entorno de '{container_id}'[/]"
1618
+ )
1619
+ else:
1620
+ console.print(f" [green]✔ Sin secretos detectados en las envs de '{container_id}'[/]")
1621
+
1622
+ return hallazgos
1623
+
1624
+
1625
+ def scan_all_docker_containers() -> List[Finding]:
1626
+ """
1627
+ Escanea los contenedores Docker actualmente en ejecución.
1628
+
1629
+ Ejecuta 'docker ps -q' para obtener los IDs de todos los contenedores
1630
+ activos y aplica scan_docker_container() a cada uno.
1631
+
1632
+ Retorna la lista combinada de hallazgos de todos los contenedores.
1633
+ """
1634
+ if not _docker_disponible():
1635
+ console.print("[yellow] Aviso: 'docker' no encontrado en PATH — omitiendo escaneo de contenedores[/]")
1636
+ return []
1637
+
1638
+ # Obtener lista de IDs de contenedores en ejecución
1639
+ try:
1640
+ result = subprocess.run(
1641
+ ["docker", "ps", "-q"],
1642
+ capture_output=True,
1643
+ text=True,
1644
+ timeout=10,
1645
+ )
1646
+ ids = [i.strip() for i in result.stdout.splitlines() if i.strip()]
1647
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as exc:
1648
+ console.print(f"[red] Error ejecutando 'docker ps': {exc}[/]")
1649
+ return []
1650
+
1651
+ if not ids:
1652
+ console.print("[yellow] No hay contenedores Docker en ejecución.[/]")
1653
+ return []
1654
+
1655
+ console.print(f"[cyan] Escaneando {len(ids)} contenedor(es) Docker en ejecución...[/]")
1656
+ todos_hallazgos: List[Finding] = []
1657
+
1658
+ for cid in ids:
1659
+ todos_hallazgos.extend(scan_docker_container(cid))
1660
+
1661
+ return todos_hallazgos
1662
+
1663
+
1664
+ def _scan_string_for_secrets(texto: str) -> List[Dict]:
1665
+ """
1666
+ Aplica todos los SECRET_PATTERNS a una cadena de texto y devuelve
1667
+ una lista de coincidencias brutas (sin crear objetos Finding).
1668
+
1669
+ Cada elemento del resultado es un dict con las claves:
1670
+ - 'name': nombre del patrón coincidente
1671
+ - 'severity': nivel de severidad del patrón
1672
+ - 'category': categoría del patrón
1673
+ - 'valor': fragmento de texto que coincidió (sin censurar)
1674
+
1675
+ Útil como auxiliar en funciones de escaneo de entornos externos
1676
+ (Docker runtime, Kubernetes Secrets, etc.) donde el contexto
1677
+ de creación del Finding difiere del flujo estático habitual.
1678
+ """
1679
+ coincidencias: List[Dict] = []
1680
+ for pat in SECRET_PATTERNS:
1681
+ match = pat["compiled"].search(texto)
1682
+ if match:
1683
+ coincidencias.append({
1684
+ "name": pat["name"],
1685
+ "severity": pat["severity"],
1686
+ "category": pat.get("category", "Secret"),
1687
+ "valor": match.group(0),
1688
+ })
1689
+ return coincidencias
1690
+
1691
+
1692
+ def _kubectl_disponible() -> bool:
1693
+ """Comprueba si el binario 'kubectl' está disponible en el PATH."""
1694
+ try:
1695
+ subprocess.run(
1696
+ ["kubectl", "version", "--client", "--output=json"],
1697
+ capture_output=True,
1698
+ timeout=5,
1699
+ )
1700
+ return True
1701
+ except (FileNotFoundError, OSError):
1702
+ return False
1703
+
1704
+
1705
+ def scan_kubernetes_secrets(namespace: Optional[str] = None) -> List[Finding]:
1706
+ """
1707
+ Escanea los Kubernetes Secrets del clúster activo buscando credenciales
1708
+ y datos sensibles codificados en base64.
1709
+
1710
+ Parámetros:
1711
+ namespace: si se indica, limita la búsqueda a ese namespace;
1712
+ si es None se escanean todos los namespaces.
1713
+
1714
+ Requiere que 'kubectl' esté disponible en el PATH y que el contexto
1715
+ activo tenga permisos de lectura sobre los Secrets del namespace indicado.
1716
+
1717
+ Retorna una lista de objetos Finding con los secretos detectados.
1718
+ """
1719
+ if not _kubectl_disponible():
1720
+ console.print("[yellow] Aviso: 'kubectl' no encontrado en PATH — omitiendo escaneo de K8s Secrets[/]")
1721
+ return []
1722
+
1723
+ # Construir comando kubectl
1724
+ if namespace:
1725
+ cmd = ["kubectl", "get", "secret", "-n", namespace, "-o", "json"]
1726
+ scope_label = f"namespace '{namespace}'"
1727
+ else:
1728
+ cmd = ["kubectl", "get", "secret", "--all-namespaces", "-o", "json"]
1729
+ scope_label = "todos los namespaces"
1730
+
1731
+ console.print(f"[cyan] Consultando Kubernetes Secrets ({scope_label})...[/]")
1732
+
1733
+ try:
1734
+ result = subprocess.run(
1735
+ cmd,
1736
+ capture_output=True,
1737
+ text=True,
1738
+ timeout=30,
1739
+ )
1740
+ except subprocess.TimeoutExpired:
1741
+ console.print("[red] Tiempo de espera agotado ejecutando 'kubectl get secret'[/]")
1742
+ return []
1743
+ except (FileNotFoundError, OSError) as exc:
1744
+ console.print(f"[red] Error ejecutando kubectl: {exc}[/]")
1745
+ return []
1746
+
1747
+ if result.returncode != 0:
1748
+ console.print(f"[red] kubectl salió con código {result.returncode}: {result.stderr.strip()[:200]}[/]")
1749
+ return []
1750
+
1751
+ try:
1752
+ data = json.loads(result.stdout)
1753
+ except json.JSONDecodeError as exc:
1754
+ console.print(f"[red] Error parseando salida JSON de kubectl: {exc}[/]")
1755
+ return []
1756
+
1757
+ # Puede ser una lista (--all-namespaces) o un objeto único
1758
+ items = data.get("items", [data]) if "items" in data else [data]
1759
+ hallazgos: List[Finding] = []
1760
+
1761
+ for secret in items:
1762
+ meta = secret.get("metadata", {})
1763
+ ns = meta.get("namespace", "default")
1764
+ name = meta.get("name", "desconocido")
1765
+ tipo = secret.get("type", "")
1766
+ datos = secret.get("data") or {}
1767
+
1768
+ for clave, valor_b64 in datos.items():
1769
+ # Decodificar el valor base64
1770
+ try:
1771
+ valor_decoded = base64.b64decode(valor_b64).decode("utf-8", errors="replace")
1772
+ except Exception:
1773
+ continue
1774
+
1775
+ # Buscar patrones de secreto en el valor decodificado
1776
+ coincidencias = _scan_string_for_secrets(valor_decoded)
1777
+
1778
+ # También escanear la clave (puede revelar el tipo de secreto)
1779
+ clave_coincidencias = _scan_string_for_secrets(clave)
1780
+
1781
+ # Usar el valor decodificado si no hay coincidencias por patrón
1782
+ # pero la clave sugiere que es un secreto
1783
+ claves_sensibles = {"password", "passwd", "secret", "token", "key",
1784
+ "apikey", "api_key", "private_key", "auth"}
1785
+ es_clave_sensible = any(k in clave.lower() for k in claves_sensibles)
1786
+
1787
+ if not coincidencias and es_clave_sensible:
1788
+ # Hallazgo genérico basado en nombre de clave
1789
+ fp = _fingerprint(valor_decoded, f"K8S_SECRET_KEY:{clave}")
1790
+ extracto = _censor(valor_decoded[:120])
1791
+ hallazgos.append(Finding(
1792
+ file=f"k8s://{ns}/{name}",
1793
+ line_no=0,
1794
+ pattern=f"K8S_SECRET_KEY: {clave} (tipo: {tipo})",
1795
+ category="Kubernetes · Secret",
1796
+ severity=Severity.HIGH,
1797
+ preview=extracto,
1798
+ context=[f"namespace={ns}", f"secret={name}", f"tipo={tipo}", f"clave={clave}"],
1799
+ fingerprint=fp,
1800
+ ))
1801
+ else:
1802
+ for c in coincidencias:
1803
+ fp = _fingerprint(c["valor"], f"K8S_SECRET:{ns}/{name}/{clave}")
1804
+ extracto = _censor(c["valor"])
1805
+ hallazgos.append(Finding(
1806
+ file=f"k8s://{ns}/{name}",
1807
+ line_no=0,
1808
+ pattern=f"K8S_SECRET: {c['name']} (clave: {clave})",
1809
+ category="Kubernetes · Secret",
1810
+ severity=c["severity"],
1811
+ preview=extracto,
1812
+ context=[f"namespace={ns}", f"secret={name}", f"tipo={tipo}",
1813
+ f"clave={clave}", f"patron={c['name']}"],
1814
+ fingerprint=fp,
1815
+ ))
1816
+
1817
+ if hallazgos:
1818
+ console.print(f"[dim] {len(hallazgos)} hallazgo(s) en Kubernetes Secrets[/]")
1819
+ else:
1820
+ console.print("[dim] Sin secretos detectados en Kubernetes Secrets[/]")
1821
+
1822
+ return hallazgos
1823
+
1824
+
1494
1825
  # ─────────────────────────────────────────────────────────────────────────────
1495
1826
  # CLI
1496
1827
  # ─────────────────────────────────────────────────────────────────────────────
@@ -1566,6 +1897,27 @@ def parse_args() -> argparse.Namespace:
1566
1897
  "El baseline se crea si no existe (default: .vamp-secrets-baseline.json)."
1567
1898
  ))
1568
1899
 
1900
+ docker = p.add_argument_group("Escaneo Docker en runtime (v2.3)")
1901
+ docker.add_argument("--scan-container", metavar="NAME_OR_ID",
1902
+ dest="scan_container", default=None,
1903
+ help="Escanear variables de entorno de un contenedor Docker en ejecución. "
1904
+ "Requiere que 'docker' esté disponible en el PATH.")
1905
+ docker.add_argument("--scan-all-containers", action="store_true",
1906
+ dest="scan_all_containers",
1907
+ help="Escanear todos los contenedores Docker actualmente en ejecución "
1908
+ "('docker ps -q'). Requiere que 'docker' esté disponible en el PATH.")
1909
+
1910
+ k8s = p.add_argument_group("Escaneo Kubernetes Secrets (v2.4)")
1911
+ k8s.add_argument("--k8s", action="store_true",
1912
+ dest="k8s",
1913
+ help="Escanear los Kubernetes Secrets del clúster activo (todos los namespaces). "
1914
+ "Requiere que 'kubectl' esté disponible en el PATH y que el contexto "
1915
+ "activo tenga permisos de lectura sobre los Secrets.")
1916
+ k8s.add_argument("--k8s-namespace", metavar="NS",
1917
+ dest="k8s_namespace", default=None,
1918
+ help="Limitar el escaneo de K8s Secrets a un namespace concreto "
1919
+ "(implica --k8s). Ej: --k8s-namespace production")
1920
+
1569
1921
  ci = p.add_argument_group("Integración CI/CD")
1570
1922
  ci.add_argument("--install-hook", action="store_true",
1571
1923
  help="Instalar hook pre-commit git en el directorio objetivo y salir")
@@ -2016,6 +2368,49 @@ def main() -> None:
2016
2368
  console.print("[dim] Sin misconfiguraciones Vault detectadas[/]\n")
2017
2369
  all_findings.extend(vault_findings)
2018
2370
 
2371
+ # ── Fase 2d: escaneo de contenedores Docker en runtime (v2.3) ────────────
2372
+ _contenedor_a_escanear = getattr(args, "scan_container", None)
2373
+ _escanear_todos = getattr(args, "scan_all_containers", False)
2374
+
2375
+ if _contenedor_a_escanear:
2376
+ console.print("[bold cyan] FASE 2d[/] — Escaneando contenedor Docker en runtime...")
2377
+ docker_findings = scan_docker_container(_contenedor_a_escanear)
2378
+ if docker_findings:
2379
+ console.print(
2380
+ f"[dim] {len(docker_findings)} hallazgo(s) en variables de entorno "
2381
+ f"del contenedor '{_contenedor_a_escanear}'[/]\n"
2382
+ )
2383
+ else:
2384
+ console.print("[dim] Sin secretos detectados en el contenedor indicado[/]\n")
2385
+ all_findings.extend(docker_findings)
2386
+
2387
+ elif _escanear_todos:
2388
+ console.print("[bold cyan] FASE 2d[/] — Escaneando todos los contenedores Docker en ejecución...")
2389
+ docker_findings = scan_all_docker_containers()
2390
+ if docker_findings:
2391
+ console.print(
2392
+ f"[dim] {len(docker_findings)} hallazgo(s) en variables de entorno "
2393
+ f"de contenedores Docker[/]\n"
2394
+ )
2395
+ else:
2396
+ console.print("[dim] Sin secretos detectados en los contenedores en ejecución[/]\n")
2397
+ all_findings.extend(docker_findings)
2398
+
2399
+ # ── Fase 2e: escaneo de Kubernetes Secrets (v2.4) ────────────────────────
2400
+ _k8s_ns = getattr(args, "k8s_namespace", None)
2401
+ _k8s = getattr(args, "k8s", False) or bool(_k8s_ns)
2402
+
2403
+ if _k8s:
2404
+ console.print("[bold cyan] FASE 2e[/] — Escaneando Kubernetes Secrets...")
2405
+ k8s_findings = scan_kubernetes_secrets(namespace=_k8s_ns)
2406
+ if k8s_findings:
2407
+ console.print(
2408
+ f"[dim] {len(k8s_findings)} hallazgo(s) en Kubernetes Secrets[/]\n"
2409
+ )
2410
+ else:
2411
+ console.print("[dim] Sin secretos detectados en Kubernetes Secrets[/]\n")
2412
+ all_findings.extend(k8s_findings)
2413
+
2019
2414
  # ── Deduplicación y filtrado ──────────────────────────────────────────────
2020
2415
  seen: Set[str] = set()
2021
2416
  unique: List[Finding] = []