quinta-feira, 21 de dezembro de 2017

[Python] Banner – quick n' dirty or not?

Já faz um bom tempo que deixo o Python de canto e mantenho minha escolha conservadora por criar determinado código em C, se for para automatizar, utilizo o bash. Porém, nos últimos momentos, optei por escrever um script em Python para automatizar minhas tarefas. O que me fez mudar de costume foi por eu estar trabalhando em um projeto que é totalmente escrito em Python.

Após criar a automatização, trabalhei para potencializar quaisquer tarefas desejadas. Portanto, estava deixando o script um pouco mais agradável e, ao incluir um banner, deparei-me com algo que deu conflito um dos meus "toques".

Exemplo 1 - maneira ridícula e preguiçosa!

Veja abaixo um script como exemplo:
#!/usr/bin/python
# coding: utf-8

import argparse

class init:
    def banner(self):
        self.asciiart="""
              __
     /|
    / \\
    \\  \\
    }]::)=={) *     *   *
    /  /  aim for your goal
    \\ /   and let me change 
     \\|__   the destiny
        """
        parser = argparse.ArgumentParser(self.asciiart)
        parser.add_argument('-e', '--example', required=False, help='This is a just fucking example')

        parser.parse_args()

    if __name__ == '__main__':
        start = init()
        start.banner()

Considerando o script acima, o output seria este:
$ python mercy.py -h
usage: 
        __
      /|
     / \
     \  \
     }]::)=={) *     *   *
     /  /  aim for your goal
     \ /   and let me change 
      \|__   the destiny
  
       [-h] [-e EXAMPLE]

optional arguments:
  -h, --help            show this help message and exit
  -e EXAMPLE, --example EXAMPLE
                        This is a just fucking example

Perceba que houve um "usage:" acima do banner. Isso, REALMENTE, me incomoda muito!
Incomoda ainda mais quando inserir um argumento inválido, porque o script irá imprimir o banner duas vezes! Porque o banner está passando por um método argparse.ArgumentParser() -, esse método é o entry point para a manipulação dos argumentos passados na linha de comando, não é à toa que é usado o método "add_argument()" para expandir seu comportamento. Logo depois é chamado o método parser.parse_args(), o responsável por analisar os argumentos válidos através do método "parse_known_args()" coletando através do velho truque:
if args is None:
    args = _sys_argv[1:]"
else:
    args = list(args)
A rotina para detectar os argumentos obrigatórios é responsabilidade de outro método chamado "consume_positionals()", mas ele não nos vêm ao caso agora.

Uma maneira simples para corrigir essa bagunça é utilizando uma classe que é feita para formatar a saída da mensagem de help, formatter_class:
#!/usr/bin/python
# coding: utf-8

import argparse

ascii_art="""
        __
      /|
     / \\
     \\  \\
     }]::)=={) *     *   *
     /  /  aim for your goal
     \\ /   and let me change 
      \\|__   the destiny
"""
def banner():
 parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
     description=ascii_art)
 parser.add_argument('-e', '--example', required=False, help='This is a just fucking example')

 parser.parse_args()

if __name__ == '__main__':
 banner()
Mesmo que você configure o argumento para "required=True", a saída não irá imprimir o banner.
Porém, mesmo fazendo essa modificação, repare no resultado do output abaixo, que o "usage:" ainda permanece no topo:
$ python mercy_modified.py -h
usage: mercy_modified.py [-h] -e EXAMPLE

        __
      /|
     / \
     \  \
     }]::)=={) *     *   *
     /  /  aim for your goal
     \ /   and let me change 
      \|__   the destiny

optional arguments:
  -h, --help            show this help message and exit
  -e EXAMPLE, --example EXAMPLE
                        This is a just fucking example


Tempos atrás quando eu ainda usava Python para automatizar minhas tarefas, eu já havia me deparado com essa confusão, foi aí que eu tenho procurado alguns projetos escritos em Python e a melhor maneira que eu tenho visto na época foi o método do sqlmap. No arquivo "/lib/core/settings.py" é declarado uma variável chamado BANNER, onde ele trata as informações e o estilo (cores) de como será impresso; No arquivo "/lib/core/common.py", faz a importação da variável BANNER.

Exemplo 2 - Semelhante ao projeto sqlmap

Para entender melhor, veja o código abaixo:
$ mkdir lib
$ >lib/__init__.py
$ cat lib/asciiart.py
ASCII_ART="""
                      __
      /|
     / \\
     \\  \\
     }]::)=={) *     *   *
     /  /  aim for your goal
     \\ /   and let me change 
      \\|__   the destiny
"""
$ cat mercy2.py
#!/usr/bin/python
# coding: utf-8

import argparse
import sys
from lib.asciiart import ASCII_ART

def banner():
 print ASCII_ART
 parser = argparse.ArgumentParser()
 parser.add_argument('-e', '--example', required=False, help='This is a just fucking example')

 parser.parse_args()
 if len(sys.argv) < 2:
  parser.print_help()
  return -1

if __name__ == '__main__':
 banner()

Ao executar o script [mercy2.py] acima, lidamos com o seguinte resultado:
$ python mercy2.py -h
        __
      /|
     / \\
     \\  \\
     }]::)=={) *     *   *
     /  /  aim for your goal
     \\ /   and let me change 
      \\|__   the destiny

usage: mercy2.py [-h] [-e EXAMPLE]

optional arguments:
  -h, --help            show this help message and exit
  -e EXAMPLE, --example EXAMPLE
                        This is a just fucking example

Examplo 3 - banner estático

Outro método semelhante, porém, também, acho insatisfatório por razões de ser completamente estático:
$ cat ./startrek
        __
      /|
     / \\
     \\  \\
     }]::)=={) *     *   *
     /  /  aim for your goal
     \\ /   and let me change 
      \\|__   the destiny
$ mkdir lib
$ >lib/__init__.py
$ cat lib/asciiart.py
def asciiart():
    with open('startrek', 'r') as fd:
        print(fd.read())
$ cat mercy3.py
#!/usr/bin/python
# coding: utf-8

import argparse
from lib import asciiart

def banner():
 parser = argparse.ArgumentParser(description=asciiart.asciiart())
 parser.add_argument('-e', '--example', required=False, help='This is a just fucking example')

 parser.parse_args()

if __name__ == '__main__':
 banner()

Este é o modo em que o banner tem que ser apenas um simples banner. Se houvesse informações sobre o script, tal como a versão, será chato fazer a mudança, até porque ele está em um arquivo separado. Por este motivo, a maneira sem "quick dirty" para imprimir banner em uma ferramenta, utilizando a Standard Library – Ok, eu sei que imprimir banner está fora do codestyle Python Enhancement Proposals – é utilizando o método semelhante ao sqlmap. Embora, você também possa criar seu próprio analisador de argumentos passados na linha de comando.


Refer:
[1] sqlmap - importando a variável banner
https://github.com/sqlmapproject/sqlmap/blob/master/lib/core/common.py#L96


Nenhum comentário:

Postar um comentário