Работаю я SOC-аналитиком в большой компании. Сижу, разбираю очередной инцидент, заходит ко мне начальство и говорит: "Нам необходимо знать, какие ОС используются у нас в компании" - эти данные необходимы, чтобы мучить сисадминов с обновлением ОС. Та самая вечная борьба сисадминов и безопасников. В каждой большой компании ведется учет пк/серверов и ОС на них, но перебирать ~18000 устройств у меня желания не было, начал думать, как бы всё это автоматизировать.
О моей работе с JSON
После получения задачи, подумал о реализации с помощью Selenium, который я освоил на должности тестеровщика в предыдущей компании. Открываю pycharm, кладу руки на клавиатуру и ... И ничего! Сразу приходит мысль об авторизации, а указывать логин и пароль в коде - очень плохая затея, к тому же как я пройду 2FA? К счастью, KUMA, как и многие современные системы, имеет токен авторизации, т.е. нет необходимости оставлять учетные данные в коде, только отправить авторизационный токен в запросе. Но я таким никогда не занимался и понятия не имею, как авторизоваться с помощью токена. Задал вопрос Алисе, после чего понял, что лучше обратиться к документации, ибо Российский ИИ мне не помог. Прочитав документацию, приступил к написанию кода. Авторизация по токену оказалась очень легким занятием.
token = "my_token" url = "my_api_url" api_header={ "Content-Type": "application/json", "Authorization": f"Bearer {token}" } # Далее необходимо сделать запрос req = request.get(url, headers=api_header) # Данные получены!
После этого я почувствовал себя гением, но продолжалось это чувство не долго... Оказалось, URL в консоле разработчика и для работы с API разные, о таком я не подумал и долго мучился. Коллеги подсказали нужную URL. После получения данных, я просто достаю их по ключам. Структуру json-ответа нашел также в документации. Для вашего удобства размещу её здесь. P. S. Я пытался 30 минут понять, как этот json читать.
Скрытый текст
type Response []Asset
type Asset struct {
ID string json:"id"
TenantID string json:"tenantID"
TenantName string json:"tenantName"
Name string json:"name"
FQDN string json:"fqdn"
IPAddresses []string json:"ipAddresses"
MACAddresses []string json:"macAddresses"
Owner string json:"owner"
OS *OS json:"os"
Software []Software json:"software"
Vulnerabilities []Vulnerability json:"vulnerabilities"
KICSRisks []*assets.KICSRisk json:"kicsVulns"
KSC *KSCFields json:"ksc"
Created string json:"created"
Updated string json:"updated"
}
type KSCFields struct {
NAgentID string json:"nAgentID"
KSCInstanceID string json:"kscInstanceID"
KSCMasterHostname string json:"kscMasterHostname"
LastVisible string json:"lastVisible"
}
type OS struct {
Name string json:"name"
Version uint64 json:"version"
}
type Software struct {
Name string json:"name"
Version string json:"version"
Vendor string json:"vendor"
}
type Vulnerability struct {
KasperskyID string json:"kasperskyID"
ProductName string json:"productName"
DescriptionUrl string json:"descriptionUrl"
RecommendedMajorPatch string json:"recommendedMajorPatch"
RecommendedMinorPatch string json:"recommendedMinorPatch"
SeverityStr string json:"severityStr"
Severity uint64 json:"severity"
CVE []string json:"cve"
ExploitExists bool json:"exploitExists"
MalwareExists bool json:"malwareExists"
}
type assets.KICSRisk struct {
ID int64 json:"id"
Name string json:"name"
Category string json:"category"
Description string json:"description"
DescriptionUrl string json:"descriptionUrl"
Severity int json:"severity"
Cvss float64 json:"cvss"
}
Далее просто доставал данные по ключам.
data = req.json() data.get("name") # И так далее, все, что мне было нужно
На данном этапе я снова преисполнился гордость за себя, разобрался с тем, что делают на 1-м курсе института, а более интересующиеся ещё в школе. Не долго у меня был воодушевленный настрой. Нужные мне данные имели вложенность. Я не долго думал и сделал цикл в цикле, вложенность не большая, но вычислялось это долго. Немного погуглив, оказалось, что для борьбы с вложенностью применяют рекурсию.
def recursive_extract(data, result=None): result = [] if isinstance(data, dict): for key, value in data.items(): if key == "тут достаю данные поля software": result.append(value) recursive_extract(value, result) return result
При чем тут KUMA?
В KUMA (Kaspersky Unified Monitoring and Analysis Platform) - SIEM-система, по мимо обычного просмотра событий, существует возможность просматривать устройства и установленное на них ПО. Что несомненно является плюсом.
Ухх... Какими только добрыми и недобрыми словами я не вспоминал разработчиков KUMA. А всё потому, что в веб-интерфейсе можно отфильтровать ПО так, как ты хочешь, т.е. отправить post запрос, и с api казалось бы всё то же самое, НО! Для работы с api в KUMA предусмотрен post запрос исключительно для создания актива (устройства), а мне нужен был фильтр по активам... Для наглядности посмотрите на изображение.

Также в KUMA нельзя посмотреть дистрибутив Linux. Система только может сказать, что то или иное устройство является Linux, но дистрибутива с версией не показывает.
О версионировании пакетов Linux
Обычное версионирование (семантическое): MAJOR.MINOR.PATCH (например: 2.36.105), где 2 - major, 36 - minor, 105 - patch. Major увеличивается при серьёзных несовместимых изменениях, minor при добавлении новых функций или обновлений безопасности, patch при исправле��ии багов, ошибок.
1.0.0- Стабильная версия.1.0.1- Исправили опечатку или критический баг (Патч).1.1.0- Добавили новый фильтр поиска (Минор).2.0.0- Переписали систему авторизации, теперь старый метод не работает (Мажор).
Пример посложнее: 1.2.3-1ubuntu2. 1.2.3 - версия автора, не сложно догадаться где здесь major, minor, patch. 1 после тире - это номер сборки пакета (не код программы, а например: исправление путей установки). Суффикс ubuntu - говорит, что пакет собран специально для Ubuntu, а не взят напрямую из Debian (родительского дистрибутива). Цифра 2 означает, что Ubuntu делала модификации этого конкретного Debian-пакета дважды.
Более сложный пример: 2.2.6-2+deb10u9+ci1+astra1. 2.2.6-2 по аналогии с предыдущим примером. Знак + означает, что к оригинальной версии (2.2.6-2) что-то добавили. deb10 - маркер Debian 10. u9 - означает "update 9", девятое обновление этого пакета для Debian 10. ci - маркер, который означает, что пакет прошел через автоматизированную систему сборки и тестирования. Цифра 1 - означает номер сборки или итерации. astra - пакет адаптирован для Astra Linux. Цифра 1 после astra является первой версией адаптации под Astra Linux. ci1 относится к astra, т.е. его прогнали через автосборку команда Astra Linux (Скорее всего автосборка сделана командой Astra, в этом я не уверен, эксперты поправьте меня в комментариях).
Определения дистрибутивов Linux по пакетам
Я потратил большое кол-во времени, делая то, что придет в голову, не имея при этом никакой четкой структуры мысли, только идею. Мне нужно было продумать работу программы, в один момент я понял, что нужно ТЗ. Основной ид��ей было то, что алгоритм анализирует установленные пакеты на устройстве, чтобы определить:
Типу ОС (Windows/Linux/Unknown)
Конкретный дистрибутив (Ubuntu, Debian, ALT Linux и т.д.)
Версию дистрибутива
Уровень уверенности в определении
Опираясь на идею, я написал ТЗ. Приступив, я понял, что это не самое простое занятие, как козалось, и на продумывание логики ушло часа 2 с учетом.
Скрытый текст
1. ГИБКИЙ ФОРМАТ ВЫВОДА
Пользователь выбирает CSV или JSON
Опциональное разделение результатов по уровню уверенности
Два режима: с разделением (high/medium vs low confidence) и без
2. АЛГОРИТМИЧЕСКАЯ СУТЬ
2.1. Простое определение типа ОС
Windows → если в os.name есть "windows"
Linux → если в os.name есть "linux" или os равно null
Unknown → во всех остальных случаях
2.2. Сложная логика для Linux, система весов:
150 баллов: Уникальные системные пакеты (ubuntu-release, alt-release)
20 баллов: Менеджеры пакетов (apt, yum, rpm)
15 баллов: Суффиксы в версиях (.ubuntu, .alt, .el8)
10 баллов: Специфичные пакеты дистрибутива
5 баллов: Косвенные признаки
2.3. ПОДДЕРЖИВАЕМЫЕ ДИСТРИБУТИВЫ
Ubuntu, Debian, Linux Mint
ALT Linux, Astra Linux
Fedora, CentOS, RHEL, RedOS
Arch Linux, openSUSE
2.4. ЛОГИКА ОПРЕДЕЛЕНИЯ
Сканирование пакетов → начисление баллов
Подсчет суммы для каждого дистрибутива
Выбор победителя
Разрешение конфликтов при равных баллах
2.5. ОПРЕДЕЛЕНИЕ ВЕРСИИ
Из системных пакетов (наиболее надежно)
Из суффиксов (.el8 → версия 8)
По кодовым именам (focal, bionic для Ubuntu)
2.6. УРОВЕНЬ УВЕРЕННОСТИ
High: ≥150 баллов или ≥50 баллов
Medium: 20-49 баллов
Low: <20 баллов или данные неполные
3. КЛЮЧЕВЫЕ ОСОБЕННОСТИ
3.1. ОБРАБОТКА НЕПОЛНЫХ ДАННЫХ
Работает даже при os: null
Обрабатывает пустой массив software
Использует резервные методы при недостатке данных
3.2. ИЕРАРХИЯ ПРИОРИТЕТОВ
Уникальные системные пакеты
Суффиксы в версиях
Менеджеры пакетов
Специфичные пакеты
Косвенные признаки
3.3. РАЗРЕШЕНИЕ КОНФЛИКТОВ
При равных баллах: приоритет системным пакетам
Далее: приоритет менеджерам пакетов
Затем: количество специфичных пакетов
После формирования ТЗ спросил у нейронки о признаках дистрибутивов. Вот, что она мне выдала:
Скрытый текст
{
"Ubuntu": {
"release_packages": ["ubuntu-release", "lsb-release-ubuntu"],
"package_managers": ["apt", "dpkg"],
"version_suffixes": [".ubuntu", "+ubuntu", "ubuntu"],
"specific_packages": ["ubuntu-advantage-tools", "ubuntu-minimal"],
"code_names": ["bionic", "focal", "jammy", "trusty", "xenial"],
"exclusive_suffixes": [".ubuntu", "+ubuntu"]
},
"Debian": {
"release_packages": ["debian-release"],
"package_managers": ["apt", "dpkg"],
"version_suffixes": [".deb", "+deb", "debian"],
"specific_packages": ["debian-archive-keyring"],
"code_names": ["buster", "bullseye", "bookworm", "sid"],
"exclusive_suffixes": [".deb", "+deb", "debian"]
},
"Linux Mint": {
"release_packages": ["mint-release", "linuxmint-release"],
"package_managers": ["apt", "dpkg"],
"version_suffixes": [".mint", "mint"],
"specific_packages": ["mint-common", "mintdrivers"],
"exclusive_suffixes": [".mint", "mint"]
},
"ALT Linux": {
"release_packages": ["alt-release", "altlinux-release", "alt-os-release"],
"package_managers": ["apt-rpm", "rpm-alt"],
"version_suffixes": [".alt", "+alt", "alt"],
"specific_packages": ["systemd-alt", "apt4rpm", "altlinux-repos", "Sisyphus"],
"exclusive_suffixes": [".alt", "+alt"]
},
"Astra Linux": {
"release_packages": ["astra-version", "astra-release"],
"package_managers": ["dpkg", "apt"],
"version_suffixes": [".astra", "astra", "smolensk"],
"specific_packages": ["astra-common", "astra-security"],
"exclusive_suffixes": [".astra", "astra"]
},
"Fedora": {
"release_packages": ["fedora-release"],
"package_managers": ["dnf", "yum", "rpm"],
"version_suffixes": [".fc", ".fedora"],
"specific_packages": ["fedora-obsolete-packages"],
"exclusive_suffixes": [".fc", ".fedora"]
},
"CentOS": {
"release_packages": ["centos-release"],
"package_managers": ["yum", "rpm"],
"version_suffixes": [".el", ".centos"],
"specific_packages": ["centos-release", "centos-logos"],
"exclusive_suffixes": [".centos"]
},
"RHEL": {
"release_packages": ["redhat-release"],
"package_managers": ["yum", "rpm"],
"version_suffixes": [".el", ".rhel"],
"specific_packages": ["redhat-release", "rhel-docs"],
"exclusive_suffixes": [".rhel"]
},
"RedOS": {
"release_packages": ["redos-release"],
"package_managers": ["yum", "rpm", "dnf"],
"version_suffixes": [".redos", "redos", ".el"],
"specific_packages": ["redos-release", "redos-docs"],
"exclusive_suffixes": [".redos", "redos"]
},
"Arch Linux": {
"release_packages": ["arch-release"],
"package_managers": ["pacman"],
"version_suffixes": [".arch", "arch"],
"specific_packages": ["archlinux-keyring"],
"exclusive_suffixes": [".arch", "arch"]
},
"openSUSE": {
"release_packages": ["opensuse-release"],
"package_managers": ["zypper", "yast"],
"version_suffixes": [".suse", ".opensuse"],
"specific_packages": ["yast2", "opensuse-build-key"],
"exclusive_suffixes": [".suse", ".opensuse"]
}
С этими данными уже можно было работать. Конечно, в процессе тестирования и доработки программы туда были добавлены новые признаки.
Основная функция анализа дистрибутивов. Тут все просто, куча циклов которые проверяют принадлежность пакетов и добавляют дистрибутивам баллы.
def analyze_linux(self, device: Dict) -> Dict: software = device.get('software', []) # Инициализация счетчиков scores = {distro: 0 for distro in DISTRO_RULES} specific_counts = {distro: 0 for distro in DISTRO_RULES} suffix_counts = {distro: 0 for distro in DISTRO_RULES} # Анализ каждого пакета for pkg in software: if not isinstance(pkg, dict): continue pkg_name = pkg.get('name', '').lower() pkg_version = pkg.get('version', '').lower() # 1. Проверка release-пакетов (150 баллов) for distro, rules in DISTRO_RULES.items(): for release_pkg in rules['release_packages']: if release_pkg in pkg_name: scores[distro] += WEIGHTS['release_package'] # 2. Проверка менеджеров пакетов (20 баллов) for distro, rules in DISTRO_RULES.items(): for manager in rules['package_managers']: if manager in pkg_name: scores[distro] += WEIGHTS['package_manager'] # 3. Проверка суффиксов версий (15 баллов) for distro, rules in DISTRO_RULES.items(): for suffix in rules['version_suffixes']: if suffix in pkg_version: scores[distro] += WEIGHTS['version_suffix'] suffix_counts[distro] += 1 # 4. Проверка специфичных пакетов (10 баллов) for distro, rules in DISTRO_RULES.items(): for specific_pkg in rules['specific_packages']: if specific_pkg in pkg_name: scores[distro] += WEIGHTS['specific_package'] specific_counts[distro] += 1 # 5. Косвенные признаки (5 баллов) for distro in scores: if suffix_counts[distro] >= 3: scores[distro] += WEIGHTS['indirect'] # Выбор победителя result = self._select_distro_winner(scores, specific_counts, device) # Определение версии if result.get('os_distro') != 'не удалось определить ОС': version = self._determine_linux_version(device, result['os_distro']) result['os_version'] = version if version else 'не удалось определить версию' return result
Далее функция с выбором победителей. Логика - проста, несколько дистрибутивов набрало одинаковое кол-во баллов - необходимо выбрать только 1.
Уровень 0: обнаружен уникальный release-пакет (не встречающийся у других дистрибутивов) → немедленный return.
Уровень 1: дистрибутивы, набравшие ≥150 баллов (release-пакеты). Если один – победа, если несколько – разрешение по максимальному счёту и дополнительным признакам (Например lsb-release).
Уровень 2: проверка специальных случаев через checkspecial_cases() (например, комбинация apt+rpm для ALT Linux, суффикс .astra для Astra Linux и т.д.).
Уровень 3: обычный максимум баллов. При равенстве применяются последовательные критерии: уникальные признаки → количество специфичных пакетов → комбинации менеджеров → алфавитный порядок.
Уровень 4: fallback на поле os.name.
def _select_distro_winner(self, scores: Dict, specific_counts: Dict, device: Dict) -> Dict: default_result = {'os_type': 'linux', 'os_distro': 'не удалось определить ОС', 'os_version': 'не удалось определить версию', 'confidence': 'low', 'detection_method': 'none'} try: # ... подготовка all_packages ... # Приоритет 0: уникальные release-пакеты for pkg_name, _ in all_packages: for distro, rules in DISTRO_RULES.items(): for release_pkg in rules.get('release_packages', []): if release_pkg in pkg_name: is_unique = True for other_distro, other_rules in DISTRO_RULES.items(): if distro != other_distro: if any(release_pkg in other_release for other_release in other_rules.get('release_packages', [])): is_unique = False break if is_unique: return {'os_type': 'linux', 'os_distro': distro, 'confidence': 'high', 'detection_method': 'unique_release_package'} # Приоритет 1: release-пакеты (≥150 баллов) release_candidates = [(d, s) for d, s in scores.items() if s >= WEIGHTS['release_package']] if release_candidates: # ... логика выбора ... # Приоритет 2: специальные случаи special = self._check_special_cases(all_packages, scores) if special: return special # Приоритет 3: обычный максимум max_score = max(scores.values()) if scores else 0 if max_score > 0: top = [d for d, s in scores.items() if s == max_score] # ... разрешение конфликтов (уникальные признаки, специфичные пакеты) ... except Exception as e: print(f"Ошибка в _select_distro_winner: {e}") return default_result return default_result
Функция check_special_cases() (из приоритета 2) - по сути это определение по суффиксам. Вот пример для astra linux, с остальными дистрибутивами аналогично.
def _check_special_cases(self, all_packages: List[Tuple[str, str]], scores: Dict) -> Optional[Dict]: package_names = [pkg_name for pkg_name, _ in all_packages] package_versions = [pkg_version for _, pkg_version in all_packages] has_astra_package = any('astra-' in pkg_name for pkg_name in package_names) has_astra_suffix = any('.astra' in pkg_version for pkg_version in package_versions) if has_astra_suffix or has_astra_package: astra_score = scores.get('Astra Linux', 0) if astra_score > 0: return {'os_type': 'linux', 'os_distro': 'Astra Linux', 'confidence': 'medium' if scores['Astra Linux'] >= 20 else 'low', 'detection_method': 'special_case_astra_linux'}
Функция определения версии. После выбора дистрибутива вызывается determinelinux_version(), которая делегирует работу специализированным функциям: determine_ubuntu_version(), determine_debian_version(), determine_alt_version(), determine_astra_version() и т.д. Приведу пример для astra linux, с остальными дистрибутивами ситуация очень похожа, изменяются по большей части только паттеры для поиска версий.
def _determine_astra_version(self, software: List[Dict]) -> Optional[str]: for pkg in software: if not isinstance(pkg, dict): continue pkg_name = pkg.get('name', '') pkg_version = pkg.get('version', '') if any(release in pkg_name for release in ['astra-release', 'astra-version']): if pkg_version: # Ищем паттерн +vX.X.X version_match = re.search(r'\+v?(\d+(?:\.\d+)+)', pkg_version) if version_match: extracted = version_match.group(1) if self._looks_like_astra_version(extracted): return extracted # ... SE версии # ... поиск по суффиксам .astra return None
Заключение
Код не определяет дистрибутивы на 100%. На практике у меня получилось определить правильно 95-98% дистрибутивов.
Это был мой первый опыт использования python для подобных целей. Да, местами я анализирую код и понимаю, где-то можно оптимизировать, где-то доработать. Но я пользуюсь золотым правилом: "Работает - не трогай!". Может существует другой способ определения дистрибутивов linux, но я до него не додумался, если знаете такой, пишите в комментариях.
