Organización y estructura¶
En este bloque se tratan los casos de estudio relacionados con la organización y estructura de proyectos.
ST-001 - Estructura de proyecto¶
Problema¶
Muchas herramientas no contemplan la opción de ser usadas como librería (con un “import”) además de por linea de comandos, o la interfaz que tengan definida.
Su uso puede ser complicado y, muchas veces, tan solo pueden ser usadas por linea de comandos, lo que implica llamar a un proceso externo del sistema y parsear su salida.
Solución¶
Una estructura correcta en la organización de archivos, pensada para ser re-utilizable.
Cómo¶
En la imagen se puede ver la estructura propuesta:
Donde:
- LICENCE: Fichero de licencia. Contiene el texto legal con el que queremos redistribuir nuestro código.
- README.rst: Fichero índice de documentación del proyecto. Será el primero en mostrarse y leerse por defecto.
- requirements.txt: Contiene las dependencias de librerías externas de nuestro proyecto.
- setup.py: Contiene información para la instalación, configuración y redistribución de nuestro software.
- TODO.rst: Ideas futuras a implementar en nuestro proyecto. Es buena idea tenerlo por si alguien quiere colaborar con nuestro proyecto, ya que podrá encontrar tareas e ideas por hacer.
- app_name: Carpeta, que actúa como paquete Python, con el nombre de nuestra aplicación.
- app_name/bin: Ejecutables disponibles en nuestra aplicación.
- app_name/doc: Archivos de documentación, usualmente escrita con Shpinx. Esta guía es un ejemplo de su uso.
- app_name/lib: Librerías propias que genere nuestra aplicación.
- app_name/test: Contiene los diferentes test de la aplicación: Unitarios, de integración, rendimiento...
ST-002 - Entrada de parámetros globales¶
Problema¶
Una vez preparada la estructura para nuestra aplicación, ahora queremos “llamar” o ejecutar el programa en cuestión. Problemas:
- Añadir nuevos parámetros de entrada al programa, requiere muchos cambios.
- El código se hace cada vez más enrevesado e inmanejable.
- Cambios de versiones de un mismos programas son incompatibles.
Este ejemplo de programa que divide dos números. Acepta 2 parámetros por linea de comandos:
- Nominado
- Denominador
Si ocurre una división por 0 devuelve -1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import argparse
# ----------------------------------------------------------------------
def divide(param1, param2):
try:
return param1 / param2
except ZeroDivisionError:
return -1
# ----------------------------------------------------------------------
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-p1", dest="param1", help="parameter1")
parser.add_argument("-p2", dest="param1", help="parameter2")
params = parser.parse_args()
d = divide(params.param1, params.param2)
print(d)
|
Ahora queremos añadir otro parámetro, “verbosity”, como opción a nuestro programa.
Vemos que hay que modificar el código en 2 sitios diferentes. En todos aquellos donde se tenga que pasar información de las opciones de ejecución:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # ----------------------------------------------------------------------
def divide(param1, param2, verbosity):
try:
return param1 / param2
except ZeroDivisionError as e:
if verbosity > 0:
print(e)
return -1
# ----------------------------------------------------------------------
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-p1", dest="param1", help="parameter1")
parser.add_argument("-p2", dest="param1", help="parameter2")
parser.add_argument("-v", dest="verbosity", type="int", help="verbosity")
params = parser.parse_args()
d = divide(params.param1, params.param2, params.verbosity)
print(d)
|
Solución¶
Hacer un objeto global que sea el que contenga los parámetros globales de ejecución (proyectos grandes, como nmap, proyecto lo hacen de esta forma):
Cómo¶
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 | # --------------------------------------------------------------------------
class Parameters:
"""Program parameters"""
# ----------------------------------------------------------------------
def __init__(self, **kwargs):
self.param1 = kwargs.get("param1")
self.param2 = kwargs.get("param2")
self.verbosity = kwargs.get("verbosity")
# ----------------------------------------------------------------------
def divide(input_params):
try:
return input_params.param1 / input_params.param2
except ZeroDivisionError as e:
if input_params.verbosity > 0:
print(e)
return -1
# ----------------------------------------------------------------------
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-p1", dest="param1", help="parameter1")
parser.add_argument("-p2", dest="param1", help="parameter2")
parser.add_argument("-v", dest="verbosity", type="int", help="verbosity")
params = parser.parse_args()
input_parameters = Parameters(param1=params.param1,
param2=params.param2,
verbosity=params.verbosity)
d = divide(input_parameters)
print(d)
|
ST-003 - Gestión de resultados¶
Problema¶
Recuperar la información de salida de una aplicación es muy complicado:
- No está estandarizada
- Hay que parsear muchos XML/JSON.
- La herramienta solo reporta resultados por consola.
Usando el mismo ejemplo que en el anterior caso:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import argparse
# ----------------------------------------------------------------------
def divide(param1, param2):
try:
return param1 / param2
except ZeroDivisionError:
return -1
# ----------------------------------------------------------------------
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-p1", dest="param1", help="parameter1")
parser.add_argument("-p2", dest="param1", help="parameter2")
params = parser.parse_args()
d = divide(params.param1, params.param2)
print(d)
|
Cuando queremos añadir un parámetro nuevo en la devolución, vemos que hay que modificar varias lineas de código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # ----------------------------------------------------------------------
def divide(param1, param2):
try:
results = param1 / param2
is_2_divisor = results % 2
return results, is_2_divisor
except ZeroDivisionError as e:
return -1, False
# ----------------------------------------------------------------------
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-p1", dest="param1", help="parameter1")
parser.add_argument("-p2", dest="param1", help="parameter2")
params = parser.parse_args()
result, is_divisor = divide(params.param1, params.param2)
print("Results: %s - %s" % (result, is_divisor))
|
Solución¶
Usar objetos genéricos que contengan la información resultante de la ejecución del a herramienta.
Además, estos objetos, nos permiten abstraer el almacenamiento de información, de cómo ésta es exportada o transformada: XML, JSON, HTML, PDF ...
Cómo¶
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 | # --------------------------------------------------------------------------
class Results:
"""Program parameters"""
# ----------------------------------------------------------------------
def __init__(self, **kwargs):
self.result = kwargs.get("result", 0)
self.is_2_divisor = kwargs.get("is_2_multiple", False)
# ----------------------------------------------------------------------
def divide(param1, param2):
try:
results = param1 / param2
is_2_divisor = results % 2
return Results(result=results,
is_2_divisor=is_2_divisor)
except ZeroDivisionError:
return Results()
# ----------------------------------------------------------------------
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='ktcal2 - SSH brute forcer')
parser.add_argument("-p1", dest="param1", help="parameter1")
parser.add_argument("-p2", dest="param1", help="parameter2")
parser.add_argument("-v", dest="verbosity", type="int", help="verbosity")
params = parser.parse_args()
result = divide(params.param1, params.param2)
print("Results: %s - %s" % (result.result, result.is_2_divisor))
|
ST-004 - Unificar puntos de entrada¶
Problema¶
Tengo que cambiar mucho código y rehacer gran parte de mi aplicación, cada vez que quiero hacer una nueva UI (User Interface):
- Linea de comandos.
- Interfaz gráfica.
- Web.
- Que se use como librería.
Solución¶
Centralizar el punto de entrada a la ejecución de tu aplicación en un único punto y que todas las UI usen el mismo.
Cómo¶
Incluir el concepto de api.py y enseñar como el command line y el import funcionan igual.
Tras incluir el fichero “api.py”, la UI de linea de comandos (st-004-s1.py) nos quedará como sigue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from .api import Parameters, run
# ----------------------------------------------------------------------
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-p1", dest="param1", help="parameter 1")
parser.add_argument("-p2", dest="param2", help="parameter 2")
params = parser.parse_args()
input_parameters = Parameters(param1=params.param1,
param2=params.param2,
verbosity=params.verbosity)
run(params)
|
Si echamos un vistazo a api.py, podemos observar que este fichera centraliza las llamadas al resto de librerías:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # Import data
from .lib.data import *
# Import libs
from .lib.operations import divide
from .lib.exports import display_results
# ----------------------------------------------------------------------
def run(input_parameters):
# Run operation
r = divide(input_parameters)
# Display results
display_results(r)
|
ST-005 - La linea de comandos¶
Problema¶
Crear una linea de comandos avanzada puede no resultar sencilla, y mucho menos intutiva.
Estos son algunos de los problemas más comunes que podemos encontrarnos:
- Crear opciones posicionales: main.py -n -i 9 POSITIONAL_PARAM.
- Crear una ayuda personalizada, cuando se ejecute la opción -h: main.py -h.
- Los tipos de datos recogidos por la linea de comandos son incorrectos: main.py -t 4, por defecto 4 será tratado como un string, no como un int.
- Ausencia de opciones por defecto.
- Establecer ciertas opciones como obligatorios.
- Establecer un opciones y una versión abreviada de las mismas: main.py -t 8 equivalente a main.py --threads 8.
- Opciones sin parámetros, tratados como bool: main.py -j.
- Opciones que puedan usarse como acumuladores: main.py -v -> main.py -vv -> main.py -vvv.
Solución¶
En este caso de prueba la solución no es otra que usar correctamente la libraría de Python argparse
Cómo¶
Para que sea más ilustrativo, vamos a partir de una linea de comandos básica y vamos ir mejorándola poco a poco.
La linea de comandos de la que partiremos la que sigue, de un posible escaneador de puertos:
1 2 3 4 5 6 7 8 9 | if __name__ == "__main__":
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-t", dest="targets", help="target")
parser.add_argument('-v', dest='verbosity level')
parser.add_argument("--open", dest="only_open", help="only display open ports")
parser.add_argument("--port", dest="port")
params = parser.parse_args()
|
Que mostrará el siguiente resultado por consola, cuando ejecutamos st-005-p1.py -h:
usage: st-005-p1.py [-h] [-t TARGETS] [-v VERBOSITY LEVEL] [--open ONLY_OPEN]
[-p PORTS_RANGE]
OMSTD Example
optional arguments:
-h, --help show this help message and exit
-t TARGETS target
-v VERBOSITY LEVEL
--open ONLY_OPEN only display open ports
--port PORT
Nota
Nótese que se puede ejecutar sin parámetros y no se devolvería ningún error a pesar de que, por razones obvias, necesitamos como mínimo un parámetros: targets.
1 - Opciones obligatorios¶
En primer lugar vamos a solventar el problema del parámetro obligatorio y vamos a obligar a especificar un target:
1 2 3 4 5 6 7 | parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-t", dest="targets", help="target", required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level', type=int)
parser.add_argument("--open", dest="only_open", help="only display open ports", default=0)
parser.add_argument("--port", dest="port", type=int, help="port to scan")
params = parser.parse_args()
|
Vemos cómo, tras en cambio, se obliga al usuario a especificar un target:
usage: st-005-p1.py [-h] -t TARGETS [-v VERBOSITY LEVEL] [--open ONLY_OPEN]
[-p PORTS_RANGE]
st-005-p1.py: error: the following arguments are required: -t
2 - Tipos de datos¶
Vemos que las opciones verbosity (0-2) y port son tratadas como string (por defecto), pero realmente son del tipo int.
Añadimos la mejora:
1 2 3 4 5 6 7 | parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-t", dest="targets", help="target", required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level', type=int)
parser.add_argument("--open", dest="only_open", help="only display open ports", default=0)
parser.add_argument("--port", dest="port", type=int, help="port to scan")
params = parser.parse_args()
|
3 - Valores por defecto¶
Sería interesante contar con valores por defecto para cada tipo de parámetro.
De esta forma evitaremos errores, por ausencia de dichos valores por parte del usuarios, e introduciremos configuración predeterminada, lo que hará el uso de la herramienta más fácil:
1 2 3 4 5 6 7 8 | parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-t", dest="targets", help="target", required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level', type=int, default=0)
parser.add_argument("--open", dest="only_open", help="only display open ports", default=0)
parser.add_argument("--port", dest="port", type=int, default=80,
help="port to scan. Default: 80.")
params = parser.parse_args()
|
4 - Opciones sin parámetros¶
Si observamos, la opción –open, es realmente un booleano. Es decir, que solo necesitamos saber si está a True o a False.
Actualizamos para que dicha opción sea tratada como un booleano.
1 2 3 4 5 6 7 8 9 | parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-t", dest="targets", help="target", required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level', type=int, default=0)
parser.add_argument("--open", dest="only_open", action="store_true",
help="only display open ports", default=False)
parser.add_argument("--port", dest="port", type=int, default=80,
help="port to scan. Default: 80.")
params = parser.parse_args()
|
Nota
Los datos booleanos no necesitan indicarles el tipo, cuando se usan con la opción action="store_true|false. El tipo está implícido cuando usamos el parámetro de configuración action="store_XXXX".
5 - Opciones abreviadas¶
Cuando tenemos muchas opciones en la linea de comandos, es intersante poder poner la opciones en formato más abrevidado.
Para nuestro ejemplo, cuando indicamos del puerto, podríamos crear la opción abreviada -p, además de --port como sigue:
1 2 3 4 5 6 7 8 9 | parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-t", dest="targets", help="target", required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level', type=int, default=0)
parser.add_argument("--open", dest="only_open", action="store_true",
help="only display open ports", default=False)
parser.add_argument("-p", "--port", dest="port", type=int, default=80,
help="port to scan. Default: 80.")
params = parser.parse_args()
|
6 - Opciones auto-incrementales¶
Existen ciertas opciones que tienen más sentido que su valor vaya incrementando en función de las veces que se repite. Por ejemplo:
- -v en lugar de 0.
- -vv en lugar de 1.
- -vvv en lugar de 2.
En nuestro ejemplo, dicho comportamiento es perfecto para el opación verbosity:
1 2 3 4 5 6 7 8 9 10 | parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-t", dest="targets", help="target", required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level: -v, -vv, -vvv.',
action="count", type=int, default=0)
parser.add_argument("--open", dest="only_open", action="store_true",
help="only display open ports", default=False)
parser.add_argument("-p", "--port", dest="port", type=int, default=80,
help="port to scan. Default: 80.")
params = parser.parse_args()
|
7 - Opciones posicionales¶
Por último, puede que nos interese tener parámetro qeu no van precedidos de ninguna opción que empiece por -. Por ejemplo:
- main.py 127.0.0.1 en lugar de main.py -t 127.0.0.1.
Esta forma de configurar las opciones de la linea de comandos se conoce como opciones posicionales.
Entenderemos mejor esto con un ejemplo:
1 2 3 4 5 6 7 8 9 10 11 | parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("targets", metavar='TARGETS', help="targets to scan", nargs="+",
required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level: -v, -vv, -vvv.',
action="count", type=int, default=0)
parser.add_argument("--open", dest="only_open", action="store_true",
help="only display open ports", default=False)
parser.add_argument("-p", "--port", dest="port", type=int, default=80,
help="port to scan. Default: 80.")
params = parser.parse_args()
|
La linea de comando ahora se verá así:
usage: st-005-s1.py [-h] [-v] [--open] [-p PORT] TARGETS [TARGETS ...]
OMSTD Example
positional arguments:
TARGETS targets to scan
optional arguments:
-h, --help show this help message and exit
-v verbosity level: -v, -vv, -vvv.
--open only display open ports
-p PORT, --ports PORT
port to scan. Defaul: 80.
Una ver parseada la linea de comandos, en el objeto params, podremos obtener la lista de los parámetros posicionales leyendo el parámetro params.targets.
8 - Documentación extendida¶
Suele ser muy útil para nuestros programas poner pequeños ejemplos al final de la ayuda que nos proporciona la opción -h. Para esto, usamos el parámetro epilog, del módulo argparse:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | footed_description = """
Usage examples:
+ Basic scan:
%(name)s 127.0.0.1
+ Specifing port to test:
%(name)s -p 443 127.0.0.1
+ Only diplay open ports
%(name)s -p 139 127.0.0.1
""""" % dict(name="st-005-s1.py")
parser = argparse.ArgumentParser(description='OMSTD Example', epilog=footed_description,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("targets", metavar='TARGETS', help="targets to scan", nargs="+")
parser.add_argument('-v', dest='verbosity', default=0, action="count",
help="verbosity level: -v, -vv, -vvv.")
parser.add_argument("--open", dest="only_open", action="store_true",
help="only display open ports", default=False)
parser.add_argument("-p", "--ports", dest="port", type=int,
help="port to scan. Defaul: 80.", default=80)
params = parser.parse_args()
|
Este es el aspecto que tendría al ejecutar st-005-s1.py -h:
usage: st-005-s1.py [-h] [-v] [--open] [-p PORT] TARGETS [TARGETS ...]
OMSTD Example
positional arguments:
TARGETS targets to scan
optional arguments:
-h, --help show this help message and exit
-v verbosity level: -v, -vv, -vvv.
--open only display open ports
-p PORT, --ports PORT
port to scan. Defaul: 80.
Usage examples:
+ Basic scan:
st-005-s1.py 127.0.0.1
+ Specifing port to test:
st-005-s1.py -p 443 127.0.0.1
+ Only diplay open ports
st-005-s1.py -p 139 127.0.0.1