feat(pfsense): add AW poller and host group sections
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
- `proxmox/` — шаблонные скрипты подготовки и наполнения CT на стороне Proxmox.
|
- `proxmox/` — шаблонные скрипты подготовки и наполнения CT на стороне Proxmox.
|
||||||
- `aw-server/` — установочные скрипты, env-шаблон, systemd unit и RU patch для Web UI.
|
- `aw-server/` — установочные скрипты, env-шаблон, systemd unit и RU patch для Web UI.
|
||||||
- `ansible/` — Ansible-ensemble для автоматизированного сервера (Debian/CT).
|
- `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_*`).
|
- `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-пайплайн проверок.
|
- `scripts/quality-gate.sh` — локальный preflight-пайплайн проверок.
|
||||||
|
|
||||||
@@ -37,6 +38,10 @@
|
|||||||
|
|
||||||
- `ansible/deploy_aw_windows_phase2.yml`
|
- `ansible/deploy_aw_windows_phase2.yml`
|
||||||
|
|
||||||
|
Для внешнего pfSense poller'а:
|
||||||
|
|
||||||
|
- `ansible/deploy_aw_pfsense_poller.yml`
|
||||||
|
|
||||||
Скрипты `proxmox/create-ct.sh` и `proxmox/push-aw-artifacts.sh` по умолчанию читают:
|
Скрипты `proxmox/create-ct.sh` и `proxmox/push-aw-artifacts.sh` по умолчанию читают:
|
||||||
|
|
||||||
- `secrets/deploy.secrets.env`
|
- `secrets/deploy.secrets.env`
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
- деплой на уже существующий Debian host/CT;
|
- деплой на уже существующий Debian host/CT;
|
||||||
- полный цикл с нуля в Proxmox: создание CT + bootstrap + установка ActivityWatch + RU patch.
|
- полный цикл с нуля в Proxmox: создание CT + bootstrap + установка ActivityWatch + RU patch.
|
||||||
- централизованный деплой Windows phase-2 collectors по WinRM.
|
- централизованный деплой 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_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/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_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/inventory.example.ini` — шаблон inventory.
|
||||||
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/all.example.yml` — шаблон переменных.
|
- `/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.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/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/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_incident_artifacts_root: 'C:\...\incident-artifacts'` — переопределить путь артефактов;
|
||||||
- `aw_windows_skip_hardening: true` — пропустить `hardening-recovery.ps1` внутри ensemble-скрипта.
|
- `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.
|
- Установлен ActivityWatch Server.
|
||||||
- Создан systemd-unit `activitywatch-server.service`.
|
- Создан systemd-unit `activitywatch-server.service`.
|
||||||
- Установлен RU Web UI patch.
|
- Установлен RU Web UI patch.
|
||||||
- Для Web UI используется checksum-based cache-bust для `ru-patch-v5.js` и `sw-cleanup.js`, чтобы браузер не держал старую DLP/русскую статику после деплоя.
|
- Для 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`.
|
- Выполнена валидация API `http://127.0.0.1:5600/api/0/info`.
|
||||||
- Для full-stack сценария CT создаётся автоматически через `pct create`.
|
- Для full-stack сценария CT создаётся автоматически через `pct create`.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
aw_bootstrap_dir: "/tmp/aw-rus-bootstrap"
|
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_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_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_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 }}"
|
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/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-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-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:
|
notify:
|
||||||
- Reload systemd
|
- Reload systemd
|
||||||
- Restart activitywatch
|
- Restart activitywatch
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -12,6 +12,7 @@ source "$ENV_FILE"
|
|||||||
WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-/opt/activitywatch/webui-ru}"
|
WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-/opt/activitywatch/webui-ru}"
|
||||||
PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js"
|
PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js"
|
||||||
SW_CLEANUP_SRC="/root/bootstrap/aw-sw-cleanup.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"
|
INDEX_HTML="$WEBUI_DIR/index.html"
|
||||||
SERVICE_WORKER="$WEBUI_DIR/service-worker.js"
|
SERVICE_WORKER="$WEBUI_DIR/service-worker.js"
|
||||||
TS=$(date +%Y%m%d%H%M%S)
|
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 "$PATCH_JS_SRC" ]] || { echo "missing $PATCH_JS_SRC" >&2; exit 1; }
|
||||||
[[ -f "$SW_CLEANUP_SRC" ]] || { echo "missing $SW_CLEANUP_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; }
|
[[ -f "$INDEX_HTML" ]] || { echo "missing $INDEX_HTML" >&2; exit 1; }
|
||||||
|
|
||||||
install -d "$WEBUI_DIR/js"
|
install -d "$WEBUI_DIR/js"
|
||||||
install -m 0644 "$PATCH_JS_SRC" "$WEBUI_DIR/js/aw-ru-patch.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 "$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"
|
cp "$INDEX_HTML" "$INDEX_HTML.bak.$TS"
|
||||||
|
|
||||||
sed -i '/aw-ru-patch.js/d;/aw-sw-cleanup.js/d' "$INDEX_HTML"
|
sed -i '/aw-ru-patch.js/d;/aw-sw-cleanup.js/d' "$INDEX_HTML"
|
||||||
|
|||||||
@@ -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": "Прочие хосты"
|
||||||
|
}
|
||||||
+223
-3
@@ -1,6 +1,6 @@
|
|||||||
(function () {
|
(function () {
|
||||||
window.__awRuPatchVersion = "template-v4-dlp-review";
|
window.__awRuPatchVersion = "template-v5-dlp-review-host-groups";
|
||||||
document.documentElement.setAttribute("data-aw-ru-patch", "template-v4-dlp-review");
|
document.documentElement.setAttribute("data-aw-ru-patch", "template-v5-dlp-review-host-groups");
|
||||||
|
|
||||||
const exact = new Map([
|
const exact = new Map([
|
||||||
["ActivityWatch", "АктивВотч"],
|
["ActivityWatch", "АктивВотч"],
|
||||||
@@ -322,7 +322,17 @@
|
|||||||
'.aw-ru-dlp-message { margin-top: 8px; font-size: 12px; }',
|
'.aw-ru-dlp-message { margin-top: 8px; font-size: 12px; }',
|
||||||
'.aw-ru-dlp-actions { display: flex; gap: 6px; flex-wrap: wrap; }',
|
'.aw-ru-dlp-actions { display: flex; gap: 6px; flex-wrap: wrap; }',
|
||||||
'.aw-ru-dlp-section { margin-top: 18px; }',
|
'.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");
|
].join("\n");
|
||||||
document.head.appendChild(style);
|
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 ? '<a href="' + escapeHtml(href) + '">' + escapeHtml(linkDef.label || "Открыть") + '</a>' : "";
|
||||||
|
}).filter(Boolean).join("");
|
||||||
|
return '<div class="aw-ru-host-item">' +
|
||||||
|
'<div class="aw-ru-host-item-title">' + escapeHtml(host) + '</div>' +
|
||||||
|
'<div class="aw-ru-host-links">' + links + '</div>' +
|
||||||
|
'</div>';
|
||||||
|
}).join("");
|
||||||
|
cards.push(
|
||||||
|
'<section class="aw-ru-host-group-card">' +
|
||||||
|
'<h4>' + escapeHtml(group.name || group.id) + '</h4>' +
|
||||||
|
'<p>' + escapeHtml(group.description || "") + '</p>' +
|
||||||
|
'<div class="aw-ru-host-list">' + (items || '<div class="aw-ru-host-item">Хосты пока не обнаружены.</div>') + '</div>' +
|
||||||
|
'</section>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ungroupedHosts = grouped.get("__ungrouped__") || [];
|
||||||
|
if (ungroupedHosts.length) {
|
||||||
|
cards.push(
|
||||||
|
'<section class="aw-ru-host-group-card">' +
|
||||||
|
'<h4>' + escapeHtml(config.ungrouped_name || "Прочие хосты") + '</h4>' +
|
||||||
|
'<p>Хосты, которые пока не попали под шаблоны группировки.</p>' +
|
||||||
|
'<div class="aw-ru-host-list">' +
|
||||||
|
ungroupedHosts.map(function (host) {
|
||||||
|
return '<div class="aw-ru-host-item"><div class="aw-ru-host-item-title">' + escapeHtml(host) + '</div></div>';
|
||||||
|
}).join("") +
|
||||||
|
'</div>' +
|
||||||
|
'</section>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function injectHostGroupsCenter(root) {
|
||||||
|
if (!isHomeRoute()) return;
|
||||||
|
const heading = root.querySelector("h3");
|
||||||
|
if (!heading) return;
|
||||||
|
|
||||||
|
let center = root.querySelector("[data-aw-ru-host-groups='1']");
|
||||||
|
if (!center) {
|
||||||
|
center = document.createElement("section");
|
||||||
|
center.className = "aw-ru-host-groups";
|
||||||
|
center.setAttribute("data-aw-ru-host-groups", "1");
|
||||||
|
center.innerHTML =
|
||||||
|
'<h4>Разделы хостов</h4>' +
|
||||||
|
'<p>Здесь хосты разделены на пользовательские Windows RDP и инфраструктурные виртуальные серверы/Proxmox.</p>' +
|
||||||
|
'<div class="aw-ru-host-groups-grid" data-aw-ru-host-groups-grid><section class="aw-ru-host-group-card"><p>Загрузка...</p></section></div>';
|
||||||
|
heading.parentElement.insertBefore(center, heading.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = await ensureHostGroupsData();
|
||||||
|
center.querySelector("[data-aw-ru-host-groups-grid]").innerHTML = renderHostGroupCards(state);
|
||||||
|
}
|
||||||
|
|
||||||
function renderDlpTableRows(center, host) {
|
function renderDlpTableRows(center, host) {
|
||||||
const state = getSuppressionState();
|
const state = getSuppressionState();
|
||||||
const tbody = center.querySelector("[data-aw-ru-dlp-events]");
|
const tbody = center.querySelector("[data-aw-ru-dlp-events]");
|
||||||
@@ -1144,6 +1363,7 @@
|
|||||||
injectDlpNavigation(document.body);
|
injectDlpNavigation(document.body);
|
||||||
injectDlpReviewCenter(document.body);
|
injectDlpReviewCenter(document.body);
|
||||||
injectDlpAlertsCenter(document.body);
|
injectDlpAlertsCenter(document.body);
|
||||||
|
injectHostGroupsCenter(document.body).catch(function () {});
|
||||||
redirectBareTrendsRoute();
|
redirectBareTrendsRoute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# pfSense -> ActivityWatch
|
||||||
|
|
||||||
|
Для `pfSense` используется внешний poller на `Debian/Ubuntu` utility VM. Сам firewall не трогаем Windows-агентами и не превращаем в desktop-client.
|
||||||
|
|
||||||
|
## Схема
|
||||||
|
|
||||||
|
- `pfSense` отвечает по API.
|
||||||
|
- `Debian/Ubuntu` poller VM опрашивает его с интервалом.
|
||||||
|
- poller отправляет события в общий `AW server`.
|
||||||
|
|
||||||
|
## Bucket'ы
|
||||||
|
|
||||||
|
Базовая конфигурация пишет:
|
||||||
|
|
||||||
|
- `aw-pfsense-health_<HOST>`
|
||||||
|
- `aw-pfsense-interfaces_<HOST>`
|
||||||
|
- `aw-pfsense-gateways_<HOST>`
|
||||||
|
|
||||||
|
## Файлы
|
||||||
|
|
||||||
|
- [pfsense-aw-poller.py](/home/igor/tmp/AWatch-rus/pfsense/pfsense-aw-poller.py)
|
||||||
|
- [pfsense-aw-poller.service](/home/igor/tmp/AWatch-rus/pfsense/pfsense-aw-poller.service)
|
||||||
|
- [pfsense-aw-poller.example.json](/home/igor/tmp/AWatch-rus/pfsense/pfsense-aw-poller.example.json)
|
||||||
|
- [deploy_aw_pfsense_poller.yml](/home/igor/tmp/AWatch-rus/ansible/deploy_aw_pfsense_poller.yml)
|
||||||
|
- [pfsense-poller.example.yml](/home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.example.yml)
|
||||||
|
|
||||||
|
## Ручной запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo install -d /opt/aw-pfsense /etc/aw-pfsense
|
||||||
|
sudo cp /path/to/pfsense-aw-poller.py /opt/aw-pfsense/
|
||||||
|
sudo chmod 0755 /opt/aw-pfsense/pfsense-aw-poller.py
|
||||||
|
sudo cp /path/to/poller.json /etc/aw-pfsense/poller.json
|
||||||
|
python3 /opt/aw-pfsense/pfsense-aw-poller.py --config /etc/aw-pfsense/poller.json --once
|
||||||
|
```
|
||||||
|
|
||||||
|
## Через Ansible
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/igor/tmp/AWatch-rus/ansible
|
||||||
|
cp group_vars/pfsense-poller.example.yml group_vars/pfsense-poller.yml
|
||||||
|
ansible-playbook -i inventory.ini deploy_aw_pfsense_poller.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Inventory:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[aw_pfsense_pollers]
|
||||||
|
aw-poller-01 ansible_host=10.10.10.50
|
||||||
|
```
|
||||||
|
|
||||||
|
## AW Web
|
||||||
|
|
||||||
|
На `#/home` добавлен отдельный блок:
|
||||||
|
|
||||||
|
- `Windows RDP`
|
||||||
|
- `Virtual servers + Proxmox`
|
||||||
|
|
||||||
|
Для `pfSense` задавай hostname вида `PFSENSE-EDGE01`, тогда он попадёт во вторую группу.
|
||||||
Binary file not shown.
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now_iso():
|
||||||
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
||||||
|
|
||||||
|
|
||||||
|
def load_json(path):
|
||||||
|
return json.loads(Path(path).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def build_ssl_context(verify_tls):
|
||||||
|
if verify_tls:
|
||||||
|
return None
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
context.check_hostname = False
|
||||||
|
context.verify_mode = ssl.CERT_NONE
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def http_json(url, method="GET", headers=None, body=None, timeout=15, ssl_context=None):
|
||||||
|
payload = None
|
||||||
|
effective_headers = {"Content-Type": "application/json; charset=utf-8"}
|
||||||
|
if headers:
|
||||||
|
effective_headers.update(headers)
|
||||||
|
if body is not None:
|
||||||
|
payload = json.dumps(body).encode("utf-8")
|
||||||
|
request = urllib.request.Request(url, data=payload, headers=effective_headers, method=method)
|
||||||
|
with urllib.request.urlopen(request, timeout=timeout, context=ssl_context) as response:
|
||||||
|
raw = response.read().decode("utf-8")
|
||||||
|
return json.loads(raw) if raw else None
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_bucket(aw_base_url, bucket_id, client_name, bucket_type, hostname, timeout, ssl_context):
|
||||||
|
http_json(
|
||||||
|
f"{aw_base_url}/buckets/{urllib.parse.quote(bucket_id, safe='')}",
|
||||||
|
method="POST",
|
||||||
|
body={
|
||||||
|
"client": client_name,
|
||||||
|
"type": bucket_type,
|
||||||
|
"hostname": hostname,
|
||||||
|
},
|
||||||
|
timeout=timeout,
|
||||||
|
ssl_context=ssl_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_heartbeat(aw_base_url, bucket_id, event, pulse_time, timeout, ssl_context):
|
||||||
|
http_json(
|
||||||
|
f"{aw_base_url}/buckets/{urllib.parse.quote(bucket_id, safe='')}/heartbeat?pulsetime={pulse_time}",
|
||||||
|
method="POST",
|
||||||
|
body=event,
|
||||||
|
timeout=timeout,
|
||||||
|
ssl_context=ssl_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_headers(config):
|
||||||
|
headers = dict(config.get("headers") or {})
|
||||||
|
auth = config.get("auth") or {}
|
||||||
|
bearer_token = auth.get("bearer_token")
|
||||||
|
basic = auth.get("basic")
|
||||||
|
if bearer_token:
|
||||||
|
headers["Authorization"] = f"Bearer {bearer_token}"
|
||||||
|
elif basic and basic.get("username") and basic.get("password"):
|
||||||
|
import base64
|
||||||
|
token = base64.b64encode(f"{basic['username']}:{basic['password']}".encode("utf-8")).decode("ascii")
|
||||||
|
headers["Authorization"] = f"Basic {token}"
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_payload(payload):
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
return {
|
||||||
|
"keys": sorted(payload.keys())[:50],
|
||||||
|
"size": len(json.dumps(payload, ensure_ascii=False)),
|
||||||
|
}
|
||||||
|
if isinstance(payload, list):
|
||||||
|
return {
|
||||||
|
"items": len(payload),
|
||||||
|
"sample_type": type(payload[0]).__name__ if payload else "none",
|
||||||
|
"size": len(json.dumps(payload, ensure_ascii=False)),
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"type": type(payload).__name__,
|
||||||
|
"value": str(payload)[:400],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def poll_endpoint(config, endpoint, aw_base_url, aw_timeout, pf_timeout, ssl_context):
|
||||||
|
pf_host = config["pfsense"]["host"]
|
||||||
|
scheme = config["pfsense"].get("scheme", "https")
|
||||||
|
hostname = config["aw"]["hostname"]
|
||||||
|
path = endpoint["path"]
|
||||||
|
url = f"{scheme}://{pf_host}{path}"
|
||||||
|
headers = normalize_headers(config["pfsense"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = http_json(
|
||||||
|
url,
|
||||||
|
method=endpoint.get("method", "GET"),
|
||||||
|
headers=headers,
|
||||||
|
timeout=pf_timeout,
|
||||||
|
ssl_context=ssl_context,
|
||||||
|
)
|
||||||
|
bucket_prefix = endpoint["bucket_prefix"]
|
||||||
|
bucket_id = f"{bucket_prefix}_{hostname}"
|
||||||
|
ensure_bucket(
|
||||||
|
aw_base_url,
|
||||||
|
bucket_id,
|
||||||
|
endpoint.get("client", bucket_prefix),
|
||||||
|
endpoint.get("bucket_type", "aw.pfsense.metric"),
|
||||||
|
hostname,
|
||||||
|
aw_timeout,
|
||||||
|
ssl_context,
|
||||||
|
)
|
||||||
|
event = {
|
||||||
|
"timestamp": utc_now_iso(),
|
||||||
|
"duration": 0,
|
||||||
|
"data": {
|
||||||
|
"source": "pfsense-aw-poller",
|
||||||
|
"target": {
|
||||||
|
"host": pf_host,
|
||||||
|
"name": config["pfsense"].get("name", hostname),
|
||||||
|
},
|
||||||
|
"endpoint": {
|
||||||
|
"path": path,
|
||||||
|
"method": endpoint.get("method", "GET"),
|
||||||
|
"name": endpoint.get("name", bucket_prefix),
|
||||||
|
},
|
||||||
|
"summary": summarize_payload(payload),
|
||||||
|
"payload": payload,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
send_heartbeat(
|
||||||
|
aw_base_url,
|
||||||
|
bucket_id,
|
||||||
|
event,
|
||||||
|
endpoint.get("pulse_time_seconds", config["aw"].get("pulse_time_seconds", 120)),
|
||||||
|
aw_timeout,
|
||||||
|
ssl_context,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"endpoint": path,
|
||||||
|
"bucket_id": bucket_id,
|
||||||
|
"status": "ok",
|
||||||
|
}
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
return {
|
||||||
|
"endpoint": path,
|
||||||
|
"status": "http_error",
|
||||||
|
"code": error.code,
|
||||||
|
"reason": str(error),
|
||||||
|
}
|
||||||
|
except Exception as error:
|
||||||
|
return {
|
||||||
|
"endpoint": path,
|
||||||
|
"status": "error",
|
||||||
|
"reason": str(error),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_once(config):
|
||||||
|
aw = config["aw"]
|
||||||
|
aw_base_url = f"http://{aw['server_host']}:{aw.get('server_port', 5600)}/api/0"
|
||||||
|
aw_timeout = int(aw.get("timeout_seconds", 15))
|
||||||
|
pf_timeout = int(config["pfsense"].get("timeout_seconds", 15))
|
||||||
|
verify_tls = bool(config["pfsense"].get("verify_tls", False))
|
||||||
|
ssl_context = build_ssl_context(verify_tls)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for endpoint in config.get("endpoints") or []:
|
||||||
|
results.append(poll_endpoint(config, endpoint, aw_base_url, aw_timeout, pf_timeout, ssl_context))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Poll pfSense API and forward selected telemetry to ActivityWatch.")
|
||||||
|
parser.add_argument("--config", required=True, help="Path to JSON config.")
|
||||||
|
parser.add_argument("--once", action="store_true", help="Run one cycle and exit.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
config = load_json(args.config)
|
||||||
|
interval = int(config.get("poll_interval_seconds", 60))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
results = run_once(config)
|
||||||
|
print(json.dumps({"timestamp": utc_now_iso(), "results": results}, ensure_ascii=False), flush=True)
|
||||||
|
if args.once:
|
||||||
|
return 0
|
||||||
|
time.sleep(max(interval, 15))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=pfSense ActivityWatch poller
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/python3 /opt/aw-pfsense/pfsense-aw-poller.py --config /etc/aw-pfsense/poller.json
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user