diff --git a/README.md b/README.md index cfeaaa6..4c9d183 100755 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ - `proxmox/` — шаблонные скрипты подготовки и наполнения CT на стороне Proxmox. - `aw-server/` — установочные скрипты, env-шаблон, systemd unit и RU patch для Web UI. - `ansible/` — Ansible-ensemble для автоматизированного сервера (Debian/CT). +- `pfsense/` — внешний poller для pfSense API и systemd unit под Debian/Ubuntu utility VM. - `windows/` — PowerShell toolkit: single-user, domain-users, ensemble orchestration, hardening/recovery, validation, phase-2 DLP telemetry (`aw-dlp-incidents_*`, `aw-dlp-endpoint-signals_*`). - `scripts/quality-gate.sh` — локальный preflight-пайплайн проверок. @@ -37,6 +38,10 @@ - `ansible/deploy_aw_windows_phase2.yml` +Для внешнего pfSense poller'а: + +- `ansible/deploy_aw_pfsense_poller.yml` + Скрипты `proxmox/create-ct.sh` и `proxmox/push-aw-artifacts.sh` по умолчанию читают: - `secrets/deploy.secrets.env` diff --git a/ansible/README.md b/ansible/README.md index d072d1b..b8a82d7 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -5,6 +5,7 @@ - деплой на уже существующий Debian host/CT; - полный цикл с нуля в Proxmox: создание CT + bootstrap + установка ActivityWatch + RU patch. - централизованный деплой Windows phase-2 collectors по WinRM. +- deployment внешнего pfSense poller'а на Debian/Ubuntu utility VM. ## Файлы @@ -12,11 +13,13 @@ - `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_and_deploy_aw.yml` — full-stack playbook для Proxmox. - `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml` — массовый full-stack playbook (несколько CT). - `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_windows_phase2.yml` — WinRM playbook для развёртывания phase-2 Windows collector'ов. +- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_pfsense_poller.yml` — deployment pfSense poller'а. - `/home/igor/tmp/AWatch-rus/ansible/inventory.example.ini` — шаблон inventory. - `/home/igor/tmp/AWatch-rus/ansible/group_vars/all.example.yml` — шаблон переменных. - `/home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox.example.yml` — шаблон переменных CT в Proxmox. - `/home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox-matrix.example.yml` — шаблон матрицы CT. - `/home/igor/tmp/AWatch-rus/ansible/group_vars/windows.example.yml` — шаблон переменных Windows phase-2. +- `/home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.example.yml` — шаблон переменных pfSense poller'а. ## Быстрый запуск @@ -86,11 +89,31 @@ Playbook: - `aw_windows_incident_artifacts_root: 'C:\...\incident-artifacts'` — переопределить путь артефактов; - `aw_windows_skip_hardening: true` — пропустить `hardening-recovery.ps1` внутри ensemble-скрипта. +## pfSense poller rollout + +1. Подготовьте vars: + - `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.yml` +2. Добавьте inventory group `[aw_pfsense_pollers]`. +3. Запустите: + +```bash +cd /home/igor/tmp/AWatch-rus/ansible +ansible-playbook -i inventory.ini deploy_aw_pfsense_poller.yml +``` + +Playbook: + +- ставит `python3`; +- копирует `pfsense-aw-poller.py`; +- пишет `/etc/aw-pfsense/poller.json`; +- поднимает `aw-pfsense-poller.service`. + ## Результат - Установлен ActivityWatch Server. - Создан systemd-unit `activitywatch-server.service`. - Установлен RU Web UI patch. - Для Web UI используется checksum-based cache-bust для `ru-patch-v5.js` и `sw-cleanup.js`, чтобы браузер не держал старую DLP/русскую статику после деплоя. +- На `#/home` Web UI делит хосты на `Windows RDP` и `Virtual servers + Proxmox`. - Выполнена валидация API `http://127.0.0.1:5600/api/0/info`. - Для full-stack сценария CT создаётся автоматически через `pct create`. diff --git a/ansible/deploy_aw_pfsense_poller.yml b/ansible/deploy_aw_pfsense_poller.yml new file mode 100644 index 0000000..3eea27c --- /dev/null +++ b/ansible/deploy_aw_pfsense_poller.yml @@ -0,0 +1,66 @@ +--- +- name: Deploy pfSense ActivityWatch poller + hosts: aw_pfsense_pollers + become: true + gather_facts: true + + vars: + aw_pfsense_install_root: "/opt/aw-pfsense" + aw_pfsense_config_dir: "/etc/aw-pfsense" + aw_pfsense_service_name: "aw-pfsense-poller.service" + + tasks: + - name: Install required packages + ansible.builtin.apt: + name: + - python3 + state: present + update_cache: true + + - name: Ensure directories exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + loop: + - "{{ aw_pfsense_install_root }}" + - "{{ aw_pfsense_config_dir }}" + + - name: Install pfSense poller script + ansible.builtin.copy: + src: "{{ aw_repo_root }}/pfsense/pfsense-aw-poller.py" + dest: "{{ aw_pfsense_install_root }}/pfsense-aw-poller.py" + mode: "0755" + + - name: Install systemd service + ansible.builtin.copy: + src: "{{ aw_repo_root }}/pfsense/pfsense-aw-poller.service" + dest: "/etc/systemd/system/{{ aw_pfsense_service_name }}" + mode: "0644" + notify: + - Reload systemd + + - name: Write pfSense poller config + ansible.builtin.copy: + dest: "{{ aw_pfsense_config_dir }}/poller.json" + mode: "0600" + content: "{{ aw_pfsense_poller_config | to_nice_json }}" + notify: + - Restart pfSense poller + + - name: Enable and start pfSense poller + ansible.builtin.systemd: + name: "{{ aw_pfsense_service_name }}" + enabled: true + state: restarted + daemon_reload: true + + handlers: + - name: Reload systemd + ansible.builtin.systemd: + daemon_reload: true + + - name: Restart pfSense poller + ansible.builtin.systemd: + name: "{{ aw_pfsense_service_name }}" + state: restarted diff --git a/ansible/deploy_aw_server.yml b/ansible/deploy_aw_server.yml index bb558e3..d5f7fda 100644 --- a/ansible/deploy_aw_server.yml +++ b/ansible/deploy_aw_server.yml @@ -11,6 +11,7 @@ aw_bootstrap_dir: "/tmp/aw-rus-bootstrap" aw_ru_patch_cache_bust: "{{ lookup('file', aw_repo_root + '/aw-server/aw-ru-patch.js') | hash('sha1') | truncate(12, true, '') }}" aw_sw_cleanup_cache_bust: "{{ lookup('file', aw_repo_root + '/aw-server/aw-sw-cleanup.js') | hash('sha1') | truncate(12, true, '') }}" + aw_host_groups_cache_bust: "{{ lookup('file', aw_repo_root + '/aw-server/aw-host-groups.json') | hash('sha1') | truncate(12, true, '') }}" aw_worktime_classes: "{{ lookup('file', aw_repo_root + '/aw-server/settings/classes-worktime.json') | from_json }}" aw_default_views: "{{ lookup('file', aw_repo_root + '/aw-server/settings/views-default.json') | from_json }}" @@ -92,6 +93,7 @@ - { src: "{{ aw_repo_root }}/aw-server/activitywatch-server.service", dest: "/etc/systemd/system/activitywatch-server.service", mode: "0644" } - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" } - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" } notify: - Reload systemd - Restart activitywatch diff --git a/ansible/group_vars/pfsense-poller.example.yml b/ansible/group_vars/pfsense-poller.example.yml new file mode 100644 index 0000000..87aabaa --- /dev/null +++ b/ansible/group_vars/pfsense-poller.example.yml @@ -0,0 +1,30 @@ +aw_pfsense_poller_config: + poll_interval_seconds: 60 + aw: + server_host: "10.10.10.13" + server_port: 5600 + hostname: "PFSENSE-EDGE01" + pulse_time_seconds: 120 + timeout_seconds: 15 + pfsense: + name: "pfSense Edge 01" + host: "10.10.10.1" + scheme: "https" + verify_tls: false + timeout_seconds: 15 + headers: + X-API-Key: "replace-me" + X-API-Secret: "replace-me" + endpoints: + - name: "system-status" + path: "/api/v2/status/system" + bucket_prefix: "aw-pfsense-health" + bucket_type: "aw.pfsense.health" + - name: "interfaces" + path: "/api/v2/interface" + bucket_prefix: "aw-pfsense-interfaces" + bucket_type: "aw.pfsense.interfaces" + - name: "gateways" + path: "/api/v2/status/gateways" + bucket_prefix: "aw-pfsense-gateways" + bucket_type: "aw.pfsense.gateways" diff --git a/aw-server/apply_webui_ru_patch.sh b/aw-server/apply_webui_ru_patch.sh index 6ea909e..d35c71e 100755 --- a/aw-server/apply_webui_ru_patch.sh +++ b/aw-server/apply_webui_ru_patch.sh @@ -12,6 +12,7 @@ source "$ENV_FILE" WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-/opt/activitywatch/webui-ru}" PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js" SW_CLEANUP_SRC="/root/bootstrap/aw-sw-cleanup.js" +HOST_GROUPS_SRC="/root/bootstrap/aw-host-groups.json" INDEX_HTML="$WEBUI_DIR/index.html" SERVICE_WORKER="$WEBUI_DIR/service-worker.js" TS=$(date +%Y%m%d%H%M%S) @@ -22,11 +23,13 @@ TIMESPIRAL_REPLACEMENT='start:new Date(Date.now()-12*36e5)' [[ -f "$PATCH_JS_SRC" ]] || { echo "missing $PATCH_JS_SRC" >&2; exit 1; } [[ -f "$SW_CLEANUP_SRC" ]] || { echo "missing $SW_CLEANUP_SRC" >&2; exit 1; } +[[ -f "$HOST_GROUPS_SRC" ]] || { echo "missing $HOST_GROUPS_SRC" >&2; exit 1; } [[ -f "$INDEX_HTML" ]] || { echo "missing $INDEX_HTML" >&2; exit 1; } install -d "$WEBUI_DIR/js" install -m 0644 "$PATCH_JS_SRC" "$WEBUI_DIR/js/aw-ru-patch.js" install -m 0644 "$SW_CLEANUP_SRC" "$WEBUI_DIR/js/aw-sw-cleanup.js" +install -m 0644 "$HOST_GROUPS_SRC" "$WEBUI_DIR/js/aw-host-groups.json" cp "$INDEX_HTML" "$INDEX_HTML.bak.$TS" sed -i '/aw-ru-patch.js/d;/aw-sw-cleanup.js/d' "$INDEX_HTML" diff --git a/aw-server/aw-host-groups.json b/aw-server/aw-host-groups.json new file mode 100644 index 0000000..fbc2e9c --- /dev/null +++ b/aw-server/aw-host-groups.json @@ -0,0 +1,30 @@ +{ + "groups": [ + { + "id": "windows-rdp", + "name": "Windows RDP", + "description": "Пользовательские Windows/RDP хосты с активностью, DLP и рабочим временем.", + "patterns": [ + "^(SHARKON|WIN|RDP|TERM|TS-|WS-)" + ], + "links": [ + { "label": "Активность", "type": "activity" }, + { "label": "DLP", "type": "bucket", "bucket_prefix": "aw-dlp-endpoint-signals_" } + ] + }, + { + "id": "virtual-infra", + "name": "Virtual servers + Proxmox", + "description": "Инфраструктурные VM и сетевые узлы. Здесь должны лежать Proxmox, pfSense, Debian и Ubuntu серверы.", + "patterns": [ + "^(PFSENSE|PVE|PROXMOX|DEBIAN|UBUNTU|LINUX|VM-|SRV-|INFRA-)" + ], + "links": [ + { "label": "pfSense health", "type": "bucket", "bucket_prefix": "aw-pfsense-health_" }, + { "label": "pfSense gateways", "type": "bucket", "bucket_prefix": "aw-pfsense-gateways_" }, + { "label": "Все бакеты", "type": "buckets" } + ] + } + ], + "ungrouped_name": "Прочие хосты" +} diff --git a/aw-server/aw-ru-patch.js b/aw-server/aw-ru-patch.js index e0a690d..2111486 100755 --- a/aw-server/aw-ru-patch.js +++ b/aw-server/aw-ru-patch.js @@ -1,6 +1,6 @@ (function () { - window.__awRuPatchVersion = "template-v4-dlp-review"; - document.documentElement.setAttribute("data-aw-ru-patch", "template-v4-dlp-review"); + window.__awRuPatchVersion = "template-v5-dlp-review-host-groups"; + document.documentElement.setAttribute("data-aw-ru-patch", "template-v5-dlp-review-host-groups"); const exact = new Map([ ["ActivityWatch", "АктивВотч"], @@ -322,7 +322,17 @@ '.aw-ru-dlp-message { margin-top: 8px; font-size: 12px; }', '.aw-ru-dlp-actions { display: flex; gap: 6px; flex-wrap: wrap; }', '.aw-ru-dlp-section { margin-top: 18px; }', - '.aw-ru-dlp-section h5 { margin: 0 0 8px; }' + '.aw-ru-dlp-section h5 { margin: 0 0 8px; }', + '.aw-ru-host-groups { margin: 16px 0; padding: 16px; border: 1px solid rgba(120,120,120,.35); border-radius: 8px; background: rgba(20,20,20,.03); }', + '.aw-ru-host-groups-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; }', + '.aw-ru-host-group-card { border: 1px solid rgba(120,120,120,.25); border-radius: 8px; padding: 12px; background: rgba(255,255,255,.02); }', + '.aw-ru-host-group-card h4 { margin: 0 0 8px; }', + '.aw-ru-host-group-card p { margin: 0 0 12px; font-size: 13px; opacity: .85; }', + '.aw-ru-host-list { display: flex; flex-direction: column; gap: 8px; }', + '.aw-ru-host-item { border: 1px solid rgba(120,120,120,.2); border-radius: 6px; padding: 8px; }', + '.aw-ru-host-item-title { font-weight: 600; margin-bottom: 6px; }', + '.aw-ru-host-links { display: flex; flex-wrap: wrap; gap: 6px; }', + '.aw-ru-host-links a { display: inline-block; padding: 4px 8px; border-radius: 999px; background: rgba(90,140,255,.15); text-decoration: none; }' ].join("\n"); document.head.appendChild(style); } @@ -619,6 +629,215 @@ } } + function isHomeRoute() { + const hash = window.location.hash || ""; + return !hash || /^#\/home(?:[/?#]|$)/i.test(hash); + } + + function getDefaultHostGroupsConfig() { + return { + groups: [ + { + id: "windows-rdp", + name: "Windows RDP", + description: "Пользовательские Windows/RDP хосты.", + patterns: ["^(SHARKON|WIN|RDP|TERM|TS-|WS-)"], + links: [ + { label: "Активность", type: "activity" }, + { label: "DLP", type: "bucket", bucket_prefix: "aw-dlp-endpoint-signals_" } + ] + }, + { + id: "virtual-infra", + name: "Virtual servers + Proxmox", + description: "Инфраструктурные VM, Proxmox и сетевые узлы.", + patterns: ["^(PFSENSE|PVE|PROXMOX|DEBIAN|UBUNTU|LINUX|VM-|SRV-|INFRA-)"], + links: [ + { label: "pfSense health", type: "bucket", bucket_prefix: "aw-pfsense-health_" }, + { label: "pfSense gateways", type: "bucket", bucket_prefix: "aw-pfsense-gateways_" }, + { label: "Все бакеты", type: "buckets" } + ] + } + ], + ungrouped_name: "Прочие хосты" + }; + } + + function getHostGroupsState() { + if (!window.__awRuHostGroupsState) { + window.__awRuHostGroupsState = { + config: null, + buckets: null, + loading: false + }; + } + return window.__awRuHostGroupsState; + } + + async function ensureHostGroupsData() { + const state = getHostGroupsState(); + if (state.loading) return state; + if (state.config && state.buckets) return state; + state.loading = true; + try { + if (!state.config) { + try { + state.config = await awApiJson("/js/aw-host-groups.json?v=" + encodeURIComponent(window.__awRuPatchVersion), { method: "GET", headers: {} }); + } catch (error) { + state.config = getDefaultHostGroupsConfig(); + } + } + if (!state.buckets) { + state.buckets = await awApiJson("/api/0/buckets/", { method: "GET", headers: {} }); + } + } finally { + state.loading = false; + } + return state; + } + + function extractHostFromBucket(bucketId, bucketMeta) { + if (bucketMeta && bucketMeta.hostname) return String(bucketMeta.hostname); + const prefixes = [ + "aw-watcher-window_", + "aw-watcher-afk_", + "aw-dlp-endpoint-signals_", + "aw-dlp-incidents_", + "aw-pfsense-health_", + "aw-pfsense-gateways_", + "aw-pfsense-interfaces_" + ]; + for (const prefix of prefixes) { + if (bucketId.indexOf(prefix) === 0) { + return bucketId.slice(prefix.length); + } + } + return ""; + } + + function buildHostBucketMap(rawBuckets) { + const result = new Map(); + const entries = Array.isArray(rawBuckets) + ? rawBuckets.map(function (item) { return [item.id || "", item]; }) + : Object.entries(rawBuckets || {}); + entries.forEach(function (entry) { + const bucketId = entry[0]; + const meta = entry[1] || {}; + const host = extractHostFromBucket(bucketId, meta); + if (!host) return; + if (!result.has(host)) result.set(host, []); + result.get(host).push(bucketId); + }); + return result; + } + + function matchHostGroup(host, groups) { + for (const group of groups) { + const patterns = Array.isArray(group.patterns) ? group.patterns : []; + for (const pattern of patterns) { + try { + if (new RegExp(pattern, "i").test(host)) { + return group.id; + } + } catch (error) { + } + } + } + return ""; + } + + function buildHostLink(host, hostBuckets, linkDef) { + if (!linkDef || !linkDef.type) return ""; + if (linkDef.type === "activity") { + return '#/activity/' + encodeURIComponent(host) + '/day/' + encodeURIComponent(new Date().toISOString().slice(0, 10)) + '/view/summary'; + } + if (linkDef.type === "buckets") { + return "#/buckets"; + } + if (linkDef.type === "bucket" && linkDef.bucket_prefix) { + const bucketId = String(linkDef.bucket_prefix) + host; + return hostBuckets.indexOf(bucketId) >= 0 ? '#/buckets/' + encodeURIComponent(bucketId) : ""; + } + return ""; + } + + function renderHostGroupCards(state) { + const config = state.config || getDefaultHostGroupsConfig(); + const groups = Array.isArray(config.groups) ? config.groups : []; + const hostBuckets = buildHostBucketMap(state.buckets); + const grouped = new Map(); + + groups.forEach(function (group) { + grouped.set(group.id, []); + }); + grouped.set("__ungrouped__", []); + + Array.from(hostBuckets.keys()).sort().forEach(function (host) { + const groupId = matchHostGroup(host, groups) || "__ungrouped__"; + grouped.get(groupId).push(host); + }); + + const cards = []; + groups.forEach(function (group) { + const hosts = grouped.get(group.id) || []; + const items = hosts.map(function (host) { + const links = (group.links || []).map(function (linkDef) { + const href = buildHostLink(host, hostBuckets.get(host) || [], linkDef); + return href ? '' + escapeHtml(linkDef.label || "Открыть") + '' : ""; + }).filter(Boolean).join(""); + return '
' + escapeHtml(group.description || "") + '
' + + 'Хосты, которые пока не попали под шаблоны группировки.
' + + 'Здесь хосты разделены на пользовательские Windows RDP и инфраструктурные виртуальные серверы/Proxmox.
' + + 'Загрузка...