Как создавать и публиковать консольные приложения на Python



Книга Как создавать и публиковать консольные приложения на Python

Подробное руководство по созданию и публикации консольных приложений на Python


Консольные приложения — это те, которые вы запускаете в терминале. Скорее всего, вы уже пытались их создать. Или, по крайней мере, думали об их создании.


Но создание консольного приложения — это одно, а публикация его в репозиторий с открытым кодом (например, PyPI) — совсем другое. Хотя ни первое, ни второе не является чем-то запредельно трудным.


В этой статье я подробно расскажу, как можно создать простой CLI на Python и опубликовать его в PyPI.


Начало


Не так давно я занялся изучением уязвимостей open-source кода и понял, что хочу иметь в арсенале инструмент командной строки, который мог бы находить уязвимости напрямую из терминала. Уязвимости open-source кода обычно публикуются в открытых базах данных. Их можно найти на таких сайтах, как CVE, NVD, WhiteSource Vuln и т.д.


В этой статье мы создадим примитивный скрейпер для поиска и просмотра уязвимостей с сайта CVE, обернем его в простое консольное приложение и опубликуем на PyPI.


Для начала нужно настроить среду разработки и установить необходимые модули. Я предлагаю установить виртуальную среду. Это простое решение, которое позволит избежать конфликтов между версиями модулей.


Для создания виртуальной среды можно воспользоваться командой python -m venv <path/name> (для Python 3) либо установить virtualenvwrapper с помощью pip install virtualenvwrapper и создать виртуальную среду virtualenv через mkvirtualenv -p /path/topython <path/name>.


После того, как виртуальная среда настроена и активирована, вы можете создать папку проекта и установить необходимые модули:


mkvirtualenv -p /usr/bin/python cvecli-env
mkdir cvecli && cd cvecli
mkdir cver && touch setup.py && touch README.md && touch cver/__init__.py && touch .gitignore
pip install requests beautifulsoup4 lxml twine click
pip freeze > requirements.txt

Как только все успешно запустилось, откройте свой проект в любом редакторе кода. Вы увидите похожую структуру:



Создание веб-скрейпера


Для того, чтобы искать и просматривать уязвимости на сайте CVE, потребуется веб-скрейпер. Он поможет нам собирать информацию об уязвимостях. Мы создаем скрейпер в Requests и BeautifulSoup. Вот что будет делать наш скрейпер:


1. искать уязвимости;


2. получать информацию об уязвимости по ее названию на CVE.


Теперь откроем папку cver и создадим в ней файл под названием cve_scraper. Затем пропишем его базовые настройки:


import requests
from bs4 from BeautifulSoup

SEARCH_URL = "https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword="

CVE_URL = "https://cve.mitre.org/cgi-bin/cvename.cgi?name="

def get_html(url):
request = requests.get(url)
if request.status_code == 200:
return request.content
else:
raise Exception("Bad request")


def search(s):
pass

def lookup_cve(name):
pass

Поиск уязвимостей


Для поиска уязвимостей на CVE используется URL в следующем формате: https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=<ключевое_слово>. Такой формат позволяет извлекать список уязвимостей, соответствующих ключевому слову.


Например, через URL можно получить список всех уязвимостей, связанных с Python:



Для извлечения данных открываем инструменты разработчика (Developer Console) и исследуем DOM-элемент с нужным представлением. Для этого кликните правой кнопкой по любому месту на странице и выберите “исследовать элемент” (inspect element) либо нажмите Ctrl + F12.



Если вы присмотритесь к DOM-структуре выше, то увидите, что результаты представлены в виде таблицы, а каждое значение указано в отдельной строке под таблицей. Такие данные можно запросто извлечь:


def search(s):    

url = f"{SEARCH_URL}{s}"
results=[]
html = get_html(url)
soup = BeautifulSoup(html, "lxml")
result_rows = soup.select("#TableWithRules table tr")

for row in result_rows:
_row = {}
name = row.select_one("td a")
description = row.select_one("td:nth-child(2)")

if all([name, description]):

_row["name"] = name.text
_row["url"] = name.get("href")
_row["description"] = description.text

results.append(_row)

return results

В коде выше мы:


1. отправляем запрос в SEARCH_URL с помощью Requests и получаем DOM-содержимое;


2. преобразуем DOM-содержимое в объекты BeautifulSoup. Это позволит нам выделять DOM-элементы с помощью CSS-селекторов, XPATH и других методов;


3. выделяем все tr под таблицей #TableWithRules. Выделяем первый столбец строки в качестве названия, а в качестве описания берем второй. Затем извлекаем текст.


Просмотр информации об уязвимостях


Чтобы просмотреть информацию об уязвимости, нужно взять ее CVE-ID и передать по этому адресу: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-ID.



Откройте инструменты разработчика и исследуйте DOM-структуру.



Такая структура чуть сложнее, поскольку в строках таблицы отсутствует ID или название класса. Поэтому нам нужно пройтись циклом по каждой строке и проверить, не является ли она подзаголовком. Если да, то следующий элемент берется в качестве содержимого-потомка. Каждый подзаголовок отображается в th, а его содержимое — в td.


def lookup_cve(name):
url = f"{CVE_URL}{name}"
html = get_html(url)
soup = BeautifulSoup(html, "lxml")
result_rows = soup.select("#GeneratedTable table tr")

subtitle = ""
description = ""

raw_results = {}

for row in result_rows:
head = row.select_one("th")
if head:
subtitle = head.text
else:
body = row.select_one("td")
description = body.text.strip().strip("
")
raw_results[subtitle.lower()] = description

return raw_results


Готово! Мы успешно создали веб-скрейпер с CVE. Теперь добавим в него две функции (search и lookup_sve), которые будут искать уязвимости и получать информацию по ним через CVE-ID.


import requests
from bs4 import BeautifulSoup

SEARCH_URL = "https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword="
CVE_URL = "https://cve.mitre.org/cgi-bin/cvename.cgi?name="


def get_html(url):
request = requests.get(url)
if request.status_code == 200:
return request.content
else:
raise Exception("Bad request")


def search(s):
url = f"{SEARCH_URL}{s}"
results=[]
html = get_html(url)
soup = BeautifulSoup(html, "lxml")
result_rows = soup.select("#TableWithRules table tr")


for row in result_rows:
_row = {}
name = row.select_one("td a")
description = row.select_one("td:nth-child(2)")
if all([name, description]):

_row["name"] = name.text
_row["url"] = name.get("href")
_row["description"] = description.text

results.append(_row)

return results


def lookup_cve(name):
url = f"{CVE_URL}{name}"
html = get_html(url)
soup = BeautifulSoup(html, "lxml")
result_rows = soup.select("#GeneratedTable table tr")

subtitle = ""
description = ""

raw_results = {}

for row in result_rows:
head = row.select_one("th")
if head:
subtitle = head.text
else:
body = row.select_one("td")
description = body.text.strip().strip("
")
raw_results[subtitle.lower()] = description

return raw_results

Создание консольного приложения


Наш следующий шаг — структурирование и создание консольного приложения через библиотеку Click.


Click — это Python-пакет для создания красивых интерфейсов командной строки с минимальным количеством кода и возможностью компоновки. Это один из лучших Python-пакетов для создания CLI, и с ним очень удобно работать.


Click позволяет создавать интерфейсы командной строки любого уровня — от самых простых до навороченных (например, Heroku).


В нашем CLI мы реализуем две команды:


1. поиск уязвимости;


2. просмотр уязвимости.


В папке cver создаем файл под названием __main__.py и прописываем его базовые настройки:


import sys
import click

@click.group()
@click.version_option("1.0.0")
def main():
"""A CVE Search and Lookup CLI"""
print("Hye")
pass

@main.command()
@click.argument('keyword', required=False)
def search(**kwargs):
"""Search through CVE Database for vulnerabilities"""
click.echo(kwargs)
pass

@main.command()
@click.argument('name', required=False)
def look_up(**kwargs):
"""Get vulnerability details using its CVE-ID on CVE Database"""
click.echo(kwargs)
pass

if __name__ == '__main__':
args = sys.argv
if "--help" in args or len(args) == 1:
print("CVE")
main()

Поиск уязвимостей


Здесь мы будем импортировать функцию поиска search из веб-скрейпера и передавать ей аргумент keyword из командной строки. Таким образом, приложение будет искать уязвимости, совпадающие с ключевым словом:


from scraper import search as cve_search, lookup_cve

@main.command()
@click.argument('keyword', required=False)
def search(**kwargs):
"""Search through CVE Database for vulnerabilities"""
results = cve_search(kwargs.get("keyword"))
for res in results:
click.echo(f'{res["name"]} - {res["url"]}
{res["description"]}')

Для запуска этой команды:


python cver/__main__.py search python


Просмотр уязвимости


Принцип тот же: используем lookup_cve из веб-скрейпера и передаем туда аргумент name из команды look_up.


@main.command()
@click.argument('name', required=False)
def look_up(**kwargs):
"""Get vulnerability details using its CVE-ID on CVE Database"""
details = lookup_cve(kwargs.get("name"))
click.echo(f'CVE-ID

{details["cve-id"]}
')
click.echo(f'Description

{details["description"]}
')
click.echo(f'References

{details["references"]}
')
click.echo(f'Assigning CNA

{details["assigning cna"]}
')
click.echo(f'Date Entry

{details["date entry created"]}')

Для запуска этой команды:


python cver/__main__.py look-up CVE-2013–4238


Готово! Мы успешно создали инструмент командной строки по поиску с CVE.


Публикация консольного приложения на PyPI


После того, как мы создали консольное приложение и убедились в его работоспособности, можно опубликовать его на PyPI в публичном доступе.


PyPI — это хранилище приложений для пакетов Python. Там можно найти практически все пакеты, которые устанавливаются через pip. Для публикации пакета потребуется аккаунт на PyPI. Если он у вас уже есть, то смело читайте дальше.


Настройка пакета


Следующий шаг — это настройка Python-пакета с помощью setup.py. Если вы хотите, чтобы ваш пакет попал на PyPI, то нужно снабдить его базовым описанием. Эта информация указывается в файле setup.py.


Откройте setup.py в основной директории проекта и поместите в начало файла следующий код:


from setuptools import setup, find_packages
from io import open
from os import path

import pathlib
# Директория, в которой содержится этот файл
HERE = pathlib.Path(__file__).parent

# Текст README-файла
README = (HERE / "README.md").read_text()

# Автоматически собирает в requirements.txt все модули для install_requires, а также настраивает ссылки на зависимости
with open(path.join(HERE, 'requirements.txt'), encoding='utf-8') as f:
all_reqs = f.read().split('
')

install_requires = [x.strip() for x in all_reqs if ('git+' not in x) and (
not x.startswith('#')) and (not x.startswith('-'))]
dependency_links = [x.strip().replace('git+', '') for x in all_reqs \
if 'git+' not in x]

В примере выше мы преобразовали содержимое файла README.md в одну строку для дальнейшего использования. Кроме того, мы перечислили все необходимые модули из requirements.txt и сгенерировали ссылки на их зависимости.


Ваш файл requirements.txt выглядит примерно так:


click
requests
beautifulsoup4
lxml
twine

Теперь давайте рассмотрим параметры настроек:


setup (
name = 'cver',
description = 'A simple commandline app for searching and looking up opensource vulnerabilities',
version = '1.0.0',
packages = find_packages(), # list of all packages
install_requires = install_requires,
python_requires='>=2.7', # any python greater than 2.7
entry_points='''
[console_scripts]
cver=cver.__main__:main
''',
author="Oyetoke Toby",
keyword="cve, vuln, vulnerabilities, security, nvd",
long_description=README,
long_description_content_type="text/markdown",
license='MIT',
url='https://github.com/CITGuru/cver',

download_url='https://github.com/CITGuru/cver/archive/1.0.0.tar.gz',
dependency_links=dependency_links,
author_email='[email protected]',
classifiers=[
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
]
)

В коде выше мы добавили множество строк. Но подробнее обсудим только те из них, которые нужны для установки.


1. name — название пакета, которое появится на PyPI;


2. version — текущая версия пакета;


3. packages — пакеты и подпакеты с исходным кодом. В ходе установки мы пользуемся модулем find_packages. Он автоматически находит все подпакеты;


4. install_requires — используется для перечисления всех зависимостей или сторонних библиотек пакета. В cver мы пользуемся Requests, Beautifulsoup 4 и Click. Их нужно включить в требования к установке install_requires. Нам не нужно добавлять эту информацию вручную, поскольку она присутствует в requirements.txt;


5. entry_points — используется для создания скриптов, которые вызывают функцию внутри пакета. В данном случае мы создаем новый скрипт cver, который вызывает main() внутри файла cver/__main__.py. Наш основной элемент — это __main__.py, который вызывает функцию main() для запуска Click.


До того, как опубликовать пакет на PyPI или выложить в открытый доступ, необходимо снабдить его документацией. То, как будет выглядеть документация, целиком и полностью зависит от самого проекта. Это может быть как простой файл README.md, так и Readme.rst.


Пример хорошо оформленного README.md:


# CVER

A simple commandline app for searching and looking up opensource vulnerabilities

# Установка

## Через Pip

```bash
$ pip install cver
```

## Вручную

```bash
$ git clone https://github.com/citguru/cevr
$ cd cver
$ python setup.py install
```
# Использование

```bash
$ cver
```

## Поиск
`search <keyword>`

```bash
$ cver search python
```
## Просмотр

`search <name>`

```bash
$ cver look-up CVE-2020-2121
```

Кроме того, не забудьте создать файл .gitignore:


# Байтовая компиляция / оптимизация / DLL-файлы
__pycache__/
*.py[cod]
*$py.class

# C-расширения
*.so

# Сборка дистрибутива / пакета
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Обычно такие файлы пишутся Python-скриптом по шаблону
# до того, как PyInstaller создаст exe. Это нужно для добавления в файл даты и прочей информации.
*.manifest
*.spec

# Логи установщика
pip-log.txt
pip-delete-this-directory.txt

# Модульные тесты / отчеты по покрытию
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Переводы
*.mo
*.pot

# Всякое на Django:
*.log
local_settings.py
db.sqlite3

# Всякое на Flask:
instance/
.webassets-cache

# Всякое на Scrapy:
.scrapy

# Sphinx-документация
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# Schedule-файл Celery Beat
celerybeat-schedule

# Проанализированные файлы SageMath
*.sage.py

# Окружения
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Настройки Spyder Project
.spyderproject
.spyproject

# Настройки Rope Project
.ropeproject

# Документация mkdocs
/site

# mypy
.mypy_cache/

Вот и все.


from setuptools import setup, find_packages
from io import open
from os import path

import pathlib
# Директория, в которой содержится этот файл
HERE = pathlib.Path(__file__).parent

# Текст README-файла
README = (HERE / "README.md").read_text()

# Автоматически собирает в requirements.txt все модули для install_requires
with open(path.join(HERE, 'requirements.txt'), encoding='utf-8') as f:
all_reqs = f.read().split('
')

install_requires = [x.strip() for x in all_reqs if ('git+' not in x) and (
not x.startswith('#')) and (not x.startswith('-'))]
dependency_links = [x.strip().replace('git+', '') for x in all_reqs \
if 'git+' not in x]
setup (
name = 'cver',
description = 'A simple commandline app for searching and looking up opensource vulnerabilities',
version = '1.0.0',
packages = find_packages(), # list of all packages
install_requires = install_requires,
python_requires='>=2.7', # any python greater than 2.7
entry_points='''
[console_scripts]
cver=cver.__main__:main
''',
author="Oyetoke Toby",
keyword="cve, vuln, vulnerabilities, security, nvd",
long_description=README,
long_description_content_type="text/markdown",
license='MIT',
url='https://github.com/CITGuru/cver',
download_url='https://github.com/CITGuru/cver/archive/1.0.0.tar.gz',
dependency_links=dependency_links,
author_email='[email protected]',
classifiers=[
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
]
)

Публикация на PyPI


Теперь ваш пакет готов к публикации на PyPI. Еще раз проверьте, есть ли у вас уже аккаунт на PyPI. Добавьте к нему тестовый аккаунт на тестовом сервере PyPI. Этот аккаунт нужен для тестирования пакетов перед публикацией их на рабочем сервере.


Загружать Python-пакеты на PyPI мы будем с помощью специального инструмента — Twine. По идее, вы уже установили его на одном из предыдущих этапов. Если нет, то это можно сделать через pip install twine.


Создание и локальное тестирование пакета на тестовом сервере


Python-пакеты, опубликованные на PyPI, не распространяются в виде «голого» кода. Они оборачиваются в дистрибутивы. Самыми распространенными форматами дистрибутивов в Python являются Wheels и Source Archives.


Wheels — это zip-архив с кодом и готовыми расширениями. Source Archives содержит исходный код и вспомогательные файлы, упакованные в tar-архив.


Для локального тестирования пакета выполните:


python setup.py install

Теперь мы можем использовать его как:


cver search python

Для проверки пакета на тестовом сервере PyPI нужно сгенерировать сборку для локального тестирования. При создании этой сборки сгенерируются архивы Wheels и Source Archives.


Создание сборки:


python setup.py sdist bdist_wheel

Код ниже сгенерирует два файла в директории dist:


cvecli/

└── dist/
├── cver-1.0.0-py3-none-any.whl
└── cver-1.0.0.tar.gz

Затем воспользуемся Twine. Теперь мы можем загрузить пакет на тестовый сервер PyPI:


twine upload — repository-url https://test.pypi.org/legacy/ dist/*

Затем у вас спросят логин и пароль.



Если пакет загружается на тестовый сервер без ошибок, то он готов к публикации на рабочем сервере. Проверить можно здесь.


Для установки из TestPyPI выполните следующую команду:


pip install -i https://test.pypi.org/simple/ cver==1.0.0

В тестовой среде вы можете проверить все команды и убедиться в их правильности перед публикацией пакета на рабочем сервере.


Протестировав все команды локально, переходите к публикации пакета на рабочем сервере:


twine upload dist/*

В процессе загрузки укажите свой логин и пароль. Вот и все!



Теперь пакет можно установить через:


pip install cver

Поздравляю! Ваш пакет был опубликован на PyPI. Просмотреть его можно здесь!


Заключение


В этой статье я пошагово объяснил процесс создания и публикации консольного приложения на Python.


500   0  

Comments

    Ничего не найдено.