Как определить дистрибутив Linux? А что, если имеется только информация о пакетах, установленных на устройстве? История одного «велосипеда» или мы не нашли готового решения.Представьте, что вам нужно определить дистрибутивы, опираясь исключительно на установленные пакеты.
Работаю я 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 запрос исключительно для создания актива (устройства), а мне нужен был фильтр по активам... Для наглядности посмотрите на изображение.

Работа через веб-интерфейс и api
Также в 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 с учетом.
Скрытый текстПользователь выбирает CSV или JSON
Опциональное разделение результатов по уровню уверенности
Два режима: с разделением (high/medium vs low confidence) и без
Windows → если в os.name есть "windows"
Linux → если в os.name есть "linux" или os равно null
Unknown → во всех остальных случаях
150 баллов: Уникальные системные пакеты (ubuntu-release, alt-release)
20 баллов: Менеджеры пакетов (apt, yum, rpm)
15 баллов: Суффиксы в версиях (.ubuntu, .alt, .el8)
10 баллов: Специфичные пакеты дистрибутива
5 баллов: Косвенные признаки
Ubuntu, Debian, Linux Mint
ALT Linux, Astra Linux
Fedora, CentOS, RHEL, RedOS
Arch Linux, openSUSE
Сканирование пакетов → начисление баллов
Подсчет суммы для каждого дистрибутива
Выбор победителя
Разрешение конфликтов при равных баллах
Из системных пакетов (наиболее надежно)
Из суффиксов (.el8 → версия 8)
По кодовым именам (focal, bionic для Ubuntu)
High: ≥150 баллов или ≥50 баллов
Medium: 20-49 баллов
Low: <20 баллов или данные неполные
Работает даже при os: null
Обрабатывает пустой массив software
Использует резервные методы при недостатке данных
Уникальные системные пакеты
Суффиксы в версиях
Менеджеры пакетов
Специфичные пакеты
Косвенные признаки
При равных баллах: приоритет системным пакетам
Далее: приоритет менеджерам пакетов
Затем: количество специфичных пакетов
После формирования ТЗ спросил у нейронки о признаках дистрибутивов. Вот, что она мне выдала:
Скрытый текст{
"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, но я до него не додумался, если знаете такой, пишите в комментариях.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Мастерская хакера. Стресс-тестинг сайтов, бенчмарк для команд, работа с текстом и другие полезности | 2 | 7 | 23-10-2025 |
| 2 | Linux диванного гитариста. Часть 2. Кемпер для бедных | 0 | 9.13 | 11-05-2026 |
| 3 | Redirecting apt sources to address increased load: A Guide to Using Community Mirrors | 0 | 7.3 | 17-04-2026 |
| 4 | Шабаш в баше. Учимся автоматизировать действия в командной строке и применять мощные утилиты | 5 | 7 | 23-07-2025 |
| 5 | Что происходит у меня в вэбмастере? | 0 | 8.98 | 24-05-2026 |
| 6 | How Linux Security Teams Spot Vulnerabilities Before CVEs Are Published | 0 | 7 | 14-07-2026 |
| 7 | Linux Kernel Module Rootkits: How Attackers Hide After Compromising Cloud Workloads | -2 | 7 | 02-07-2026 |
| 8 | Detecting Systemd Abuse on Linux Servers for Better Security | 0 | 5 | 19-05-2026 |
| 9 | Обновления и новый функционал в выдаче Яндекс | 0 | 5.62 | 29-11-2025 |