From f3a9275b29c956fe33a75c1df6de114bd57f915f Mon Sep 17 00:00:00 2001 From: ogrechko Date: Wed, 10 Dec 2025 11:03:01 +0000 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20playbooks/scan=5Finventory.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- playbooks/scan_inventory.yml | 316 +++++++++++++++++++---------------- 1 file changed, 175 insertions(+), 141 deletions(-) diff --git a/playbooks/scan_inventory.yml b/playbooks/scan_inventory.yml index 06d725b..2d00e72 100644 --- a/playbooks/scan_inventory.yml +++ b/playbooks/scan_inventory.yml @@ -1,188 +1,222 @@ --- -- name: Умное сканирование сети и классификация устройств +- name: Сканирование сети через SNMP (Community: public) hosts: localhost connection: local gather_facts: no - become: yes # !!! ВАЖНО: Для определения OS (nmap -O) нужны права root + become: yes # Нужно для сканирования UDP (SNMP) vars: - # --- НАСТРОЙКИ SEMAPHORE --- + # --- SEMAPHORE API --- semaphore_url: "http://192.168.0.198:9999" semaphore_project_id: 1 - semaphore_key_id: 7 + #semaphore_key_id: 7 semaphore_api_token: "9ojexqiwt1xkemig7j1bd1pe-frh7hkre4reryk2occ=" inventory_name: "Smart Network Scan" + + # --- ID КЛЮЧЕЙ ДЛЯ БУДУЩИХ ПОДКЛЮЧЕНИЙ --- + # Даже если мы нашли устройство по SNMP, для управления им позже нужен ключ. + # Если ключей нет, можно указать ID "пустого" ключа или любого существующего. + key_windows: 7 + key_linux: 8 + key_mikrotik: 9 + key_printers: 7 # Можно тот же, принтерами обычно ansible не управляет # --- СЕТИ --- - subnets: - - "192.168.0.0/23" - - "192.168.2.0/24" - - "192.168.3.0/24" - - "172.19.8.0/24" - - "172.19.9.0/24" - - "172.19.10.0/24" - - "172.19.24.0/24" - - "172.19.26.0/24" - - "172.19.40.0/24" - - "172.19.42.0/24" - - "172.19.56.0/24" - - "172.19.58.0/24" - - "172.19.90.0/24" - # добавьте другие подсети + subnets: [ + "192.168.0.0/24" + "192.168.0.0/23" + "192.168.2.0/24" + "192.168.3.0/24" + "172.19.8.0/24" + "172.19.9.0/24" + "172.19.10.0/24" + "172.19.24.0/24" + "172.19.26.0/24" + "172.19.40.0/24" + "172.19.42.0/24" + "172.19.56.0/24" + "172.19.58.0/24" + "172.19.90.0/24" + ] - # Порты для проверки (добавил 8291 для MikroTik, 80/443 для веб-морд, 631 для принтеров) - scan_ports: [22, 445, 5985, 80, 443, 8291, 631] + # SNMP Community (пароль для чтения) + snmp_community: "public" tasks: # ---------------------------------------------------------------- - # ШАГ 1: Быстрый поиск живых хостов (Ping Sweep) - # Это нужно, чтобы не сканировать пустоту тяжелым сканом + # ШАГ 1: Поиск живых хостов (Ping) # ---------------------------------------------------------------- - - name: Быстрый поиск активных IP - command: > - nmap -sn -n --min-rate 1000 -T4 -oG - - {{ subnets | join(' ') }} + - name: Ping Sweep + command: "nmap -sn -n --min-rate 1000 -T4 -oG - {{ subnets | join(' ') }}" register: ping_scan changed_when: false - - name: Формирование списка живых IP + - name: Список IP set_fact: active_ips: "{{ ping_scan.stdout | regex_findall('Host: ([0-9.]+)') | unique | list }}" - - - name: Проверка - fail: - msg: "Живых хостов не найдено." - when: active_ips | length == 0 + + - fail: msg="Сеть пуста" when: active_ips|length == 0 # ---------------------------------------------------------------- - # ШАГ 2: Глубокий анализ каждого IP (ОС, Версии, Имена) - # Используем -O (OS), -sV (Versions) и скрипты + # ШАГ 2: Опрос через SNMP + Проверка портов + # -sU -p 161: Сканируем UDP порт SNMP + # --script snmp-sysdescr: Скрипт, который тянет описание ОС # ---------------------------------------------------------------- - - name: Глубокое сканирование найденных хостов + - name: SNMP Discovery shell: | - # Запускаем Nmap с определением ОС и скриптами SMB/Banner - # -O: Определение ОС (требует sudo) - # --osscan-guess: Пытаться угадать, если не уверен - # --script: Скрипты для точного имени Windows и SSH баннеров - nmap -O -sV -p {{ scan_ports | join(',') }} --script=smb-os-discovery,banner -Pn -n -T4 {{ item }} + nmap -sU -sS -p U:161,T:22,T:445,T:8291,T:9100 \ + --script snmp-sysdescr \ + --script-args snmpcommunity={{ snmp_community }} \ + -Pn -n -T4 {{ item }} loop: "{{ active_ips }}" - register: deep_scan_results + register: scan_results changed_when: false # ---------------------------------------------------------------- - # ШАГ 3: Анализ и классификация (Магия Ansible + Jinja2) + # ШАГ 3: Классификация на основе ответа SNMP # ---------------------------------------------------------------- - - name: Классификация хостов + - name: Анализ устройств set_fact: - classified_hosts: [] - - - name: Разбор результатов сканирования - set_fact: - classified_hosts: "{{ classified_hosts + [ host_data ] }}" + classified_hosts: "{{ classified_hosts | default([]) + [ host_data ] }}" vars: - output: "{{ item.stdout }}" + out: "{{ item.stdout }}" ip: "{{ item.item }}" - # --- ЛОГИКА ОПРЕДЕЛЕНИЯ ИМЕНИ --- - # Пытаемся найти имя через SMB, если нет - DNS, если нет - IP - smb_name: "{{ output | regex_search('Computer name: ([\\w-]+)', '\\1') | first | default('') }}" - dns_name_cmd: "nslookup {{ ip }} 192.168.1.250 | grep 'name =' | awk '{print $NF}' | sed 's/\\.$//'" - # (В реальном плейбуке nslookup внутри loop медленно, тут упрощено) - final_name: >- - {% if smb_name != '' %}{{ smb_name | lower }} - {% else %}host_{{ ip | replace('.', '_') }}{% endif %} - - # --- ЛОГИКА ОПРЕДЕЛЕНИЯ ТИПА --- - host_type: >- - {% if 'Windows' in output or 'Microsoft Windows' in output or '445/tcp open' in output %}windows - {% elif 'MikroTik' in output or 'RouterOS' in output or '8291/tcp open' in output %}mikrotik - {% elif 'Linux' in output or 'Ubuntu' in output or 'Debian' in output %}linux - {% elif 'HP' in output or 'Printer' in output or '631/tcp open' in output %}printer + # Пытаемся вытащить строку sysDescr из вывода Nmap + # Пример: "SNMPv2-MIB::sysDescr.0: RouterOS rb750..." + snmp_desc: "{{ out | regex_search('sysDescr\\.0: ([^\\n]+)', '\\1') | first | default('') }}" + + # Определяем тип на основе SNMP ответа (или открытых портов, если SNMP молчит) + detected_type: >- + {% if 'Windows' in snmp_desc or '445/tcp open' in out %}windows + {% elif 'RouterOS' in snmp_desc or 'MikroTik' in snmp_desc or '8291/tcp open' in out %}mikrotik + {% elif 'Linux' in snmp_desc or 'Ubuntu' in snmp_desc or '22/tcp open' in out %}linux + {% elif 'JetDirect' in snmp_desc or 'LaserJet' in snmp_desc or 'Samsung' in snmp_desc or 'Kyocera' in snmp_desc or '9100/tcp open' in out %}printer {% else %}other{% endif %} - - # --- СОБИРАЕМ ОБЪЕКТ --- + + # Формируем имя: Либо берем из SNMP, либо генерируем host_IP + # Часто в SNMP имя есть в sysName, но для упрощения оставим генерацию или возьмем первое слово из описания host_data: ip: "{{ ip }}" - name: "{{ final_name }}" - type: "{{ host_type }}" - raw_os: "{{ output | regex_search('OS details: ([^\\n]+)', '\\1') | first | default('Unknown') }}" - loop: "{{ deep_scan_results.results }}" + type: "{{ detected_type }}" + desc: "{{ snmp_desc | replace('\"', '') | default('No SNMP info') }}" + name: "{{ detected_type }}_{{ ip | replace('.', '_') }}" + loop: "{{ scan_results.results }}" no_log: true # ---------------------------------------------------------------- - # ШАГ 4: Генерация инвентаря по группам + # ШАГ 4: Разбиение на группы # ---------------------------------------------------------------- - - name: Генерация текста инвентаря - set_fact: - inventory_content: | - # === Windows Systems === - [windows] - {% for host in classified_hosts if host.type == 'windows' %} - {{ host.name }} ansible_host={{ host.ip }} # Detected: {{ host.raw_os }} - {% endfor %} - - [windows:vars] - ansible_connection=ssh - ansible_shell_type=powershell - ansible_user=o.grechko - ansible_port=22 - - # === Linux Systems === - [linux] - {% for host in classified_hosts if host.type == 'linux' %} - {{ host.name }} ansible_host={{ host.ip }} # Detected: {{ host.raw_os }} - {% endfor %} - - [linux:vars] - ansible_connection=ssh - ansible_user=root - - # === MikroTik Routers === - [routers_mikrotik] - {% for host in classified_hosts if host.type == 'mikrotik' %} - {{ host.name }} ansible_host={{ host.ip }} - {% endfor %} - - [routers_mikrotik:vars] - ansible_connection=network_cli - ansible_network_os=routeros - ansible_user=admin - - # === Printers & Others === - [others] - {% for host in classified_hosts if host.type == 'printer' or host.type == 'other' %} - {{ host.name }} ansible_host={{ host.ip }} # Type: {{ host.type }}, OS: {{ host.raw_os }} - {% endfor %} + - set_fact: + list_win: "{{ classified_hosts | selectattr('type', 'equalto', 'windows') | list }}" + list_lin: "{{ classified_hosts | selectattr('type', 'equalto', 'linux') | list }}" + list_tik: "{{ classified_hosts | selectattr('type', 'equalto', 'mikrotik') | list }}" + list_prn: "{{ classified_hosts | selectattr('type', 'equalto', 'printer') | list }}" # ---------------------------------------------------------------- - # ШАГ 5: Отправка в Semaphore (Ваш рабочий метод через файл) + # ШАГ 5: Отправка в Semaphore (Windows) # ---------------------------------------------------------------- - - name: Сохранение JSON - copy: - content: | - { - "name": "{{ inventory_name }} {{ 1000 | random }}", - "project_id": {{ semaphore_project_id | int }}, - "type": "static", - "ssh_key_id": {{ semaphore_key_id | int }}, - "become_key_id": null, - "repository_id": null, - "inventory": {{ inventory_content | to_json }} - } - dest: /tmp/semaphore_smart_payload.json + - block: + - set_fact: + content_win: | + [windows] + {% for h in list_win %} + {{ h.name }} ansible_host={{ h.ip }} # SNMP: {{ h.desc }} + {% endfor %} + [windows:vars] + ansible_connection=ssh + ansible_shell_type=powershell + ansible_port=22 - - name: Отправка через CURL - command: > - curl -v -X POST "{{ semaphore_url }}/api/project/{{ semaphore_project_id }}/inventory" - -H "Authorization: Bearer {{ semaphore_api_token }}" - -H "Content-Type: application/json" - -H "Accept: application/json" - -d @/tmp/semaphore_smart_payload.json - register: curl_result - ignore_errors: yes + - copy: + content: | + { + "name": "SNMP Windows {{ 1000 | random }}", + "project_id": {{ semaphore_project_id }}, + "type": "static", + "ssh_key_id": {{ key_windows }}, + "become_key_id": null, + "repository_id": null, + "inventory": {{ content_win | to_json }} + } + dest: /tmp/p_win.json + + - command: > + curl -X POST "{{ semaphore_url }}/api/project/{{ semaphore_project_id }}/inventory" + -H "Authorization: Bearer {{ semaphore_api_token }}" + -H "Content-Type: application/json" + -d @/tmp/p_win.json + ignore_errors: yes + when: list_win | length > 0 + + # ---------------------------------------------------------------- + # ШАГ 6: Отправка в Semaphore (MikroTik) + # ---------------------------------------------------------------- + - block: + - set_fact: + content_tik: | + [routers] + {% for h in list_tik %} + {{ h.name }} ansible_host={{ h.ip }} # {{ h.desc }} + {% endfor %} + [routers:vars] + ansible_connection=network_cli + ansible_network_os=routeros + + - copy: + content: | + { + "name": "SNMP MikroTik {{ 1000 | random }}", + "project_id": {{ semaphore_project_id }}, + "type": "static", + "ssh_key_id": {{ key_mikrotik }}, + "become_key_id": null, + "repository_id": null, + "inventory": {{ content_tik | to_json }} + } + dest: /tmp/p_tik.json + + - command: > + curl -X POST "{{ semaphore_url }}/api/project/{{ semaphore_project_id }}/inventory" + -H "Authorization: Bearer {{ semaphore_api_token }}" + -H "Content-Type: application/json" + -d @/tmp/p_tik.json + ignore_errors: yes + when: list_tik | length > 0 + + # ---------------------------------------------------------------- + # ШАГ 7: Отправка в Semaphore (Принтеры) + # Принтеры полезно видеть, но ansible ими обычно не управляет + # ---------------------------------------------------------------- + - block: + - set_fact: + content_prn: | + [printers] + {% for h in list_prn %} + {{ h.name }} ansible_host={{ h.ip }} # {{ h.desc }} + {% endfor %} + [printers:vars] + ansible_connection=local + + - copy: + content: | + { + "name": "SNMP Printers {{ 1000 | random }}", + "project_id": {{ semaphore_project_id }}, + "type": "static", + "ssh_key_id": {{ key_printers }}, + "become_key_id": null, + "repository_id": null, + "inventory": {{ content_prn | to_json }} + } + dest: /tmp/p_prn.json + + - command: > + curl -X POST "{{ semaphore_url }}/api/project/{{ semaphore_project_id }}/inventory" + -H "Authorization: Bearer {{ semaphore_api_token }}" + -H "Content-Type: application/json" + -d @/tmp/p_prn.json + ignore_errors: yes + when: list_prn | length > 0 - name: Очистка - file: path=/tmp/semaphore_smart_payload.json state=absent - - - name: Результат - debug: - msg: "Проверьте инвентарь! Создан ID, если статус 200/201" \ No newline at end of file + shell: rm -f /tmp/p_*.json \ No newline at end of file