Сканируем уязвимости без рутины: «Сканер-ВС 6», API и Telegram-оповещения

mkgpcwu61rvfegb6abs4hj_ckwu.png

Ручное сканирование уязвимостей — это нормально, когда у вас десяток адресов и море свободного времени. Но если адресов сотни, а сканировать их нужно регулярно, процесс быстро превращается в рутину. Забыли запустить скан? Потеряли часть результатов? Результаты есть, но никто о них не узнал? Решение простое — автоматизация.

Меня зовут Антон, я инженер по информационной безопасности в Selectel. В тексте расскажу, как настроить скрипт, который через API «Сканер-ВС 6» возьмет все под контроль: сам запустит сканирование, создаст отчеты и отправит уведомление в Telegram. Все по расписанию через cron, без ручных запусков.

jargq5pdefecou6_ouzhfadcrr8.gifМы в Selectel готовим новый сервис. Если арендуете серверы в рабочих или личных проектах, нам очень поможет ваш опыт — записывайтесь на короткое онлайн-интервью. За участие подарим плюшевого Тирекса и бонусы на услуги Selectel.

Используйте навигацию, если не хотите читать текст полностью:
→ Знакомство с инструментом
→ Создание сервера
→ Разворачивание сканера
→ Автоматизация сканирований
→ Шаг 0. Авторизация в сканере
→ Шаг 1. Проверка обновления баз уязвимостей
→ Шаг 2. Создание задач на исследование сети
→ Шаг 3. Создание Assets
→ Шаг 4. Создание задач на сканирование уязвимостей
→ Шаг 5. Генерация отчетов о сканировании
→ Шаг 6. Оповещение через Telegram-бота
→ Завершающий шаг

Знакомство с инструментом


Для сканирования уязвимостей мы используем «Сканер-ВС 6» — универсальный инструмент для решения широкого спектра задач по тестированию и анализу защищенности информационных систем, а также контроля эффективности средств защиты информации. Рассмотрим ключевые возможности сканера.
  • Анализ безопасности конфигурации ОС: проверка базовых настроек безопасности.
  • Удобное управление информационными активами: сетевое сканирование хостов, построение карт сети, инвентаризация установленного ПО.
  • Выявление уязвимостей по версиям установленного ПО, ежедневно обновляемая база уязвимостей, включающая данные из БДУ ФСТЭК России, NIST NVD, и др.
  • Подбор паролей к сетевым сервисам: поддержка протоколов ftp, imap, imaps, mssql, mysql, pop3, pop3s, postgres, rdp, redis, и др.
  • Интеграция с внешними системами: отправка событий в SIEM-системы, открытый API.

qxzz_kcgu4uy7jehko8woulyxig.png

Создание сервера


Для работы со сканером нам нужно настроить сервер с ОС Astra Linux.

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

8130cc375c351e1067d6949f562a9cfd.png

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

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

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

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

Конфигурация сервера.

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

b6be51aa59e687127fddc979416827a2.png

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

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

Разворачивание сканера


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

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

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

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

0e804bbab7d5d7e327f8c61fe80d39b6.png

7. Запускаем инсталлер:
#root@taliyah:/home/astra/pkg# ./installer install

7.1. Устанавливаем сканер с дефолтными настройками.
4c3a692b83abb6705b76b2d0725bdc92.png

45dcbde6ddf3bf2504ec8917022f5adb.png

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

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

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

Страница аутентификации.
8ef1cce88de014162dc7962a6290a1f4.png

Главная страница инструмента.

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

b9c3402212b9b4c3c9f68d3d9808ca4a.png

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

Автоматизация сканирований


ed50a7edb2f736deca279405187f4e6a.png

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

В тексте покажем примеры кода для каждого из этапов — не целиком, а фрагментами. Так вам будет проще понять логику работы с 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.
5d027c58102fef7387f4e859baf9b79b.png

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

Из примера ниже видим, по какому адресу нужно отправить 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}")

Как работает функция
  1. Проверяет, переданы ли cookies. Если нет — выводит сообщение об ошибке и завершает выполнение.
  2. Отправляет GET-запрос на URL, используя cookies для авторизации.
  3. Проверяет статус-код ответа. Если статус 200, то выводит сообщение об успешном обновлении баз, иначе — выводит сообщение об ошибке с кодом и текстом ответа.

78d6a88a53140bfaad6a8f026e82e359.png

Пример вывода функции.

Все 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.

cd7c7d174e27d7f326e20cfda92f48bd.png

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.

dc02915a799c97e20b691a3a5dbb5968.png

Пример вывода программы.

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. Выполняем его и получаем снипет.

175da1e59fc43db7355f39a9b9b1cf4b.png

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

d5f097283e491320a436febcd988e4fa.png

Пример вывода программы.

Затем созданную задачу нужно запустить:

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}")


Пример вывода программы.
Теперь iplist.json выглядит так: 

[
    {"id": "1", "name": "имя хоста", "ip": "адрес хоста", "id_asset”:”100”,”id_netscan”:”123”},
]

Помимо прочего, нам нужно проверять, что за статус у наших задач. Для этого в Swagger ищем GET-запрос, вводим id нужного задания и выполняем.
40fd0354641a4401549701f26d58e4a3.png

Получаем снипет:
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 — извлекаем статус задачи из ответа, иначе выводит ошибку.
e889e0ac119efc0d75a0d860909143f6.png

Пример вывода программы.

Сканирование сети успешно завершено.

Поиск уязвимостей на хостах и открытых портах


Открываем task-control в Swagger. Видим большой POST-запрос со всеми задачами, но нам нужен именно vulnscanOptions:
175da1e59fc43db7355f39a9b9b1cf4b.png

        "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».
bfa07e9b95cc3a8abcafd4c5a724b0f0.png

Пример вывода части программы.

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

Шаг 5. Генерация отчетов о сканировании


1. Для генерации отчетов переходим в report-control в Swagger, выполняем запрос.
21b1a77fb2f25175be78fda14af64ec5.png

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

1f0012b469d1fa0fef286547a765ba71.png

Пример вывода части программы.

Шаг 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.
65d51595b7767153d9bd5eba30ecd028.png

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


145ef38d6dc1647995671e49c4333dca.png

Рассмотренная автоматизация берет на себя всю рутину: от сбора данных до финального отчета. Вы просто запускаете скрипт (а лучше — настраиваете 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.

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

В тексте привели только ключевые фрагменты кода, а не готовый к запуску скрипт. Если все настроено правильно, то единственное, что останется вам сделать — зайти в систему и принять меры по устранению уязвимостей. Теперь процесс сканирования и отчетности будет быстрее, проще и надежнее.

© Habrahabr.ru