Ethical Hacking + Python III - Tests

hacking python

Vamos a mejorar la calidad de nuestro código e implementar pruebas unitarias en Habu https://gitlab.com/securetia/habu

Introducción

Tarde o temprano, el código que escribimos va a dejar de funcionar.

Por modificar algo en una librería, por no contemplar una determinada dependencia o por mil otras razones diferentes.

Sabiendo que va a fallar, tenemos que intentar manejar esos fallos de la mejor manera posible.

Eso lo logramos a través de dos cosas:

  1. Pruebas unitarias
  2. Integración contínua

Además, tenemos que pensar que nuestro código tiene que ser mantenido, ya sea por nosotros o por alguna otra persona, por lo tanto, tenemos que volver ese código fácil de leer y entender.

Eso lo logramos siguiendo las mejores prácticas para la escritura de código.

En este artículo vamos a resolver esos tres puntos.

Pruebas Unitarias

Las pruebas unitarias apuntan a verificar que las porciones de nuestro código (por ejemplo, funciones de una librería) funcionan como deberían.

Dichas pruebas también van a ser escritas en código Python, y van a ser simples funciones que verifican los resultados devueltos por las funciones que queremos probar.

Python, en su librería estándar, ya cuenta con el módulo unittest, pero nosotros vamos a utilizar el módulo PyTest, que es más fácil de utilizar y ofrece salidas más completas y claras.

    pip install pytest

A continuación, vamos a crear el directorio ‘tests’ dentro del directorio principal de nuestro proyecto.

Y vamos a escribir algunos tests sencillos para nuestra librería xor.

Estos tests los vamos a poner en un archivo llamado ‘test_xor.py’.

El hecho de que el nombre del archivo empiece con ‘test’ y esté dentro del directorio ‘tests’, le permite a PyTest encontrar las pruebas y ejecutarlas de una forma sencilla.

El contenido del archivo ‘test_xor.py’ va a ser el siguiente:

    from habu.lib.xor import xor



    def test_xor():
        text = b'text to encrypt'
        encrypted = xor(text)
        assert text == xor(encrypted)


    def test_xor_w_key():
        text = b'text to encrypt'
        key = b'secret'
        encrypted = xor(text, key)
        assert text == xor(encrypted, key)

Como podemos ver, tenemos dos funciones, las dos muy parecidas.

La primera, ‘test_xor()’, xorea el texto de la variable ‘text’ y luego verifica que al volver a xorearlo, se obtenga el texto original.

La función ‘asset’ nos permite verificar una condición y hacer que el éxito o el fracaso del test dependa de ella.

En este caso, estamos verificando que ‘text’ sea igual al resultado de ‘xor(encrypted)‘.

La segunda, ‘test_xor_w_key()’, hace lo mismo que la primera, pero utiliza el parámetro que define la clave con la cual vamos a xorear.

Si guardamos el archivo y ejecutamos el comando ‘pytest -v’ (el parámetro ‘-v’, como es habitual, habilita el modo verbose, para que obtengamos más datos acerca de la ejecución de pytest):

    $ pytest 
    =================== test session starts =============================
    platform linux -- Python 3.5.2, pytest-3.0.3, py-1.4.31, pluggy-0.4.0
    rootdir: /home/f/p/habu, inifile: pytest.ini
    collected 2 items 

    test_xor.py::test_xor PASSED
    test_xor.py::test_xor_w_key PASSED

    =============== 2 passed in 0.02 seconds ============================

Como podemos ver, los tests pasaron (‘PASSED’).

Si modificamos la función ‘test_xor()’ y la cambiamos por:

    def test_xor():
        text = b'text to encrypt'
        encrypted = xor(text)
        assert text != xor(encrypted)

Vamos a generar que la prueba falle (cambiamos el == por un !=), es decir, la prueba espera que text sea diferente al resultado de xor(encrypted).

    ====================== test session starts ====================================
    platform linux -- Python 3.5.2, pytest-3.0.3, py-1.4.31, pluggy-0.4.0
    cachedir: ../.cache
    rootdir: /home/f/p/habu, inifile: pytest.ini
    collected 2 items 

    test_xor.py::test_xor FAILED
    test_xor.py::test_xor_w_key PASSED

    =========================== FAILURES ==========================================
    ___________________________ test_xor __________________________________________

        def test_xor():
            text = b'text to encrypt'
            encrypted = xor(text)
    >       assert text != xor(encrypted)
    E       assert b'text to encrypt' != b'text to encrypt'
    E        +  where b'text to encrypt' = xor(b'DUHDD_U^SBI@D')

    test_xor.py:8: AssertionError
    ============== 1 failed, 1 passed in 0.04 seconds =============================

Claramente, PyTest nos está explicando qué fue lo que falló.

Deberíamos escribir pruebas para todas las funciones, o por lo menos, para las que consideremos más importantes.

Esto también nos fuerza a escribir funciones pequeñas, que hagan cosas claras.

Es muy difícil crear tests para funciones complejas.

Nota: En los ejemplos, podemos ver que los tests que acabo de escribir no son perfectos. Un simple caso de error sería el caso en el que la función xor() no esté haciendo absolutamente nada, y devolviera los valores sin modificar. En ese caso, las pruebas serían exitosas. Eso podríamos arreglarlo mejorando las pruebas, o creando otras pruebas adicionales, que verifiquen puntualmente el hecho de que el resultado de ‘xor()’ sea el esperado.

Integración Contínua

Con la integración contínua hacemos que nuestros tests sean ejecutados en uno o varios ambientes, para verificar que no funcionan únicamente en nuestras PC.

(Es muy habitual que las cosas funcionen solo en la máquina del desarrollador, y esto es un claro signo de que algo anda mal)

Como Habu es un proyecto hosteado en GitLab, podemos utilizar directamente su solución de integración contínua de forma gratuita.

(Para los que hosteen sus repositorios en GitHub, pueden utilizar Travis
https://travis-ci.org/).

Para que GitLab sepa que queremos utilizar su CI, debemos generar un archivo llamado ‘.gitlab-ci.yml’ en el directorio raíz de nuestro repositorio.

En el caso de Habu, el archivo contiene lo siguiente:

    test:
      script:
      - apt-get update -qy
      - apt-get install -y python3-dev python3-pip
      - pip3 install virtualenv
      - virtualenv --python=/usr/bin/python3 venv
      - venv/bin/pip install -r requirements.txt
      - venv/bin/pip install -r dev-requirements.txt
      - venv/bin/pip install -e .
      - venv/bin/python setup.py pytest

Como podemos ver, son simplemente los comandos que debemos ejecutar para obtener un entorno en el cual se pueda instalar Habu y ejecutar sus tests.

Cada vez que hagamos un nuevo commit del código, veremos el resultado de los tests en la interfaz web de GitLab.

NOTA: Si tienen dudas sobre el funcionamiento de GitLab-CI, pueden visitar el siguiente link: https://docs.gitlab.com/ce/ci/quick_start/README.html.

Otro de los cambios que debemos hacer para que esto funcione, es agregar las dependencias de PyTest y PyTest-Runner (este último ayuda a que sea más fácil ejecutar de forma automática PyTest).

Para esto, editamos el archivo ‘setup.py’, y lo configuramos de la siguiente forma:

    with open('README.rst') as f:
        readme = f.read()

    setup(
        name='habu',
        version='0.0.10',
        description='Ethical Hacking Utils',
        long_description=readme,
        author='Fabian Martinez Portantier',
        author_email='fportantier@securetia.com',
        url='https://gitlab.com/securetia/habu',
        license='GNU General Public License v3 (GPLv3)',
        install_requires=[
            'Click',
            'Requests',
        ],
        tests_require=[
            'pytest',
            'pytest-runner',
        ],
        packages=['habu'],
        include_package_data=True,
        entry_points='''
            [console_scripts]
            habu.ip=habu.cli.cmd_ip:cmd_ip
            habu.xor=habu.cli.cmd_xor:cmd_xor
        ''',
        classifiers=[
            "Environment :: Console",
            "Intended Audience :: Developers",
            "Intended Audience :: Information Technology",
            "Intended Audience :: System Administrators",
            "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
            "Topic :: Security",
            "Topic :: System :: Networking",
            "Programming Language :: Python :: 3.0",
            "Programming Language :: Python :: 3.6",
        ],
        keywords=['security'],
        zip_safe=False,
        test_suite='py.test',
    )

Las partes importates para lo que vimos hasta ahora son:

  1. tests_require: Define qué paquetes son necesarios para ejecutar los tests
  2. test_suite: Define cuál es el comando a ejecutar para correr los tests

Calidad de Código

Python cuenta con varias mejores prácticas a la hora de escribir código. La mayoría de las cuales están detalladas en el documento PEP8 https://www.python.org/dev/peps/pep-0008/.

Para verificar que nuestro código cumple con dichas mejores prácticas, y validar algunas otras que no están definidas en PEP8, podemos utilizar el módulo ‘flake8’, que reúne varias pruebas y las pone a disposición a través del comando ‘flake8’.

    $ pip install flake8

Ahora, si ejecutamos ‘flake8’, podremos ver algo como lo siguiente:

    $ flake8 
    ./habu/lib/xor.py:2:1: F401 'datetime.datetime' imported but unused
    ./habu/lib/xor.py:4:1: E302 expected 2 blank lines, found 1
    ./habu/lib/xor.py:9:26: E203 whitespace before ':'
    ./habu/lib/xor.py:11:14: E225 missing whitespace around operator
    ./habu/lib/xor.py:14:1: W391 blank line at end of file

Veremos varios problemas que agregué a propósito para demostrar el funcionamiento de flake8, y no están en el repositorio de código.

Más allá de cuestiones de estilo, como el hecho de dejar o no espacios entre operadores, podemos ver que nos está alertando sobre la importación de un módulo que no ha sido utilizado (datetime).

Corregir todas estas alertas puede ser tedioso, pero hace que nuestro código sea mucho más legible y aceptable por toda la comunidad Python.

En el caso de que flake8 no envie ninguna salida por pantalla, quiere decir que no ha encontrado ningún error.

Conclusión

Hemos visto varias formas de mejorar nuestro código, y detectar las fallas que pudieran introducir nuestras modificaciones lo antes posible.

En los próximos artículos

Vamos a agregar nuevas funcionalidades a Habu, más relacionadas con el Ethical Hacking (perdón, pero hablar de la calidad del código era necesario antes de seguir escribiendo nuevas funcionalidades).

hacking python