
Ручное сканирование уязвимостей — это нормально, когда у вас десяток адресов и море свободного времени. Но если адресов сотни, а сканировать их нужно регулярно, процесс быстро превращается в рутину. Забыли запустить скан? Потеряли часть результатов? Результаты есть, но никто о них не узнал? Решение простое — автоматизация.
Меня зовут Антон, я инженер по информационной безопасности в Selectel. В тексте расскажу, как настроить скрипт, который через API «Сканер-ВС 6» возьмет все под контроль: сам запустит сканирование, создаст отчеты и отправит уведомление в Telegram. Все по расписанию через cron, без ручных запусков.
Используйте навигацию, если не хотите читать текст полностью:
→ Знакомство с инструментом
→ Создание сервера
→ Разворачивание сканера
→ Автоматизация сканирований
→ Шаг 0. Авторизация в сканере
→ Шаг 1. Проверка обновления баз уязвимостей
→ Шаг 2. Создание задач на исследование сети
→ Шаг 3. Создание Assets
→ Шаг 4. Создание задач на сканирование уязвимостей
→ Шаг 5. Генерация отчетов о сканировании
→ Шаг 6. Оповещение через Telegram-бота
→ Завершающий шаг
Знакомство с инструментом
Для сканирования уязвимостей мы используем «Сканер-ВС 6» – универсальный инструмент для решения широкого спектра задач по тестированию и анализу защищенности информационных систем, а также контроля эффективности средств защиты информации. Рассмотрим ключевые возможности сканера.
- Анализ безопасности конфигурации ОС: проверка базовых настроек безопасности.
- Удобное управление информационными активами: сетевое сканирование хостов, построение карт сети, инвентаризация установленного ПО.
- Выявление уязвимостей по версиям установленного ПО, ежедневно обновляемая база уязвимостей, включающая данные из БДУ ФСТЭК России, NIST NVD, и др.
- Подбор паролей к сетевым сервисам: поддержка протоколов ftp, imap, imaps, mssql, mysql, pop3, pop3s, postgres, rdp, redis, и др.
- Интеграция с внешними системами: отправка событий в SIEM-системы, открытый API.

Создание сервера
Для работы со сканером нам нужно настроить сервер с ОС Astra Linux.
1. В панели управления Selectel перейдем в раздел Продукты → Облачные серверы.

3. Во вкладке Серверы нажимаем кнопку Создать сервер.

4. Собираем сервер по минимальным системным требованиям решения «Сканер-ВС 6».

В качестве источника выбираем Astra Linux Орел 1.7. Конфигурацию выбираем в соответствии с таблицей выше.

Выбор источника в панели управления.

Конфигурация сервера.
5. В поле Сеть выберем Публичная подсеть и размер подсети /29 (5 адресов IPv4). Они нам пригодятся в дальнейшем для создания серверов,, которые будем сканировать.

6. Добавляем SSH-ключ в разделе Доступ. Сгенерировать пару SSH-ключей можно с помощью команды ssh-keygen -t ed25519 в терминале. Подробн��е о создании — в инструкции.

7. Проверяем конфигурацию и стоимость, нажимаем Создать сервер.

Разворачивание сканера
1. С официального сайта скачиваем демоверсию с лицензией на 16 IP-адресов (активов).

Скриншот официального сайта, заполнение данных для получения демоверсии.

Скриншот официального сайта, выбор варианта исполнения и загрузка файла.
2. Копируем файлы со своей машины на созданный сервер:
#scp -i C:\Users\user\.ssh\ssh.txt C:\Users\user\Downloads\license.lic C:\Users\user\.ssh\ssh.txt C:\Users\user\Downloads\scanner-signed.run astra@ip_addr:/home/astra
По приватному ключу заходим на сервер:
# ssh -i [путь к файлу с ключом] astra@[ваш адрес]
3. Переходим в режим суперпользователя:
#astra@taliyah:~$ sudo su #Password: #root@taliyah:/home/astra#
4. Распаковываем архив:
#root@taliyah:/home/astra# sh scanner-signed.run
5. Копируем файл с лицензией в папку:
#cp license.lic pkg
6. Проверяем, что все пакеты в наличии:
/home/astra/pkg# ls

7. Запускаем инсталлер:
#root@taliyah:/home/astra/pkg# ./installer install
7.1. Устанавливаем сканер с дефолтными настройками.


7.3. Выбираем интерфейс, с которого будем обращаться к веб версии сканера.

8. Сканер успешно установлен на ОС.

9. Заходим в веб-версию сканера по адресу
https://ip_addr и авторизуемся с данными admin/admin. Со��етуем сразу сменить пароль на более надежный.
Страница аутентификации.

Главная страница инструмента.
10. Проверяем наличие лицензии в разделе О программе.

Инструмент развернут, теперь у мы можем проводить сканирования. Однако этого недостаточно, чтобы автоматизировать процесс.
Автоматизация сканирований

Перейдем к основным этапам автоматизации.

В тексте покажем примеры кода для каждого из этапов — не целиком, а фрагментами. Так вам будет проще понять логику работы с API, но останется место для своих доработок.
Шаг 0. Авторизация в сканере
При каждом обращении к сканеру по API нужно авторизовываться. Однако для упрощения процесса можно сохранять сессионные cookies, которые и будут использоваться во всех последующих запросах. Для этого создадим curl-файл:
#nano curl #!/bin/bash curl --insecure -X "POST" -d '{"Login":"admin","Password":"password"}' -c ./cookie.txt 'https://[ip_addr]/login'
Рассмотрим основные этапы, по которым работает скрипт.
- Отправляет HTTP-запрос методом POST на URL
https://ip_addr/login. В теле запроса — JSON-данные с логином (admin) и паролем (password). - Сохраняет полученные от сервера cookies в файл
cookie.txt. - Игнорирует проверку SSL-сертификата (
--insecure), что удобно для тестовой среды.
К curl-файлу создадим пустой
cookie.txt, куда будут сохраняться сессионные cookies:#touch cookie.txt
Часть кода, которая отвечает за авторизацию:
# Путь к файлу с curl-запросом curl_file = "/ваш/путь/до/файла/curl" def authorization(): # Отправляем POST-запрос с логином и паролем os.system('bash ' + curl_file) # Читаем cookies из файла with open(cookie_file, 'r') as file: content = file.read() # Извлекаем токен сессии из cookies return 'SessionToken=' + re.findall(uuid_pattern, content)[0]
Шаг 1. Проверка обновления баз уязвимостей
1. Для проверки и обновления базы уязвимостей обратимся к соответствующему разделу в Swagger-документации — update-control.

2. Найдем Get-запрос на загрузку новых обновлений и выполним его.

Из примера ниже видим, по какому адресу нужно отправить Get-запрос:
https://ip_addr/api/v1/update/auto.curl -X 'GET' \ 'https://ip_addr/api/v1/update/auto' \ -H 'accept: application/json'
Соберем готовую функцию:
def base_update(auth_cookies): if not auth_cookies: print("Нет cookies, обновление невозможно!") return headers = {'Cookie': auth_cookies} response = session.get(update_url, headers=headers) if response.status_code == 200: print("Базы уязвимостей обновлены") else: print(f"Ошибка при обновлении баз. Код: {response.status_code}, Текст: {response.text}")
Как работает функция
- Проверяет, переданы ли cookies. Если нет — выводит сообщение об ошибке и завершает выполнение.
- Отправляет GET-запрос на URL, используя cookies для авторизации.
- Проверяет статус-код ответа. Если статус 200, то выводит сообщение об успешном обновлении баз, иначе — выводит сообщение об ошибке с кодом и текстом ответа.

Пример вывода функции.
Все URL и местонахождение файлов прописываются в начале кода. Пример настройки переменных для запроса:
url = 'https://ip_addr/' login_url = url + 'app/' update_url = url + 'api/v1/update/auto' curl_file = '/ваш/путь/до/файла/curl’' cookie_file = '/ваш/путь/до/файла/cookie.txt'
Шаг 2. Создание задач на исследование сети
После обновления баз можно переходить к сканированию сети. Для этого нужно собрать список IP-адресов.
1. Для хранения IP создадим файл iplist.json:
#nano iplist.json
2. Заполним файл адресами:
[ {"id": "1", "name": "имя хоста", "ip": "адрес хоста"}, ]
3. Для открытия и чтения файла создаем функцию:
def load_ip_list(file_path): try: with open(file_path, 'r', encoding='utf-8') as file: return json.load(file) except FileNotFoundError: print(f"Файл {file_path} не найден.") return [] except json.JSONDecodeError: print(f"Ошибка при чтении JSON из файла {file_path}. Возвращаем пустой список.") return []
Как работает функция
- Принимает путь к файлу, пытается его открыть и прочитать как JSON.
- Если файл успешно прочитан — возвращает данные в виде списка.
- Если файл не найден или содержит некорректный JSON, то выводит сообщение об ошибке и возвращает пустой список.
Шаг 3. Создание Assets
По IP-адресам будут создаваться assets – активы, которые будут нам нужны для сканирования.
1. Во вкладке Asset Control в Swagger ищем post-запрос, чтобы переделать его в Python.

2. Выполняем запрос, получаем сниппет:
curl -X 'POST' \ 'https://ip_addr/api/v1/asset' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "assetInfo": { "name": "нужно задавать название asset", # Указываем название актива "importanceType": "IMPORTANCE_TYPE_UNSPECIFIED" # Оставляем без изменений }, "network": { "ipv4": "string" # Оставляем только IPv4, убираем fqdn и ipv6 }, "os": { "type": "OPERATING_SYSTEM_TYPE_UNSPECIFIED" # Оставляем без изменений }, "device": { "type": "DEVICE_TYPE_UNSPECIFIED" # Оставляем без изменений }, "tags": [ 0 ] }'
3. Отредактируем поля с комментариями и получим следующий вид:
data = { "assetInfo": { "name": f"Asset for {ip_ip}", "importanceType": "IMPORTANCE_TYPE_UNSPECIFIED" }, "network": { "ipv4": ip_ip } }
4. После отправки POST-запроса проверим его успешность:
try: asset_data = response.json() if "id" in asset_data: asset_id = asset_data["id"] print(f"Asset создан для IP: {ip_ip}, ID Asset: {asset_id}") return asset_id else: print(f"Ошибка при разборе ответа API для asset IP: {ip_ip}. Поле 'id' не найдено.") print(f"Ответ API: {asset_data}")
5. Соберем все в функцию:
def create_asset(auth_cookies, ip_ip): if not auth_cookies: print("Нет cookies, создание asset невозможно!") return None headers = { 'accept': 'application/json', 'Content-Type': 'application/json', 'Cookie': auth_cookies } data = { "assetInfo": { "name": f"Asset for {ip_ip}", "importanceType": "IMPORTANCE_TYPE_UNSPECIFIED" }, "network": { "ipv4": ip_ip } } response = session.post(asset_url, headers=headers, json=data, verify=False) if response.status_code in [200, 201]: try: asset_data = response.json() if "id" in asset_data: asset_id = asset_data["id"] print(f"Asset создан для IP: {ip_ip}, ID Asset: {asset_id}") return asset_id else: print(f"Ошибка при разборе ответа API для asset IP: {ip_ip}. Поле 'id' не найдено.") print(f"Ответ API: {asset_data}") except (KeyError, ValueError) as e: print(f"Ошибка при разборе ответа API для asset IP: {ip_ip}: {e}") else: print(f"Ошибка при создании asset для IP: {ip_ip}. Код: {response.status_code}, Текст: {response.text}") return None
Функция проверяет, переданы ли cookies. Далее — формирует заголовки и данные для POST-запроса, отправляет его на asset_url для создания asset и проверяет статус-код ответа:
- Если статус-код 200 или 201 — пытается извлечь id созданного asset из ответа;
- Если статус-код иной — выводит сообщение об ошибке и возвращает id созданного asset или None.

Пример вывода программы.
6. Будем сохранять id asset в наш файл с адресами. Для этого используем функцию:
def save_ip_list(file_path, ips): try: for ip in ips: if "id_asset" in ip and isinstance(ip["id_asset"], int): ip["id_asset"] = str(ip["id_asset"]) with open(file_path, 'w', encoding='utf-8') as file: json.dump(ips, file, indent=4, ensure_ascii=False) print(f"Список адресов успешно сохранен в файл: {file_path}") except Exception as e: print(f"Ошибка при сохранении списка адресов: {e}")
Теперь программа будет сохранять обновленный список с новой строкой id_asset в iplists.json:
[ {"id": "1", "name": "имя хоста", "ip": "адрес хоста", “id_asset”:”100”}, ]
Шаг 4. Создание задач на сканирование уязвимостей
Приступим к созданию задач на сканирование. Для этого посмотрим, как формируется запрос в Swagger.
1. Выполняем его и получаем снипет.

2. Снипет получается довольно обширный, c разными задачами, поэтому выберем только поля, которые нужны для
netscanOptions:"netscanOptions": { "ping": True, # Включение пинга для проверки доступности хостов "traceRoute": True, # Включение трассировки для топологии "osDetection": True, # Определение операционной системы хостов "aggressive": False, # Отключение агрессивного режима сканирования "targets": { "targets": ip_ips, # Целевые IP-адреса для сканирования "exclusionTargets": [] # Исключения (если необходимо) }, "port": { "enable": True, # Включение сканирования портов "tcpPorts": all_ports, # Сканируем только TCP-порты "udpPorts": [], # UDP-порты не сканируются "exclusionPorts": [], # Исключения для портов (если необходимо) "mostCommonPortsNumber": 0 # Без ограничения по количеству портов }, "service": { "enable": True, # Включение сканирования сервисов "intensity": 7 # Интенсивность сканирования, по умолчанию 7 }, "network": { "enable": False, # Не включаем сканирование сети — обязательное поле "proxies": [], # Прокси-серверы, если необходимо "sourcePort": 1, # Исходный порт для сетевых операций "interface": "" # Сетевой интерфейс (если необходимо) }, "scanPolicy": { "enable": False, # Политика сканирования отключена — обязательное поле "minHostgroup": 0, # Минимальное количество хостов в группе "maxHostgroup": 0, # Максимальное количество хостов в группе "minRate": 1, # Минимальная скорость сканирования "maxRate": 100 # Максимальная скорость сканирования } }
Теперь рассмотрим функцию:
def add_netscan_task(auth_cookies, ip_name, ip_ips): if not auth_cookies: print("Нет cookies, создание задачи невозможно!") return headers = { 'Content-Type': 'application/json', 'Cookie': auth_cookies } all_ports = list(range(1, 65536)) data = { "name": f"Netscan for {ip_name}", "type": "TYPE_NETSCAN", "netscanOptions": { "ping": True, "traceRoute": True, "osDetection": True, "aggressive": False, "targets": { "targets": ip_ips, "exclusionTargets": [] }, "port": { "enable": True, "tcpPorts": all_ports, "udpPorts": [], "exclusionPorts": [], "mostCommonPortsNumber": 0 }, "service": { "enable": True, "intensity": 7 }, "network": { "enable": False, "proxies": [], "sourcePort": 1, "interface": "" }, "scanPolicy": { "enable": False, "minHostgroup": 0, "maxHostgroup": 0, "minRate": 1, "maxRate": 100 } } } response = session.post(task_url, json=data, headers=headers, verify=False) if response.status_code in [200, 201]: try: task_data = response.json().get("task", {}) task_id = task_data.get("id") if task_id: print(f"Задача успешно создана для: {ip_name}, ID Task: {task_id}") return task_id else: print(f"Ошибка при создании задачи для {ip_name}. Task ID не найден.") except (KeyError, ValueError) as e: print(f"Ошибка при разборе ответа API для {ip_name}: {e}") else: print(f"Ошибка при создании задачи для {ip_name}. Код: {response.status_code}, Текст: {response.text}") return None

Пример вывода программы.
Затем созданную задачу нужно запустить:
def run_task(auth_cookies, task_id): if not auth_cookies: print("Нет cookies, запуск задачи невозможен!") return headers = {'Cookie': auth_cookies} run_task_url = f"{task_url}/{task_id}:run" response = session.put(run_task_url, headers=headers, verify=False) if response.status_code == 200: print(f"Задача с ID {task_id} успешно запущена.") else: print(f"Ошибка при запуске задачи. Код: {response.status_code}, Текст: {response.text}") <img src="https://849719.selcdn.ru/mcloud.media/article/scan_2025-03-21/image25.png" align="center"/> <i>Пример вывода программы.</i> Теперь iplist.json выглядит так: <source> [ {"id": "1", "name": "имя хоста", "ip": "адрес хоста", “id_asset”:”100”,”id_netscan”:”123”}, ]
Помимо прочего, нам нужно проверять, что за статус у наших задач. Для этого в Swagger ищем GET-запрос, вводим id нужного задания и выполняем.

Получаем снипет:
curl -X 'GET' \ 'https://ip_addr/api/v1/tasks/ip_task' \ -H 'accept: application/json'
С помощью него пишем функцию:
def get_task_status(auth_cookies, task_id): if not auth_cookies: print("Нет cookies, получение статуса задачи невозможно!") return None headers = {'Cookie': auth_cookies} task_status_url = f"{task_url}/{task_id}" response = session.get(task_status_url, headers=headers, verify=False) if response.status_code == 200: try: task_data = response.json().get("task", {}) task_state = task_data.get("state") print(f"Статус задачи ID={task_id}: {task_state}") return task_state except (KeyError, ValueError) as e: print(f"Ошибка при разборе ответа API для задачи ID={task_id}: {e}") else: print(f"Ошибка при получении статуса задачи ID={task_id}. Код: {response.status_code}, Текст: {response.text}") return None
Функция формирует URL для получения статуса задачи и отправляет GET-запрос с id нужной задачи. Если статус-код 200 – извлекаем статус задачи из ответа, иначе выводит ошибку.

Пример вывода программы.
Сканирование сети успешно завершено.
Поиск уязвимостей на хостах и открытых портах
Открываем task-control в Swagger. Видим большой POST-запрос со всеми задачами, но нам нужен именно
vulnscanOptions:
"vulnscanOptions": { "assetIDs": asset_ids, "forceNist": True, "ignoreNistIfVendorResultsPresent": True, "ignoreNistUncertainVersion": True, "ignoreEmptyVulns": True, "filterWinOnlyForInstalledUpdates": False }
Важно: здесь используется assetID, а не IP-адреса, как в netscan. Именно поэтому на предыдущем шаге мы создавали asset из IP-адресов.
Функция поиска уязвимостей на хостах:
def add_vulnscan_task(auth_cookies, ip_name, asset_ids): if not auth_cookies: print("Нет cookies, создание задачи невозможно!") return headers = { 'Content-Type': 'application/json', 'Cookie': auth_cookies } data = { "name": f"Vulnscan for {ip_name}", "type": "TYPE_VULNSCAN", "vulnscanOptions": { "assetIDs": asset_ids, "forceNist": True, "ignoreNistIfVendorResultsPresent": True, "ignoreNistUncertainVersion": True, "ignoreEmptyVulns": True, "filterWinOnlyForInstalledUpdates": False } } response = session.post(task_url, json=data, headers=headers, verify=False) if response.status_code in [200, 201]: try: task_data = response.json().get("task", {}) task_id = task_data.get("id") if task_id: print(f"Задача успешно создана для {ip_name}, ID Task: {task_id}") return task_id else: print(f"Ошибка при создании задачи для {ip_name}. Task ID не найден.") except (KeyError, ValueError) as e: print(f"Ошибка при разборе ответа API для {ip_name}: {e}") else: print(f"Ошибка при создании задачи для {ip_name}. Код: {response.status_code}, Текст: {response.text}") return None
Как и в netscan, функция формирует POST-запрос, отправляет его для создания задачи и проверяет статус-код ответа. Если код 200 или 201, то извлекаем id задачи из ответа и записываем в iplist.json в виде “id_vulnscan”:”667”.

Пример вывода части программы.
Сканирования выполнены, теперь нужно сформировать отчет для наших адресов.
Шаг 5. Генерация отчетов о сканировании
1. Для генерации отчетов переходим в report-control в Swagger, выполняем запрос.

2. В полученном сниппете находим нужный нам фрагмент:
curl -X 'POST' \ 'https://ip_addr/api/v1/reports' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "assetIDs": [ 0 ], "maskPassword": true, "selectSeverity": [ 0 ] }'
3. Пишем функцию, которая формирует заголовки для POST-запроса и отправляет его на URL для создания отчета. Если статус-код 200 или 201 — оповещает об успешном создании отчета.
def add_report(auth_cookies, ip_name, asset_ids): if not auth_cookies: print("Нет cookies, создание отчёта невозможно!") return headers = { 'Content-Type': 'application/json', 'Cookie': auth_cookies } data = { "name": f"Report for {ip_name}", "assetIDs": asset_ids, "maskPassword": True, "selectSeverity": [0, 1, 2, 3, 4] } response = session.post(report_url, json=data, headers=headers, verify=False) if response.status_code in [200, 201]: try: report_data = response.json().get("report", {}) if report_data is not None or "report" in response.json(): print(f"Отчёт успешно создан для: {ip_name}") return True else: print(f"Ошибка при создании отчёта для {ip_name}. Ответ API: {response.text}") except (KeyError, ValueError) as e: print(f"Ошибка при разборе ответа API для {ip_name}: {e}") else: print(f"Ошибка при создании отчёта для {ip_name}. Код: {response.status_code}, Текст: {response.text}") return None
Шаг 6. Оповещение через Telegram-бота
Когда отчет создан, нужно оповестить об этом через бота. Создадим его и получим уникальный ключ — токен. Для этого начинаем диалог с BotFather в Telegram, вводим команду /newbot и настраиваем бота. Далее — вы получите сообщение с уникальным токеном. Указываем его и ID чата:
telegram_bot_token = '' chat_id = ''
Подготовим функцию для отправки сообщения:
def send_telegram_message(message): try: bot.send_message(chat_id, message) print(f"Сообщение отправлено в Telegram: {message}") except Exception as e: print(f"Ошибка при отправке сообщения в Telegram: {e}")
Функция принимает текст сообщения, в нашем случае — «Все отчеты успешно созданы» и отправляет его через объект bot. Если отправка успешна — получаем вывод в консоль и оповещение в Telegram.

Завершающий шаг

Рассмотренная автоматизация берет на себя всю рутину: от сбора данных до финального отчета. Вы просто запускаете скрипт (а лучше — настраиваете cron), и система сама выполняет все шаги. В итоге — меньше однотипных действий, больше времени на более важные и масштабные задачи.
Если скрипт должен запускаться автоматически, например, в первый понедельник месяца с часа ночи и каждые четыре часа, настройка в cron будет выглядеть так:
# Проверяем, что понедельник — первый день недели date +%u # Открываем crontab для редактирования crontab -e # Добавляем задачу (московское время) TZ=Europe/Moscow 0 0,4,8,12,16,20 1-7 * 1 [ "$(date +\%u)" -eq 1 ] && /путь/к/нашему/скрипту
Теперь можно вообще не вмешиваться в процесс — сканирование и генерация отчетов будут происходить автоматически в нужное время. Чтобы убедиться, что задание добавилось, используем команду
crontab -l.Важно учитывать, что время сканирования зависит от количества хостов и подсетей. Чем больше адресов — тем дольше выполняется скрипт. Если у вас крупная инфраструктура, увеличивайте промежутки между запусками, чтобы избежать пересечений и излишней нагрузки.
В тексте привели только ключевые фрагменты кода, а не готовый к запуску скрипт. Если все настроено правильно, то единственное, что останется вам сделать — зайти в систему и принять меры по устранению уязвимостей. Теперь процесс сканирования и отчетности будет быстрее, проще и надежнее.
