Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43f16213bd | ||
|
|
3e0f3576e3 | ||
|
|
9b02d65f76 | ||
|
|
8278d51840 | ||
|
|
fa4bf96ebf | ||
|
|
b28cfabd57 | ||
|
|
4936d6bca3 | ||
|
|
c54b237f80 | ||
|
|
48223fbeeb | ||
|
|
7f131a6310 | ||
|
|
77591c10ce | ||
|
|
947717251f | ||
|
|
0da0d880c0 |
@@ -10,16 +10,19 @@
|
||||
- `docs/operations.md` — регламент сопровождения, бэкапов, обновлений и rollback.
|
||||
- `docs/windows/ensemble.md` — orchestration-пакет для Windows-деплоя и проверки.
|
||||
- `docs/linux-client.md` — user-space rollout Linux-клиента ActivityWatch на удалённый `AW server`.
|
||||
- `docs/linux-remote-worker.md` — полный Linux remote-worker stack: GUI, SSH/console и browser admin UI вроде Proxmox `:8006`.
|
||||
- `docs/console-ssh-logger.md` — логирование только консольных команд и SSH-сессий в AW.
|
||||
- `docs/dlp-gap-analysis.md` — разрыв до enterprise DLP и roadmap.
|
||||
- `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_*`).
|
||||
- `windows/` — PowerShell toolkit: single-user, domain-users, ensemble orchestration, hardening/recovery, validation, phase-2 DLP telemetry (`aw-dlp-incidents_*`, `aw-dlp-endpoint-signals_*`) и session-level presence для удалённых Windows/RDP пользователей (`aw-worktime-sessions_*`).
|
||||
- `scripts/quality-gate.sh` — локальный preflight-пайплайн проверок.
|
||||
- `scripts/install_aw_linux_client.sh` — установка Linux bundle + autostart для remote AW server.
|
||||
- `scripts/install_aw_console_ssh_logger.sh` — user-space установка console/ssh logger.
|
||||
- `scripts/install_aw_linux_web_category_logger.sh` — user-space классификация browser admin UI по title/class.
|
||||
- `scripts/install_aw_linux_remote_worker.sh` — полный Linux remote-worker installer.
|
||||
|
||||
## Базовый сценарий
|
||||
|
||||
@@ -51,6 +54,11 @@
|
||||
- `docs/linux-client.md`
|
||||
- `scripts/install_aw_linux_client.sh`
|
||||
|
||||
Для полного Linux remote-worker сценария:
|
||||
|
||||
- `docs/linux-remote-worker.md`
|
||||
- `scripts/install_aw_linux_remote_worker.sh`
|
||||
|
||||
Для режима “только консоль/ssh” без GUI watcher'ов:
|
||||
|
||||
- `docs/console-ssh-logger.md`
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
REPO_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
|
||||
cd "$REPO_DIR"
|
||||
|
||||
echo "==> Fetch origin"
|
||||
git fetch origin
|
||||
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
echo "==> Pull origin/$CURRENT_BRANCH"
|
||||
git pull --ff-only origin "$CURRENT_BRANCH"
|
||||
|
||||
echo "==> Done"
|
||||
git status --short --branch
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
REPO_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
|
||||
cd "$REPO_DIR"
|
||||
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
if [ "${1:-}" = "" ]; then
|
||||
echo "Usage: $0 \"commit message\"" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
COMMIT_MESSAGE=$1
|
||||
|
||||
echo "==> Git status"
|
||||
git status --short --branch
|
||||
|
||||
echo "==> Stage changes"
|
||||
git add -A
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
echo "No staged changes to commit."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "==> Commit"
|
||||
git commit -m "$COMMIT_MESSAGE"
|
||||
|
||||
echo "==> Push origin/$BRANCH"
|
||||
git push origin "$BRANCH"
|
||||
|
||||
echo "==> Done"
|
||||
@@ -14,6 +14,7 @@
|
||||
- `/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/install_full_stack.yml` — полный установочный playbook (оркестратор всех этапов).
|
||||
- `/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.
|
||||
@@ -34,6 +35,24 @@ cd /home/igor/tmp/AWatch-rus/ansible
|
||||
ansible-playbook -i inventory.ini deploy_aw_server.yml
|
||||
```
|
||||
|
||||
## Полный установочный playbook (всё за один запуск)
|
||||
|
||||
Если нужно прогнать полный цикл одной командой:
|
||||
|
||||
```bash
|
||||
cd /home/igor/tmp/AWatch-rus/ansible
|
||||
ansible-playbook -i inventory.ini install_full_stack.yml
|
||||
```
|
||||
|
||||
Что делает:
|
||||
|
||||
- `provision_proxmox_ct_and_deploy_aw.yml` (если есть хосты в группе `[proxmox]`);
|
||||
- `deploy_aw_server.yml` (группа `[aw_server]`);
|
||||
- `deploy_aw_windows_phase2.yml` (группа `[aw_windows]`);
|
||||
- `deploy_aw_pfsense_poller.yml` (группа `[aw_pfsense_pollers]`).
|
||||
|
||||
Пустые группы в `inventory.ini` безопасны: соответствующий play будет пропущен.
|
||||
|
||||
## Полный запуск с нуля в Proxmox
|
||||
|
||||
1. Подготовьте inventory и vars:
|
||||
@@ -66,6 +85,8 @@ ansible-playbook -i inventory.ini provision_proxmox_ct_matrix_and_deploy_aw.yml
|
||||
- `cp /home/igor/tmp/AWatch-rus/ansible/inventory.example.ini /home/igor/tmp/AWatch-rus/ansible/inventory.ini`
|
||||
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/windows.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/windows.yml`
|
||||
2. Заполните `inventory.ini` (секция `[aw_windows]`) и `group_vars/windows.yml`.
|
||||
- Для русской локализации Windows часто нужен `ansible_user=Администратор` (а не `Administrator`).
|
||||
- Если WinRM закрыт, playbook не сможет стартовать и нужно сначала открыть `5985/5986` и `wsman`.
|
||||
3. Запустите:
|
||||
|
||||
```bash
|
||||
@@ -77,6 +98,8 @@ Playbook:
|
||||
|
||||
- выгружает `windows/*` toolkit на целевой хост в `C:\Deploy\AWatch-rus\windows`;
|
||||
- выполняет `deploy-ensemble.ps1` (deploy + hardening/recovery) с phase-2 policy/rules;
|
||||
- после deploy принудительно запускает `ActivityWatch Recovery` и все `ActivityWatch Launch *` задачи;
|
||||
- выполняет API smoke-check bucket `aw-watcher-afk_SHARKON2025` и ожидает свежие `not-afk` события;
|
||||
- запускает `validate-deployment.ps1`;
|
||||
- забирает JSON-отчёт в локальную директорию (`/tmp/aw-rus-validation` по умолчанию).
|
||||
|
||||
|
||||
@@ -31,6 +31,12 @@
|
||||
aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
|
||||
aw_windows_validation_remote_path: "C:\\Windows\\Temp\\aw_validate_phase2_ansible.json"
|
||||
aw_windows_validation_local_dir: "/tmp/aw-rus-validation"
|
||||
aw_windows_launch_task_pattern: "ActivityWatch Launch *"
|
||||
aw_windows_recovery_task_name: "ActivityWatch Recovery"
|
||||
aw_windows_force_task_restart: true
|
||||
aw_windows_api_smoke_check_enabled: true
|
||||
aw_windows_api_smoke_check_bucket: "aw-watcher-afk_SHARKON2025"
|
||||
aw_windows_api_smoke_check_limit: 10
|
||||
|
||||
tasks:
|
||||
- name: Validate required variables
|
||||
@@ -105,6 +111,36 @@
|
||||
{% endif %}
|
||||
& "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params
|
||||
|
||||
- name: Force start ActivityWatch recovery and launch tasks
|
||||
when: aw_windows_force_task_restart | bool
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Start-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}"
|
||||
Get-ScheduledTask |
|
||||
Where-Object TaskName -like "{{ aw_windows_launch_task_pattern }}" |
|
||||
ForEach-Object { Start-ScheduledTask -TaskName $_.TaskName }
|
||||
|
||||
- name: Wait for fresh AFK events to appear on AW server
|
||||
when: aw_windows_api_smoke_check_enabled | bool
|
||||
delegate_to: localhost
|
||||
ansible.builtin.uri:
|
||||
url: "http://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
|
||||
method: GET
|
||||
return_content: true
|
||||
register: aw_windows_api_smoke
|
||||
until: >
|
||||
aw_windows_api_smoke.status == 200 and
|
||||
(aw_windows_api_smoke.json | length) > 0 and
|
||||
(
|
||||
aw_windows_api_smoke.json
|
||||
| selectattr('data.status', 'equalto', 'not-afk')
|
||||
| list
|
||||
| length
|
||||
) > 0
|
||||
retries: 10
|
||||
delay: 6
|
||||
|
||||
- name: Run validation and store report on target
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
# Full-stack installer for AWatch-rus.
|
||||
# Runs end-to-end rollout in one command:
|
||||
# 1) Proxmox CT provision + AW bootstrap (if [proxmox] exists in inventory)
|
||||
# 2) AW server deploy on [aw_server] hosts
|
||||
# 3) Windows phase2 rollout on [aw_windows] hosts
|
||||
# 4) pfSense poller deploy on [aw_pfsense_pollers] hosts
|
||||
#
|
||||
# Notes:
|
||||
# - Keep only relevant inventory groups filled for your environment.
|
||||
# - Plays with unmatched host groups are skipped automatically by Ansible.
|
||||
|
||||
- import_playbook: provision_proxmox_ct_and_deploy_aw.yml
|
||||
- import_playbook: deploy_aw_server.yml
|
||||
- import_playbook: deploy_aw_windows_phase2.yml
|
||||
- import_playbook: deploy_aw_pfsense_poller.yml
|
||||
@@ -5,4 +5,5 @@ pve-main ansible_host=192.168.10.2 ansible_user=root ansible_port=22
|
||||
aw-ct ansible_host=10.20.30.13 ansible_user=root ansible_port=22
|
||||
|
||||
[aw_windows]
|
||||
win-node1 ansible_host=192.168.100.21 ansible_user=Administrator ansible_password=CHANGE_ME ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
|
||||
# NOTE: in RU-localized installs this account is often "Администратор" instead of "Administrator".
|
||||
win-node1 ansible_host=192.168.100.21 ansible_user=Администратор ansible_password=CHANGE_ME ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
- aw-server.env.example
|
||||
- aw-ru-patch.js
|
||||
- aw-sw-cleanup.js
|
||||
- aw-host-groups.json
|
||||
- settings/classes-worktime.json
|
||||
- settings/views-default.json
|
||||
|
||||
tasks:
|
||||
- name: Execute single-CT provisioning workflow
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
- aw-server.env.example
|
||||
- aw-ru-patch.js
|
||||
- aw-sw-cleanup.js
|
||||
- aw-host-groups.json
|
||||
- settings/classes-worktime.json
|
||||
- settings/views-default.json
|
||||
|
||||
tasks:
|
||||
- name: Validate CT matrix is provided
|
||||
|
||||
@@ -9,17 +9,22 @@ fi
|
||||
|
||||
source "$ENV_FILE"
|
||||
|
||||
WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-/opt/activitywatch/webui-ru}"
|
||||
WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-${AW_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)
|
||||
PATCH_TARGET="$WEBUI_DIR/js/ru-patch-v5.js"
|
||||
SW_TARGET="$WEBUI_DIR/js/sw-cleanup.js"
|
||||
HOST_GROUPS_TARGET="$WEBUI_DIR/js/aw-host-groups.json"
|
||||
TRENDS_NEEDLE='this.activityStore.query_category_time_by_period(r)'
|
||||
TRENDS_REPLACEMENT='this.activityStore.ensure_loaded(r)'
|
||||
TIMESPIRAL_NEEDLE='start:new Date("2022-08-08")'
|
||||
TIMESPIRAL_REPLACEMENT='start:new Date(Date.now()-12*36e5)'
|
||||
CATEGORY_HELPER_NEEDLE='hostname:t.hostnameChoices[0]'
|
||||
CATEGORY_HELPER_REPLACEMENT='hostname:t.hostnameChoices.filter((function(t){return"unknown"!==t}))[0]||t.hostnameChoices[0]'
|
||||
|
||||
[[ -f "$PATCH_JS_SRC" ]] || { echo "missing $PATCH_JS_SRC" >&2; exit 1; }
|
||||
[[ -f "$SW_CLEANUP_SRC" ]] || { echo "missing $SW_CLEANUP_SRC" >&2; exit 1; }
|
||||
@@ -27,14 +32,17 @@ TIMESPIRAL_REPLACEMENT='start:new Date(Date.now()-12*36e5)'
|
||||
[[ -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"
|
||||
install -m 0644 "$PATCH_JS_SRC" "$PATCH_TARGET"
|
||||
install -m 0644 "$SW_CLEANUP_SRC" "$SW_TARGET"
|
||||
install -m 0644 "$HOST_GROUPS_SRC" "$HOST_GROUPS_TARGET"
|
||||
cp "$INDEX_HTML" "$INDEX_HTML.bak.$TS"
|
||||
|
||||
sed -i '/aw-ru-patch.js/d;/aw-sw-cleanup.js/d' "$INDEX_HTML"
|
||||
sed -i 's#</head>#<script src="/js/aw-sw-cleanup.js"></script></head>#' "$INDEX_HTML"
|
||||
sed -i 's#</body>#<script defer="defer" src="/js/aw-ru-patch.js"></script></body>#' "$INDEX_HTML"
|
||||
patch_hash="$(sha1sum "$PATCH_TARGET" | awk '{print substr($1,1,12)}')"
|
||||
sw_hash="$(sha1sum "$SW_TARGET" | awk '{print substr($1,1,12)}')"
|
||||
|
||||
sed -i '/ru-patch-v5.js/d;/sw-cleanup.js/d;/aw-ru-patch.js/d;/aw-sw-cleanup.js/d' "$INDEX_HTML"
|
||||
sed -i "s#</head>#<script src=\"/js/sw-cleanup.js?v=$sw_hash\"></script></head>#" "$INDEX_HTML"
|
||||
sed -i "s#</body>#<script defer=\"defer\" src=\"/js/ru-patch-v5.js?v=$patch_hash\"></script></body>#" "$INDEX_HTML"
|
||||
cp "$SW_CLEANUP_SRC" "$SERVICE_WORKER"
|
||||
|
||||
trends_chunk="$(grep -Rsl "$TRENDS_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
|
||||
@@ -79,4 +87,25 @@ else
|
||||
echo "Timespiral hotfix skipped: chunk not found"
|
||||
fi
|
||||
|
||||
echo "RU patch applied to $WEBUI_DIR"
|
||||
category_helper_chunk="$(grep -Rsl "$CATEGORY_HELPER_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
|
||||
if [[ -n "$category_helper_chunk" ]]; then
|
||||
cp "$category_helper_chunk" "$category_helper_chunk.bak.$TS"
|
||||
python3 - "$category_helper_chunk" "$CATEGORY_HELPER_NEEDLE" "$CATEGORY_HELPER_REPLACEMENT" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
old = sys.argv[2]
|
||||
new = sys.argv[3]
|
||||
content = path.read_text()
|
||||
if old in content:
|
||||
path.write_text(content.replace(old, new, 1))
|
||||
print(f"Category helper host hotfix applied to {path}")
|
||||
else:
|
||||
print(f"Category helper host hotfix already present in {path}")
|
||||
PY
|
||||
else
|
||||
echo "Category helper host hotfix skipped: chunk not found"
|
||||
fi
|
||||
|
||||
echo "RU patch applied to $WEBUI_DIR (ru-patch-v5.js?v=$patch_hash)"
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
"^pve-detmir$"
|
||||
],
|
||||
"links": [
|
||||
{ "label": "Активность", "type": "activity" },
|
||||
{ "label": "Активность", "type": "activity", "view": "pve_audit" },
|
||||
{ "label": "Web-admin аудит", "type": "bucket", "bucket_prefix": "aw-pve-webadmin-events_" },
|
||||
{ "label": "PVE tasks", "type": "bucket", "bucket_prefix": "aw-pve-task-events_" },
|
||||
{ "label": "SSH сессии", "type": "bucket", "bucket_prefix": "aw-ssh-sessions_" },
|
||||
{ "label": "Команды shell", "type": "bucket", "bucket_prefix": "aw-console-commands_" },
|
||||
{ "label": "Web категории", "type": "bucket", "bucket_prefix": "aw-detmir-web-category_" },
|
||||
{ "label": "Все бакеты", "type": "buckets" }
|
||||
]
|
||||
@@ -25,6 +29,21 @@
|
||||
{ "label": "DLP", "type": "bucket", "bucket_prefix": "aw-dlp-endpoint-signals_" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "linux-remote",
|
||||
"name": "Linux remote workers",
|
||||
"description": "Linux-хосты удалённых сотрудников: GUI активность, SSH/console и browser admin UI.",
|
||||
"patterns": [
|
||||
"^(LINUX-WS|LINUX-DESKTOP|LX-|DESKTOP-|ADMIN-|WORKSTATION-|DEVBOX-)"
|
||||
],
|
||||
"links": [
|
||||
{ "label": "Активность", "type": "activity" },
|
||||
{ "label": "SSH сессии", "type": "bucket", "bucket_prefix": "aw-ssh-sessions_" },
|
||||
{ "label": "Команды shell", "type": "bucket", "bucket_prefix": "aw-console-commands_" },
|
||||
{ "label": "Web категории", "type": "bucket", "bucket_prefix": "aw-detmir-web-category_" },
|
||||
{ "label": "Все бакеты", "type": "buckets" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "virtual-infra",
|
||||
"name": "Virtual servers + Proxmox",
|
||||
|
||||
+297
-10
@@ -1,6 +1,6 @@
|
||||
(function () {
|
||||
window.__awRuPatchVersion = "template-v6-pve-detmir-client";
|
||||
document.documentElement.setAttribute("data-aw-ru-patch", "template-v6-pve-detmir-client");
|
||||
window.__awRuPatchVersion = "template-v12-activity-heading-ru";
|
||||
document.documentElement.setAttribute("data-aw-ru-patch", "template-v12-activity-heading-ru");
|
||||
|
||||
const exact = new Map([
|
||||
["ActivityWatch", "АктивВотч"],
|
||||
@@ -332,7 +332,15 @@
|
||||
'.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; }'
|
||||
'.aw-ru-host-links a { display: inline-block; padding: 4px 8px; border-radius: 999px; background: rgba(90,140,255,.15); text-decoration: none; }',
|
||||
'.aw-ru-pve-audit { margin: 16px 0; padding: 16px; border: 1px solid rgba(120,120,120,.35); border-radius: 8px; background: rgba(10,20,40,.04); }',
|
||||
'.aw-ru-pve-audit-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin: 12px 0 16px; }',
|
||||
'.aw-ru-pve-audit-card { border: 1px solid rgba(120,120,120,.22); border-radius: 8px; padding: 12px; background: rgba(255,255,255,.02); }',
|
||||
'.aw-ru-pve-audit-card h5 { margin: 0 0 6px; font-size: 13px; opacity: .8; }',
|
||||
'.aw-ru-pve-audit-value { font-size: 24px; font-weight: 700; }',
|
||||
'.aw-ru-pve-audit-table { width: 100%; border-collapse: collapse; margin-top: 8px; }',
|
||||
'.aw-ru-pve-audit-table th, .aw-ru-pve-audit-table td { padding: 6px 8px; border-bottom: 1px solid rgba(120,120,120,.18); vertical-align: top; text-align: left; font-size: 13px; }',
|
||||
'.aw-ru-pve-audit-muted { opacity: .72; font-size: 13px; }'
|
||||
].join("\n");
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
@@ -358,6 +366,24 @@
|
||||
return "";
|
||||
}
|
||||
|
||||
function isPveLikeHost(host) {
|
||||
return /^pve[-_]/i.test(String(host || ""));
|
||||
}
|
||||
|
||||
function enforceSafeActivityViewForPveHost() {
|
||||
const hash = window.location.hash || "";
|
||||
const match = hash.match(/^#\/activity\/([^/]+)\/day\/([^/]+)\/view\/([^/?#]+)/i);
|
||||
if (!match) return;
|
||||
const host = decodeURIComponent(match[1] || "");
|
||||
const day = decodeURIComponent(match[2] || "");
|
||||
const viewId = decodeURIComponent(match[3] || "");
|
||||
if (!isPveLikeHost(host)) return;
|
||||
const safeHash = "#/activity/" + encodeURIComponent(host) + "/day/" + encodeURIComponent(day) + "/view/" + encodeURIComponent("pve_audit");
|
||||
if (safeHash !== hash && !/^pve_audit$/i.test(viewId)) {
|
||||
window.location.replace(safeHash);
|
||||
}
|
||||
}
|
||||
|
||||
function getDlpHostFromSettings(settings) {
|
||||
const routeHost = getCurrentHostFromHash();
|
||||
if (routeHost) return routeHost;
|
||||
@@ -605,7 +631,14 @@
|
||||
}
|
||||
|
||||
function injectDlpNavigation(root) {
|
||||
const href = getDlpHref(window.__awRuPatchSettingsHost || getCurrentHostFromHash());
|
||||
const hostForDlp = window.__awRuPatchSettingsHost || getCurrentHostFromHash();
|
||||
if (hostForDlp && isPveLikeHost(hostForDlp)) {
|
||||
removeBadDlpLinks(root);
|
||||
const ownItem = root.querySelector("[data-aw-ru-dlp-item='1']");
|
||||
if (ownItem) ownItem.remove();
|
||||
return;
|
||||
}
|
||||
const href = getDlpHref(hostForDlp);
|
||||
removeBadDlpLinks(root);
|
||||
updateDlpLinks(root, href);
|
||||
if (root.querySelector("[data-aw-ru-dlp-item='1']")) return;
|
||||
@@ -647,6 +680,19 @@
|
||||
{ label: "DLP", type: "bucket", bucket_prefix: "aw-dlp-endpoint-signals_" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "linux-remote",
|
||||
name: "Linux remote workers",
|
||||
description: "Linux-хосты удалённых сотрудников: GUI активность, SSH/console и browser admin UI.",
|
||||
patterns: ["^(LINUX-WS|LINUX-DESKTOP|LX-|DESKTOP-|ADMIN-|WORKSTATION-|DEVBOX-)"],
|
||||
links: [
|
||||
{ label: "Активность", type: "activity" },
|
||||
{ label: "SSH сессии", type: "bucket", bucket_prefix: "aw-ssh-sessions_" },
|
||||
{ label: "Команды shell", type: "bucket", bucket_prefix: "aw-console-commands_" },
|
||||
{ label: "Web категории", type: "bucket", bucket_prefix: "aw-detmir-web-category_" },
|
||||
{ label: "Все бакеты", type: "buckets" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "virtual-infra",
|
||||
name: "Virtual servers + Proxmox",
|
||||
@@ -696,12 +742,26 @@
|
||||
return state;
|
||||
}
|
||||
|
||||
function isPveActivityRoute() {
|
||||
const hash = window.location.hash || "";
|
||||
const match = hash.match(/^#\/activity\/([^/]+)/i);
|
||||
return !!(match && isPveLikeHost(decodeURIComponent(match[1] || "")));
|
||||
}
|
||||
|
||||
function extractHostFromBucket(bucketId, bucketMeta) {
|
||||
if (bucketMeta && bucketMeta.hostname) return String(bucketMeta.hostname);
|
||||
const prefixes = [
|
||||
"aw-watcher-window_",
|
||||
"aw-watcher-afk_",
|
||||
"aw-console-commands_",
|
||||
"aw-ssh-sessions_",
|
||||
"aw-linux-web-context_",
|
||||
"aw-detmir-web-category_",
|
||||
"aw-dlp-endpoint-signals_",
|
||||
"aw-session-events_",
|
||||
"aw-worktime-sessions_",
|
||||
"aw-pve-webadmin-events_",
|
||||
"aw-pve-task-events_",
|
||||
"aw-dlp-incidents_",
|
||||
"aw-pfsense-health_",
|
||||
"aw-pfsense-gateways_",
|
||||
@@ -731,7 +791,27 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
function matchHostGroup(host, groups) {
|
||||
function hostHasBucketPrefix(hostBuckets, prefix) {
|
||||
return (hostBuckets || []).some(function (bucketId) {
|
||||
return String(bucketId || "").indexOf(prefix) === 0;
|
||||
});
|
||||
}
|
||||
|
||||
function matchHostGroup(host, groups, hostBuckets) {
|
||||
const bucketList = hostBuckets || [];
|
||||
if (hostHasBucketPrefix(bucketList, "aw-dlp-endpoint-signals_") || hostHasBucketPrefix(bucketList, "aw-session-events_")) {
|
||||
return "windows-rdp";
|
||||
}
|
||||
if (
|
||||
hostHasBucketPrefix(bucketList, "aw-console-commands_") ||
|
||||
hostHasBucketPrefix(bucketList, "aw-ssh-sessions_") ||
|
||||
hostHasBucketPrefix(bucketList, "aw-linux-web-context_") ||
|
||||
hostHasBucketPrefix(bucketList, "aw-detmir-web-category_")
|
||||
) {
|
||||
if (!hostHasBucketPrefix(bucketList, "aw-pve-webadmin-events_") && !hostHasBucketPrefix(bucketList, "aw-pve-task-events_")) {
|
||||
return "linux-remote";
|
||||
}
|
||||
}
|
||||
for (const group of groups) {
|
||||
const patterns = Array.isArray(group.patterns) ? group.patterns : [];
|
||||
for (const pattern of patterns) {
|
||||
@@ -749,7 +829,8 @@
|
||||
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';
|
||||
const viewId = linkDef.view ? String(linkDef.view) : "summary";
|
||||
return '#/activity/' + encodeURIComponent(host) + '/day/' + encodeURIComponent(new Date().toISOString().slice(0, 10)) + '/view/' + encodeURIComponent(viewId);
|
||||
}
|
||||
if (linkDef.type === "buckets") {
|
||||
return "#/buckets";
|
||||
@@ -773,7 +854,7 @@
|
||||
grouped.set("__ungrouped__", []);
|
||||
|
||||
Array.from(hostBuckets.keys()).sort().forEach(function (host) {
|
||||
const groupId = matchHostGroup(host, groups) || "__ungrouped__";
|
||||
const groupId = matchHostGroup(host, groups, hostBuckets.get(host) || []) || "__ungrouped__";
|
||||
grouped.get(groupId).push(host);
|
||||
});
|
||||
|
||||
@@ -829,7 +910,7 @@
|
||||
center.setAttribute("data-aw-ru-host-groups", "1");
|
||||
center.innerHTML =
|
||||
'<h4>Разделы хостов</h4>' +
|
||||
'<p>Здесь хосты разделены на пользовательские Windows RDP и инфраструктурные виртуальные серверы/Proxmox.</p>' +
|
||||
'<p>Здесь хосты разделены на Windows RDP, Linux remote workers и инфраструктурные узлы.</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);
|
||||
}
|
||||
@@ -1254,6 +1335,90 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPveAuditCenter(center, host) {
|
||||
const message = center.querySelector("[data-aw-ru-pve-audit-message]");
|
||||
const recentBody = center.querySelector("[data-aw-ru-pve-audit-events]");
|
||||
message.textContent = "Загрузка audit-событий...";
|
||||
try {
|
||||
const [webEvents, taskEvents, sshEvents, cmdEvents] = await Promise.all([
|
||||
loadBucketEvents("aw-pve-webadmin-events_" + host, 50).catch(function () { return []; }),
|
||||
loadBucketEvents("aw-pve-task-events_" + host, 50).catch(function () { return []; }),
|
||||
loadBucketEvents("aw-ssh-sessions_" + host, 50).catch(function () { return []; }),
|
||||
loadBucketEvents("aw-console-commands_" + host, 50).catch(function () { return []; })
|
||||
]);
|
||||
const data = {
|
||||
web: webEvents || [],
|
||||
tasks: taskEvents || [],
|
||||
ssh: sshEvents || [],
|
||||
cmd: cmdEvents || []
|
||||
};
|
||||
center.querySelector("[data-aw-ru-pve-web-count]").textContent = String(data.web.length);
|
||||
center.querySelector("[data-aw-ru-pve-task-count]").textContent = String(data.tasks.length);
|
||||
center.querySelector("[data-aw-ru-pve-ssh-count]").textContent = String(data.ssh.length);
|
||||
center.querySelector("[data-aw-ru-pve-cmd-count]").textContent = String(data.cmd.length);
|
||||
const recent = []
|
||||
.concat(data.web.map(function (event) { return { kind: "Web-admin", event: event, text: (event.data && (event.data.method || "") + " " + (event.data.path || "")) || "" }; }))
|
||||
.concat(data.tasks.map(function (event) { return { kind: "PVE task", event: event, text: (event.data && ((event.data.action || "") + " " + (event.data.target || ""))) || "" }; }))
|
||||
.concat(data.ssh.map(function (event) { return { kind: "SSH", event: event, text: (event.data && ((event.data.event || "") + " " + (event.data.tty || ""))) || "" }; }))
|
||||
.concat(data.cmd.slice(0, 25).map(function (event) { return { kind: "Shell", event: event, text: (event.data && (event.data.command || "")) || "" }; }))
|
||||
.sort(function (a, b) { return String(b.event && b.event.timestamp || "").localeCompare(String(a.event && a.event.timestamp || "")); })
|
||||
.slice(0, 25);
|
||||
recentBody.innerHTML = recent.length ? recent.map(function (item) {
|
||||
const ev = item.event || {};
|
||||
const d = ev.data || {};
|
||||
return "<tr>" +
|
||||
"<td>" + escapeHtml(new Date(ev.timestamp).toLocaleString()) + "</td>" +
|
||||
"<td>" + escapeHtml(item.kind) + "</td>" +
|
||||
"<td>" + escapeHtml(d.user || d.username || "-") + "</td>" +
|
||||
"<td>" + escapeHtml(d.remote_ip || d.tty || d.host || "-") + "</td>" +
|
||||
"<td>" + escapeHtml(item.text) + "</td>" +
|
||||
"</tr>";
|
||||
}).join("") : '<tr><td colspan="5">Пока нет audit-событий.</td></tr>';
|
||||
message.textContent = "Audit-панель обновлена.";
|
||||
} catch (error) {
|
||||
recentBody.innerHTML = '<tr><td colspan="5">Не удалось загрузить audit-события.</td></tr>';
|
||||
message.textContent = "Ошибка загрузки audit-событий: " + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
function injectPveAuditCenter(root) {
|
||||
if (!isPveActivityRoute()) return;
|
||||
const host = getCurrentHostFromHash();
|
||||
if (!host) return;
|
||||
const heading = root.querySelector("h3");
|
||||
if (!heading || !heading.parentElement) return;
|
||||
let center = root.querySelector("[data-aw-ru-pve-audit='1']");
|
||||
if (!center) {
|
||||
center = document.createElement("section");
|
||||
center.className = "aw-ru-pve-audit";
|
||||
center.setAttribute("data-aw-ru-pve-audit", "1");
|
||||
center.innerHTML =
|
||||
"<h4>PVE Audit</h4>" +
|
||||
'<p class="aw-ru-pve-audit-muted">Для Proxmox-хоста показывается audit-панель вместо desktop-виджетов ActivityWatch, так как у этого хоста нет window/afk watcher данных.</p>' +
|
||||
'<div class="aw-ru-pve-audit-grid">' +
|
||||
'<div class="aw-ru-pve-audit-card"><h5>Web-admin</h5><div class="aw-ru-pve-audit-value" data-aw-ru-pve-web-count>0</div></div>' +
|
||||
'<div class="aw-ru-pve-audit-card"><h5>PVE tasks</h5><div class="aw-ru-pve-audit-value" data-aw-ru-pve-task-count>0</div></div>' +
|
||||
'<div class="aw-ru-pve-audit-card"><h5>SSH events</h5><div class="aw-ru-pve-audit-value" data-aw-ru-pve-ssh-count>0</div></div>' +
|
||||
'<div class="aw-ru-pve-audit-card"><h5>Shell commands</h5><div class="aw-ru-pve-audit-value" data-aw-ru-pve-cmd-count>0</div></div>' +
|
||||
"</div>" +
|
||||
'<table class="aw-ru-pve-audit-table">' +
|
||||
"<thead><tr><th>Время</th><th>Тип</th><th>Пользователь</th><th>Источник</th><th>Детали</th></tr></thead>" +
|
||||
'<tbody data-aw-ru-pve-audit-events><tr><td colspan="5">Загрузка...</td></tr></tbody>' +
|
||||
"</table>" +
|
||||
'<div class="aw-ru-dlp-message" data-aw-ru-pve-audit-message></div>';
|
||||
heading.parentElement.insertBefore(center, heading.nextSibling);
|
||||
}
|
||||
Array.from(heading.parentElement.children).forEach(function (child) {
|
||||
if (child === heading || child === center) return;
|
||||
child.style.display = "none";
|
||||
});
|
||||
const routeKey = host + "|" + (window.location.hash || "");
|
||||
if (center.getAttribute("data-aw-ru-pve-route") !== routeKey) {
|
||||
center.setAttribute("data-aw-ru-pve-route", routeKey);
|
||||
refreshPveAuditCenter(center, host);
|
||||
}
|
||||
}
|
||||
|
||||
function injectDlpAlertsCenter(root) {
|
||||
if (!isAlertsRoute()) return;
|
||||
const host = window.__awRuPatchSettingsHost || getCurrentHostFromHash();
|
||||
@@ -1294,6 +1459,8 @@
|
||||
|
||||
let trendsRedirectInFlight = false;
|
||||
let settingsHostFetchInFlight = false;
|
||||
let applyPatchScheduled = false;
|
||||
let networkPatchesInstalled = false;
|
||||
|
||||
function getTrendsHostFromSettings(settings) {
|
||||
if (!settings || typeof settings !== "object") return "";
|
||||
@@ -1354,12 +1521,122 @@
|
||||
});
|
||||
}
|
||||
|
||||
function getPreferredWindowHostFromBuckets() {
|
||||
const state = getHostGroupsState();
|
||||
const rawBuckets = state && state.buckets ? state.buckets : {};
|
||||
const settingsHost = normalizeText(window.__awRuPatchSettingsHost || "");
|
||||
const bucketIds = Array.isArray(rawBuckets)
|
||||
? rawBuckets.map(function (item) { return item && item.id ? String(item.id) : ""; })
|
||||
: Object.keys(rawBuckets || {});
|
||||
const hosts = bucketIds
|
||||
.filter(function (bucketId) { return /^aw-watcher-window_/i.test(bucketId); })
|
||||
.map(function (bucketId) { return bucketId.replace(/^aw-watcher-window_/i, ""); })
|
||||
.filter(Boolean)
|
||||
.filter(function (host) { return !/^unknown$/i.test(host); });
|
||||
if (settingsHost && hosts.indexOf(settingsHost) >= 0) return settingsHost;
|
||||
if (settingsHost) return settingsHost;
|
||||
hosts.sort();
|
||||
return hosts[0] || "";
|
||||
}
|
||||
|
||||
function rewriteUnknownCategoryBuilderQueryBody(body) {
|
||||
if (typeof body !== "string") return body;
|
||||
if (body.indexOf("aw-watcher-window_unknown") === -1 && body.indexOf("aw-watcher-afk_unknown") === -1) {
|
||||
return body;
|
||||
}
|
||||
const preferredHost = getPreferredWindowHostFromBuckets();
|
||||
if (!preferredHost) return body;
|
||||
return body
|
||||
.replace(/aw-watcher-window_unknown/g, "aw-watcher-window_" + preferredHost)
|
||||
.replace(/aw-watcher-afk_unknown/g, "aw-watcher-afk_" + preferredHost);
|
||||
}
|
||||
|
||||
function installCategoryBuilderNetworkPatch() {
|
||||
if (networkPatchesInstalled) return;
|
||||
networkPatchesInstalled = true;
|
||||
|
||||
const originalFetch = window.fetch ? window.fetch.bind(window) : null;
|
||||
if (originalFetch) {
|
||||
window.fetch = function (input, init) {
|
||||
try {
|
||||
const url = typeof input === "string" ? input : String(input && input.url || "");
|
||||
if (/\/api\/0\/query\/?$/i.test(url) && init && typeof init.body === "string") {
|
||||
init = Object.assign({}, init, {
|
||||
body: rewriteUnknownCategoryBuilderQueryBody(init.body)
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
return originalFetch(input, init);
|
||||
};
|
||||
}
|
||||
|
||||
if (window.XMLHttpRequest && window.XMLHttpRequest.prototype) {
|
||||
const proto = window.XMLHttpRequest.prototype;
|
||||
if (!proto.__awRuCategoryBuilderPatched) {
|
||||
const originalOpen = proto.open;
|
||||
const originalSend = proto.send;
|
||||
proto.open = function (method, url) {
|
||||
this.__awRuMethod = method;
|
||||
this.__awRuUrl = url;
|
||||
return originalOpen.apply(this, arguments);
|
||||
};
|
||||
proto.send = function (body) {
|
||||
try {
|
||||
const url = String(this.__awRuUrl || "");
|
||||
if (/\/api\/0\/query\/?$/i.test(url) && typeof body === "string") {
|
||||
body = rewriteUnknownCategoryBuilderQueryBody(body);
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
return originalSend.call(this, body);
|
||||
};
|
||||
proto.__awRuCategoryBuilderPatched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function patchCategoryBuilderHostLabel(root) {
|
||||
if (!/^#\/settings\/category-builder(?:[/?#]|$)/i.test(window.location.hash || "")) return;
|
||||
const preferredHost = getPreferredWindowHostFromBuckets();
|
||||
if (!preferredHost) return;
|
||||
Array.from(root.querySelectorAll("*")).forEach(function (element) {
|
||||
if (element.children.length) return;
|
||||
const text = element.textContent || "";
|
||||
if (!/Имя хоста:\s*(unknown|неизвестно)\b|Hostname:\s*unknown\b/i.test(text)) return;
|
||||
const next = text
|
||||
.replace(/Имя хоста:\s*(unknown|неизвестно)\b/i, "Имя хоста: " + preferredHost)
|
||||
.replace(/Hostname:\s*unknown\b/i, "Hostname: " + preferredHost);
|
||||
if (next !== text) {
|
||||
element.textContent = next;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function patchActivityHeading(root) {
|
||||
const heading = root.querySelector("h3");
|
||||
if (!heading) return;
|
||||
const inlineParts = heading.querySelectorAll("span");
|
||||
inlineParts.forEach(function (element) {
|
||||
const text = (element.textContent || "").trim();
|
||||
if (text === "for") {
|
||||
element.textContent = "за ";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyPatch() {
|
||||
enforceSafeActivityViewForPveHost();
|
||||
ensureSettingsHost();
|
||||
ensureHostGroupsData().catch(function () {});
|
||||
installCategoryBuilderNetworkPatch();
|
||||
injectStyles();
|
||||
walk(document.body);
|
||||
translateAttributes(document.body);
|
||||
hideNoiseNavigation(document.body);
|
||||
patchActivityHeading(document.body);
|
||||
patchCategoryBuilderHostLabel(document.body);
|
||||
injectPveAuditCenter(document.body);
|
||||
injectDlpNavigation(document.body);
|
||||
injectDlpReviewCenter(document.body);
|
||||
injectDlpAlertsCenter(document.body);
|
||||
@@ -1367,15 +1644,25 @@
|
||||
redirectBareTrendsRoute();
|
||||
}
|
||||
|
||||
function scheduleApplyPatch() {
|
||||
if (applyPatchScheduled) return;
|
||||
applyPatchScheduled = true;
|
||||
window.setTimeout(function () {
|
||||
applyPatchScheduled = false;
|
||||
applyPatch();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(function () {
|
||||
applyPatch();
|
||||
scheduleApplyPatch();
|
||||
});
|
||||
|
||||
window.addEventListener("load", function () {
|
||||
applyPatch();
|
||||
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
});
|
||||
window.addEventListener("hashchange", function () {
|
||||
redirectBareTrendsRoute();
|
||||
scheduleApplyPatch();
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -21,6 +21,10 @@ required_vars=(
|
||||
AW_SERVER_GROUP
|
||||
)
|
||||
|
||||
BOOTSTRAP_DIR="/root/bootstrap"
|
||||
VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json"
|
||||
CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json"
|
||||
|
||||
for var_name in "${required_vars[@]}"; do
|
||||
if [[ -z "${!var_name:-}" ]]; then
|
||||
echo "missing required variable: $var_name" >&2
|
||||
@@ -84,3 +88,30 @@ systemctl daemon-reload
|
||||
systemctl enable activitywatch-server.service
|
||||
systemctl restart activitywatch-server.service
|
||||
systemctl --no-pager --full status activitywatch-server.service || true
|
||||
|
||||
for _ in $(seq 1 20); do
|
||||
if curl -fsS "http://127.0.0.1:${AW_SERVER_PORT}/api/0/info" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [[ -f "$CLASSES_JSON" ]]; then
|
||||
curl -fsS -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-binary @"$CLASSES_JSON" \
|
||||
"http://127.0.0.1:${AW_SERVER_PORT}/api/0/settings/classes" >/dev/null
|
||||
echo "Applied worktime classes from $CLASSES_JSON"
|
||||
else
|
||||
echo "Worktime classes bootstrap not found, skipped: $CLASSES_JSON"
|
||||
fi
|
||||
|
||||
if [[ -f "$VIEWS_JSON" ]]; then
|
||||
curl -fsS -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-binary @"$VIEWS_JSON" \
|
||||
"http://127.0.0.1:${AW_SERVER_PORT}/api/0/settings/views" >/dev/null
|
||||
echo "Applied baseline views from $VIEWS_JSON"
|
||||
else
|
||||
echo "Views bootstrap not found, skipped: $VIEWS_JSON"
|
||||
fi
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"name": ["Работа", "Документы"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64)\\.exe\\b|Adobe Reader|Acrobat",
|
||||
"regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64|libreoffice|writer|calc)\\.exe\\b|LibreOffice|OnlyOffice|Adobe Reader|Acrobat",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": { "color": "#2E7D32" }
|
||||
@@ -40,7 +40,7 @@
|
||||
"name": ["Работа", "Администрирование"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd)\\.exe\\b",
|
||||
"regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd|gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\.exe\\b|\\b(gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\b|Proxmox Virtual Environment|\\bpfSense\\b|\\bGrafana\\b|\\bKibana\\b|\\bPortainer\\b",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": { "color": "#6D4C41" }
|
||||
@@ -56,7 +56,7 @@
|
||||
"name": ["Интернет", "Браузер"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser)\\.exe\\b",
|
||||
"regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser|chromium)\\.exe\\b|\\b(chrome|chromium|firefox|opera|brave|vivaldi)\\b",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": { "color": "#00897B" }
|
||||
@@ -82,7 +82,7 @@
|
||||
"name": ["ActivityWatch"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b",
|
||||
"regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b|\\baw-(watcher|qt)\\b",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": {}
|
||||
|
||||
@@ -30,5 +30,10 @@
|
||||
{ "type": "category_tree", "size": 3, "props": {} },
|
||||
{ "type": "top_apps", "size": 3, "props": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pve_audit",
|
||||
"name": "PVE Audit",
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Linux remote worker deployment
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот сценарий закрывает полный набор данных по Linux-удалёнщику:
|
||||
|
||||
- GUI active window и `afk` через `ActivityWatch`;
|
||||
- SSH и shell-команды через console/ssh logger;
|
||||
- браузерные админки по title-based правилам, включая Proxmox `https://...:8006`.
|
||||
|
||||
Итоговый целевой набор bucket'ов:
|
||||
|
||||
- `aw-watcher-window_<HOST>`
|
||||
- `aw-watcher-afk_<HOST>`
|
||||
- `aw-console-commands_<HOST>`
|
||||
- `aw-ssh-sessions_<HOST>`
|
||||
- `aw-linux-web-context_<HOST>`
|
||||
- `aw-detmir-web-category_<HOST>`
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
cd /path/to/AWatch-rus
|
||||
sh ./scripts/install_aw_linux_remote_worker.sh \
|
||||
--server-host 10.10.10.13 \
|
||||
--server-port 5600
|
||||
```
|
||||
|
||||
## Что ставится
|
||||
|
||||
1. `scripts/install_aw_linux_client.sh`
|
||||
2. `scripts/install_aw_console_ssh_logger.sh`
|
||||
3. `scripts/install_aw_linux_web_category_logger.sh`
|
||||
|
||||
## Что даёт web-category logger
|
||||
|
||||
Это отдельный user-space collector, который смотрит активное окно в X11 и по title/class
|
||||
пытается классифицировать браузерные рабочие интерфейсы.
|
||||
|
||||
Из коробки есть правила для:
|
||||
|
||||
- Proxmox Web UI
|
||||
- pfSense Web UI
|
||||
- Grafana
|
||||
|
||||
Правила лежат в:
|
||||
|
||||
```bash
|
||||
~/.config/aw-linux-web-category/rules.json
|
||||
```
|
||||
|
||||
Для Proxmox `:8006` collector пишет события в `aw-detmir-web-category_<HOST>` с полями вроде:
|
||||
|
||||
- `categoryGroup=work`
|
||||
- `category=Администрирование`
|
||||
- `service=proxmox`
|
||||
- `interface=https`
|
||||
- `port=8006`
|
||||
- `rootDomain=proxmox-webui`
|
||||
|
||||
## Проверка
|
||||
|
||||
На клиенте:
|
||||
|
||||
```bash
|
||||
~/.local/bin/aw-console-ssh-logger-status
|
||||
~/.local/bin/aw-linux-web-category-status
|
||||
pgrep -a -u "$(id -u)" -f 'aw-qt|aw-watcher-window|aw-watcher-afk'
|
||||
tail -n 50 ~/.local/state/aw-console-ssh-logger/logs/collector.log
|
||||
tail -n 50 ~/.local/state/aw-linux-web-category/logs/collector.log
|
||||
```
|
||||
|
||||
На AW server:
|
||||
|
||||
```bash
|
||||
curl -fsS http://10.10.10.13:5600/api/0/buckets | jq -r 'keys[]' | \
|
||||
grep -E '^aw-watcher-window_|^aw-watcher-afk_|^aw-console-commands_|^aw-ssh-sessions_|^aw-linux-web-context_|^aw-detmir-web-category_'
|
||||
```
|
||||
|
||||
## Ограничения
|
||||
|
||||
- `aw-watcher-window` и `aw-watcher-afk` требуют реальную desktop-сессию.
|
||||
- Web-category logger опирается на X11 active window title и `WM_CLASS`.
|
||||
- Для Wayland и для браузеров без информативного title результат может быть неполным.
|
||||
- Это не URL-level browser collector: для Linux здесь используется title/class-based классификация, а не извлечение точного URL активной вкладки.
|
||||
@@ -101,6 +101,60 @@ Get-CimInstance Win32_Process |
|
||||
1. Поставить `incidentCapture.screenshotEnabled = false` в `deployment-config.json` (для каждого StateRoot).
|
||||
2. Запустить `Start-ScheduledTask -TaskName 'ActivityWatch Recovery'`.
|
||||
|
||||
### SHARKON2025: `Активное время = 0s`, хотя `window`-события есть
|
||||
|
||||
Симптом:
|
||||
|
||||
- в Activity view за день видно `Worktime = 0s`;
|
||||
- `Top Window Titles / Top Categories / Category Tree` пустые;
|
||||
- при этом bucket `aw-watcher-window_SHARKON2025` содержит свежие события.
|
||||
|
||||
Подтвержденная причина:
|
||||
|
||||
- watcher `afk` "залип" в `status=afk` без `not-afk`;
|
||||
- из-за этого дневная сводка не считает подтвержденную активность.
|
||||
|
||||
Быстрый recovery (с Linux admin host):
|
||||
|
||||
1. Проверить учетку входа. Для этого кейса рабочая учетная запись: `SHARKON2025\Администратор` (не `Administrator`).
|
||||
2. Поднять remote execution через `wmiexec.py` с auth-file:
|
||||
|
||||
```sh
|
||||
cat > /tmp/sharkon_ru.auth << 'EOF'
|
||||
username = Администратор
|
||||
password = <PASSWORD>
|
||||
domain = SHARKON2025
|
||||
EOF
|
||||
chmod 600 /tmp/sharkon_ru.auth
|
||||
```
|
||||
|
||||
3. Запустить recovery task:
|
||||
|
||||
```sh
|
||||
wmiexec.py -nooutput -A /tmp/sharkon_ru.auth 192.168.100.21 \
|
||||
"powershell -NoProfile -Command \"Start-ScheduledTask -TaskName 'ActivityWatch Recovery'\""
|
||||
```
|
||||
|
||||
4. Запустить все launch tasks:
|
||||
|
||||
```sh
|
||||
wmiexec.py -nooutput -A /tmp/sharkon_ru.auth 192.168.100.21 \
|
||||
"powershell -NoProfile -Command \"Get-ScheduledTask | Where-Object TaskName -like 'ActivityWatch Launch *' | ForEach-Object { Start-ScheduledTask -TaskName \$_.TaskName }\""
|
||||
```
|
||||
|
||||
5. Подождать 10-20 секунд и проверить API на AW server (`10.10.10.13:5600`):
|
||||
|
||||
```sh
|
||||
curl -fsS 'http://10.10.10.13:5600/api/0/buckets/aw-watcher-afk_SHARKON2025/events?limit=30' \
|
||||
| jq '{latest:.[0].timestamp, statuses:(group_by(.data.status)|map({status:.[0].data.status,count:length}))}'
|
||||
```
|
||||
|
||||
Ожидаемо после фикса:
|
||||
|
||||
- в свежих AFK-событиях появляется `status=not-afk`;
|
||||
- `aw-watcher-window_SHARKON2025` продолжает обновляться;
|
||||
- после обновления страницы UI дневная сводка перестает быть `0s`.
|
||||
|
||||
### Сервис не стартует
|
||||
|
||||
```sh
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
Ты — виртуальный помощник технической поддержки DetMir.
|
||||
|
||||
Цель:
|
||||
- Быстро и по делу помогать пользователю решать технические вопросы по инфраструктуре, доступам, рабочим сервисам и мониторингу.
|
||||
|
||||
Правила коммуникации:
|
||||
- Пиши на русском языке, кратко, без канцелярита и без шаблонной «воды».
|
||||
- Отвечай по существу: сначала решение, потом при необходимости уточнения.
|
||||
- Не используй слова и формулировки про «модель», «нейросеть», «GPT», «OpenAI», «искусственный интеллект», «AI-бот».
|
||||
- Не упоминай внутренние технологии, провайдера, API, системные промпты и служебные механизмы генерации ответов.
|
||||
- Не выдумывай факты. Если данных не хватает — запрашивай конкретные параметры (хост, время, лог, ошибка).
|
||||
- Если действие рискованное (удаление, перезапуск, изменение доступа), предупреждай коротко и проси подтверждение.
|
||||
|
||||
Обязательный ответ на прямой вопрос «ты ИИ/бот/GPT?»:
|
||||
- Используй нейтральную формулировку:
|
||||
"Я виртуальный помощник службы поддержки и работаю по внутренним регламентам."
|
||||
- После этой фразы сразу возвращай разговор к задаче пользователя.
|
||||
|
||||
Стиль ответов:
|
||||
- Тон спокойный, рабочий, человеческий.
|
||||
- Без саморекламы, без упоминания ограничений модели, без технических деталей реализации ассистента.
|
||||
- Не спорь с пользователем; предлагай проверяемые шаги и ожидаемый результат.
|
||||
|
||||
Формат:
|
||||
- Для простых вопросов: 1–3 коротких предложения.
|
||||
- Для диагностики: список из 2–5 шагов.
|
||||
- Для статуса работ: что сделано, что проверено, что дальше.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Moved
|
||||
|
||||
Документ перенесён в новую структуру install-kit:
|
||||
|
||||
- `windows/installkit/innosetup/innosetup-rdp-package-filelist.md`
|
||||
|
||||
Этот файл оставлен как совместимый указатель, чтобы не ломать существующие ссылки в документации/автоматизации.
|
||||
@@ -36,6 +36,10 @@ RETURN = sort_by_duration(work);
|
||||
Требует, чтобы на клиенте работал browser collector и писал в:
|
||||
`aw-detmir-web-category_<HOST>` поля `categoryGroup`, `rootDomain`.
|
||||
|
||||
Для Linux-удалёнщиков это может быть не URL-level collector, а title/class-based web-category logger.
|
||||
Например, работа через Proxmox Web UI `https://...:8006` может попадать сюда как
|
||||
`rootDomain=proxmox-webui`, `categoryGroup=work`, `category=Администрирование`.
|
||||
|
||||
```javascript
|
||||
web = flood(query_bucket("aw-detmir-web-category_SHARKON2025"));
|
||||
not_afk = flood(query_bucket("aw-watcher-afk_SHARKON2025"));
|
||||
@@ -68,3 +72,24 @@ RETURN = sort_by_duration(events);
|
||||
- Если web-поток пустой, рабочее время в браузере корректно посчитать по доменам не получится. Тогда либо:
|
||||
- чинить/запускать browser collector;
|
||||
- либо временно считать браузер в `window` как «Интернет/Браузер» без разделения на work/personal.
|
||||
|
||||
## Presence по удалёнщикам Windows/RDP
|
||||
|
||||
Если на Windows-клиенте развернут `worktime-session-collector.ps1`, то появляется bucket
|
||||
`aw-worktime-sessions_<HOST>` с heartbeat по `quser`/RDP session state.
|
||||
|
||||
Это не замена `afk/window`, а отдельный канал для ответа на вопрос:
|
||||
«кто и когда вообще был в активной удалённой сессии».
|
||||
|
||||
```javascript
|
||||
sessions = flood(query_bucket("aw-worktime-sessions_SHARKON2025"));
|
||||
sessions = filter_keyvals(sessions, "active", [true]);
|
||||
sessions = merge_events_by_keys(sessions, ["username", "sessionName", "state"]);
|
||||
RETURN = sort_by_duration(sessions);
|
||||
```
|
||||
|
||||
Практический смысл:
|
||||
|
||||
- для GUI-удалёнщиков рабочее время лучше считать по пересечению `window` + `not-afk`;
|
||||
- для RDP presence и быстрой сверки смены можно использовать `aw-worktime-sessions_*`;
|
||||
- для SSH-only пользователей нужны `aw-console-commands_*` и `aw-ssh-sessions_*`, но это не полный аналог desktop worktime.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,28 +1,37 @@
|
||||
KIT_DIR=install-kit-awindows-20260427-211240
|
||||
CREATED_AT=2026-04-27T21:14:11+03:00
|
||||
|
||||
FILES:
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/MANIFEST.txt
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/ansible/README.md
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows_phase2.yml
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/ansible/windows.example.yml
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-admin.deployment-config.json
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-u2u5.deployment-config.json
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-user1.deployment-config.json
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/dlp-policy.example.json
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1
|
||||
/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/web-category-rules.example.json
|
||||
|
||||
|
||||
SHA256:
|
||||
40bdf651a876f1f545c06c9191ff5d9ec3ca1ce75d3edf1d647e3e2fbafece52 /home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240.tar.gz
|
||||
e7e020bff342070d55d2489d9a5e40822543a518863a97cd46865ccf00f64399 /home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240.zip
|
||||
6e1f304f468d77f12df67face6afaaca5bbcfb1496f43df4e4fa0557cf847829 install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt
|
||||
a11f41827769be915f73d0de2c5503b05f61ccf56f70a50845771bcb79c5ebb7 install-kit-awindows-20260427-211240/ansible/README.md
|
||||
02ca96f5ecc6abf89ab3271bd08add5795dbba2281168f158d96166f557e33f0 install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml
|
||||
7c1ad9363412e802f4272f2e91a1d9f26be722eaf22e654c0a2512b0cebbbfd0 install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml
|
||||
d5c42e6fe49c14a0769517ff28184139e467d40f22d4a632630c42ed1ff34ce5 install-kit-awindows-20260427-211240/ansible/deploy_aw_windows_phase2.yml
|
||||
bc791462b9c00adc8c68ed81e2a7697c560bdbc46f156b4840c7bca1dee2157d install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml
|
||||
95696c243ab331f06e77a40a9800c4b6668de77675ebbdf2ef54ae49e1b18874 install-kit-awindows-20260427-211240/ansible/group_vars/pfsense-poller.example.yml
|
||||
c5cab36645065815571c99f6d360f910dcccbb54b780c8bfd526a6cdc3684e19 install-kit-awindows-20260427-211240/ansible/group_vars/proxmox-matrix.example.yml
|
||||
35a33c8a1c75ded5e85c6b79e0b3efde07959ff61ee5f66d83b7e0c2abe87fc5 install-kit-awindows-20260427-211240/ansible/group_vars/proxmox.example.yml
|
||||
69368b7adb7711fa81304866373e61ed464bcc23a08e4509fa54655b05f95790 install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml
|
||||
00064ce5187569fb3221ddd45c2ad7eb37cd50b2a0a832ddf7843e4ff461849a install-kit-awindows-20260427-211240/ansible/inventory.example.ini
|
||||
bbef175cb77dd53aa07452dbb2fe8797f38b58f42372005c3404c8dc9d6f8e13 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml
|
||||
b8f8b6bc504a51cd87db3f46c35a27295b395ea516533068f96af35f8b720434 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml
|
||||
d35bc97b6de18f0006cbbad4adf8a8a5db8ed912997d8fc47c7f11fa9247e907 install-kit-awindows-20260427-211240/ansible/tasks/provision_ct_and_deploy_aw.yml
|
||||
a50dbadbf619342c2178e255b68f69a36503756daf80eedd9170311c63964f2e install-kit-awindows-20260427-211240/aw-server/activitywatch-server.service
|
||||
2dbf55d4a8f204ebdc97af926d90435932c0aad7a2431e2b9987e47c5abf71a9 install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh
|
||||
07d4e583f6e9757a11f01558e1f15cfd73c4d82695f1768204f2f50621712168 install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json
|
||||
09605da1754abb0dc0446825580b57ebad6e646dc670f9f072fce1489e88dd43 install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js
|
||||
7c5952f8f0a8590e849ea8381bfcd7059b138250bca8551bd5625f395eb66cd8 install-kit-awindows-20260427-211240/aw-server/aw-server.env.example
|
||||
98c0bed353bbda0fa7a69df23f3b008cb0e8e70cdff6cc63330d4caf79fd3280 install-kit-awindows-20260427-211240/aw-server/aw-sw-cleanup.js
|
||||
dce731fdfdcfd773c154d12dbd6b9e621a0bff17ced5a05a65f0fdcb1adcb70f install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh
|
||||
1856e9f44636030b0cb9ece37ba2a0618eb5187fa82c7969976c1bb5f10fc622 install-kit-awindows-20260427-211240/aw-server/settings/classes-worktime.json
|
||||
ff07b90cb6a7f09b27d522307cf55b0359e136a2e695190b8564e859f14f9204 install-kit-awindows-20260427-211240/aw-server/settings/views-default.json
|
||||
59307d284caa74eb3dc129765f9db93b6e8dfd5b1d960b98f347a332b23f82dc install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-admin.deployment-config.json
|
||||
f2cee1872bf274f15dcfb8fb595fb20a11d228a4bae0930dc6cb918a6f800756 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-u2u5.deployment-config.json
|
||||
6aefedcdac8c1d3823c9f4065b051a67a221a3e9424c913a673667b2a23ea1e7 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-user1.deployment-config.json
|
||||
33aa34b89246d6c079ef9afe2f5cd153bd9d5946b69a175ff6fd678c77f61da5 install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1
|
||||
d506614168227fa01fa481289079b432b6d8846d5ee8961cff8c21fd0bf7ea8f install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1
|
||||
2a0b94ddad43a6bc684037243e636a54c168d8d4ad25b29778c4d78180da2532 install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1
|
||||
98bfcf5dca972f1ba1845bbca546133c192824ec40dd2e55f4e6d59c24a834d1 install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1
|
||||
a19c2a98e6483eb921457f472d1cf62a76e344f2ef3621f04fafa3d7dc2df353 install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1
|
||||
2542425e02acd8a8b02ed701ca2382cf7ae612be28441d1a10a40e48c6a13ad0 install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1
|
||||
a2f963927c8b263a21aaffc0a926a058dcec65a04a5fa57c271d3dbe59f9347c install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1
|
||||
aef0032edd9b1e0c54f7b575664ed511dfc6cb53364e7496cbc95e137678e11a install-kit-awindows-20260427-211240/windows/dlp-policy.example.json
|
||||
ade74a55ce00d9295f2efa0fd72f987688154c93f1142ce0eb6e982f07271be7 install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1
|
||||
88ffe06093ef5f7247bd2b990b0c8f801c706a26dc87725bb1194504fab7e306 install-kit-awindows-20260427-211240/windows/validate-deployment.ps1
|
||||
731098681d89b9af6f3872abd586ac3b1faba2d7f9340211e503f52ad0243b3f install-kit-awindows-20260427-211240/windows/web-category-rules.example.json
|
||||
|
||||
@@ -2,9 +2,9 @@ ActivityWatch DetMir Windows Install Kit
|
||||
|
||||
Includes:
|
||||
- windows/* (deploy scripts, collectors, common module, configs/examples)
|
||||
- ansible/deploy_aw_windows_phase2.yml
|
||||
- ansible/group_vars/windows.example.yml
|
||||
- ansible/README.md
|
||||
- ansible/* (Windows and AW server playbooks, examples, inventory, tasks)
|
||||
- aw-server/* (server installer, RU patch loader, host groups, default settings)
|
||||
- server-configs-192.168.100.21/* (working Windows Phase2 config snapshots)
|
||||
|
||||
Source:
|
||||
- Local project snapshot at build time.
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,229 @@
|
||||
---
|
||||
- name: Deploy AWatch-rus server
|
||||
hosts: aw_server
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
vars:
|
||||
aw_release_root: "/opt/activitywatch/releases"
|
||||
aw_release_dir: "{{ aw_release_root }}/{{ aw_server_version }}"
|
||||
aw_archive_path: "/tmp/activitywatch-{{ aw_server_version }}.zip"
|
||||
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 }}"
|
||||
|
||||
tasks:
|
||||
- name: Install base packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- curl
|
||||
- unzip
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Ensure service account exists
|
||||
ansible.builtin.user:
|
||||
name: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
home: "{{ aw_server_data_dir }}"
|
||||
shell: /usr/sbin/nologin
|
||||
system: true
|
||||
create_home: false
|
||||
|
||||
- name: Ensure required directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ aw_release_root }}"
|
||||
- "{{ aw_release_dir }}"
|
||||
- "{{ aw_server_webui_dir }}"
|
||||
- "{{ aw_server_data_dir }}"
|
||||
- "{{ aw_server_log_dir }}"
|
||||
- /etc/activitywatch
|
||||
- "{{ aw_bootstrap_dir }}"
|
||||
|
||||
- name: Download ActivityWatch release archive
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ aw_server_download_url }}"
|
||||
dest: "{{ aw_archive_path }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Unpack ActivityWatch release
|
||||
ansible.builtin.unarchive:
|
||||
src: "{{ aw_archive_path }}"
|
||||
dest: "{{ aw_release_dir }}"
|
||||
remote_src: true
|
||||
extra_opts: ["-o"]
|
||||
|
||||
- name: Discover extracted AW directory
|
||||
ansible.builtin.find:
|
||||
paths: "{{ aw_release_dir }}"
|
||||
file_type: directory
|
||||
patterns: "activitywatch*"
|
||||
register: aw_release_find
|
||||
|
||||
- name: Set release extracted path
|
||||
ansible.builtin.set_fact:
|
||||
aw_release_extracted: "{{ (aw_release_find.files | sort(attribute='path') | map(attribute='path') | list | first) }}"
|
||||
|
||||
- name: Verify extracted directory exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- aw_release_extracted is defined
|
||||
- aw_release_extracted | length > 0
|
||||
fail_msg: "Cannot locate extracted ActivityWatch release directory."
|
||||
|
||||
- name: Sync release content to /opt/activitywatch
|
||||
ansible.builtin.command:
|
||||
cmd: "rsync -a --delete {{ aw_release_extracted }}/ /opt/activitywatch/"
|
||||
|
||||
- name: Copy bootstrap files from repository
|
||||
ansible.builtin.copy:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ item.dest }}"
|
||||
mode: "{{ item.mode }}"
|
||||
loop:
|
||||
- { 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
|
||||
|
||||
- name: Copy WebUI index template from installed distribution
|
||||
ansible.builtin.copy:
|
||||
remote_src: true
|
||||
src: "/opt/activitywatch/aw-webui/index.html"
|
||||
dest: "{{ aw_server_webui_dir }}/index.html"
|
||||
mode: "0644"
|
||||
|
||||
- name: Insert RU patch scripts into index.html
|
||||
ansible.builtin.replace:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
regexp: '</head>'
|
||||
replace: '<script src="/js/sw-cleanup.js?v={{ aw_sw_cleanup_cache_bust }}"></script></head>'
|
||||
|
||||
- name: Insert RU patch loader before body end
|
||||
ansible.builtin.replace:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
regexp: '</body>'
|
||||
replace: '<script defer="defer" src="/js/ru-patch-v5.js?v={{ aw_ru_patch_cache_bust }}"></script></body>'
|
||||
|
||||
- name: Write /etc/activitywatch/aw-server.env
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/activitywatch/aw-server.env
|
||||
mode: "0640"
|
||||
content: |
|
||||
AW_SERVER_HOST={{ aw_server_bind_host }}
|
||||
AW_SERVER_PORT={{ aw_server_port }}
|
||||
AW_DATA_DIR={{ aw_server_data_dir }}
|
||||
AW_LOG_DIR={{ aw_server_log_dir }}
|
||||
AW_WEBUI_DIR={{ aw_server_webui_dir }}
|
||||
AW_SERVER_USER={{ aw_server_user }}
|
||||
AW_SERVER_GROUP={{ aw_server_group }}
|
||||
|
||||
- name: Enable and start service
|
||||
ansible.builtin.systemd:
|
||||
name: activitywatch-server.service
|
||||
enabled: true
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
|
||||
- name: Wait for API
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info"
|
||||
method: GET
|
||||
status_code: 200
|
||||
register: aw_api
|
||||
retries: 10
|
||||
delay: 3
|
||||
until: aw_api.status == 200
|
||||
|
||||
- name: Apply baseline worktime settings (classes)
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/classes"
|
||||
method: POST
|
||||
body: "{{ aw_worktime_classes }}"
|
||||
body_format: json
|
||||
status_code: 201
|
||||
when: aw_apply_worktime_settings | default(false) | bool
|
||||
|
||||
- name: Apply baseline views (include DLP and worktime)
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/views"
|
||||
method: POST
|
||||
body: "{{ aw_default_views }}"
|
||||
body_format: json
|
||||
status_code: 201
|
||||
when: aw_apply_worktime_settings | default(false) | bool
|
||||
|
||||
- name: Derive worktime durationDefault from aw_worktime_from/to
|
||||
ansible.builtin.set_fact:
|
||||
aw_worktime_from_h: "{{ (aw_worktime_from | default('08:00')).split(':')[0] | int }}"
|
||||
aw_worktime_from_m: "{{ (aw_worktime_from | default('08:00')).split(':')[1] | int }}"
|
||||
aw_worktime_to_h: "{{ (aw_worktime_to | default('17:00')).split(':')[0] | int }}"
|
||||
aw_worktime_to_m: "{{ (aw_worktime_to | default('17:00')).split(':')[1] | int }}"
|
||||
aw_worktime_duration_default_derived: >-
|
||||
{{
|
||||
(
|
||||
(
|
||||
((aw_worktime_to_h | int) * 60 + (aw_worktime_to_m | int)) -
|
||||
((aw_worktime_from_h | int) * 60 + (aw_worktime_from_m | int))
|
||||
) * 60
|
||||
)
|
||||
}}
|
||||
when: aw_apply_worktime_settings | default(false) | bool
|
||||
|
||||
- name: Normalize derived durationDefault for overnight shifts
|
||||
ansible.builtin.set_fact:
|
||||
aw_worktime_duration_default_effective: >-
|
||||
{{
|
||||
(aw_worktime_duration_default_derived | int)
|
||||
if (aw_worktime_duration_default_derived | int) > 0
|
||||
else ((aw_worktime_duration_default_derived | int) + 86400)
|
||||
}}
|
||||
when: aw_apply_worktime_settings | default(false) | bool
|
||||
|
||||
- name: Validate derived durationDefault is sane
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- aw_worktime_duration_default_effective | int > 0
|
||||
- aw_worktime_duration_default_effective | int <= 86400
|
||||
fail_msg: "Invalid worktime window: {{ aw_worktime_from }}..{{ aw_worktime_to }}"
|
||||
when: aw_apply_worktime_settings | default(false) | bool
|
||||
|
||||
- name: Apply baseline worktime period (startOfDay)
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/startOfDay"
|
||||
method: POST
|
||||
body: "{{ aw_worktime_start_of_day }}"
|
||||
body_format: json
|
||||
status_code: 200
|
||||
when: aw_apply_worktime_settings | default(false) | bool
|
||||
|
||||
- name: Apply baseline worktime period (durationDefault seconds)
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/durationDefault"
|
||||
method: POST
|
||||
body: "{{ aw_worktime_duration_default_effective }}"
|
||||
body_format: json
|
||||
status_code: 200
|
||||
when: aw_apply_worktime_settings | default(false) | bool
|
||||
|
||||
handlers:
|
||||
- name: Reload systemd
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Restart activitywatch
|
||||
ansible.builtin.systemd:
|
||||
name: activitywatch-server.service
|
||||
state: restarted
|
||||
@@ -0,0 +1,24 @@
|
||||
aw_server_version: "v0.13.2"
|
||||
aw_server_download_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-linux-x86_64.zip"
|
||||
aw_server_bind_host: "0.0.0.0"
|
||||
aw_server_port: 5600
|
||||
aw_server_webui_dir: "/opt/activitywatch/webui-ru"
|
||||
aw_server_data_dir: "/var/lib/activitywatch"
|
||||
aw_server_log_dir: "/var/log/activitywatch"
|
||||
aw_server_user: "activitywatch"
|
||||
aw_server_group: "activitywatch"
|
||||
|
||||
aw_repo_root: "/home/igor/tmp/AWatch-rus"
|
||||
|
||||
# Optional: apply a baseline worktime-focused categorization and views via AW settings API.
|
||||
# WARNING: this overwrites existing server-side settings/classes/views.
|
||||
aw_apply_worktime_settings: false
|
||||
|
||||
# Optional defaults for the worktime period in Web UI.
|
||||
# startOfDay controls day-boundary and default report window start.
|
||||
# durationDefault controls default time range (seconds) shown in UI.
|
||||
#
|
||||
# Recommended: set worktime window explicitly and let the playbook derive duration.
|
||||
aw_worktime_from: "08:00"
|
||||
aw_worktime_to: "17:00"
|
||||
aw_worktime_start_of_day: "{{ aw_worktime_from }}"
|
||||
@@ -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"
|
||||
@@ -0,0 +1,38 @@
|
||||
proxmox_ct_matrix:
|
||||
- id: "203"
|
||||
hostname: "activitywatch-user1"
|
||||
storage: "local-lvm"
|
||||
template: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
|
||||
rootfs_size: "8G"
|
||||
cores: "2"
|
||||
memory: "2048"
|
||||
swap: "512"
|
||||
bridge: "vmbr10"
|
||||
ip: "10.20.30.13/24"
|
||||
gw: "10.20.30.1"
|
||||
vlan: ""
|
||||
nameserver: "1.1.1.1 8.8.8.8"
|
||||
searchdomain: "example.internal"
|
||||
password: "CHANGE_ME"
|
||||
unprivileged: "1"
|
||||
onboot: "1"
|
||||
features: "nesting=1,keyctl=1"
|
||||
|
||||
- id: "204"
|
||||
hostname: "activitywatch-user2"
|
||||
storage: "local-lvm"
|
||||
template: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
|
||||
rootfs_size: "8G"
|
||||
cores: "2"
|
||||
memory: "2048"
|
||||
swap: "512"
|
||||
bridge: "vmbr10"
|
||||
ip: "10.20.30.14/24"
|
||||
gw: "10.20.30.1"
|
||||
vlan: ""
|
||||
nameserver: "1.1.1.1 8.8.8.8"
|
||||
searchdomain: "example.internal"
|
||||
password: "CHANGE_ME"
|
||||
unprivileged: "1"
|
||||
onboot: "1"
|
||||
features: "nesting=1,keyctl=1"
|
||||
@@ -0,0 +1,18 @@
|
||||
proxmox_ct_id: "203"
|
||||
proxmox_ct_hostname: "activitywatch-server"
|
||||
proxmox_ct_storage: "local-lvm"
|
||||
proxmox_ct_template: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
|
||||
proxmox_ct_rootfs_size: "8G"
|
||||
proxmox_ct_cores: "2"
|
||||
proxmox_ct_memory: "2048"
|
||||
proxmox_ct_swap: "512"
|
||||
proxmox_ct_bridge: "vmbr10"
|
||||
proxmox_ct_ip: "10.20.30.13/24"
|
||||
proxmox_ct_gw: "10.20.30.1"
|
||||
proxmox_ct_vlan: ""
|
||||
proxmox_ct_nameserver: "1.1.1.1 8.8.8.8"
|
||||
proxmox_ct_searchdomain: "example.internal"
|
||||
proxmox_ct_password: "CHANGE_ME"
|
||||
proxmox_ct_unprivileged: "1"
|
||||
proxmox_ct_onboot: "1"
|
||||
proxmox_ct_features: "nesting=1,keyctl=1"
|
||||
@@ -0,0 +1,8 @@
|
||||
[proxmox]
|
||||
pve-main ansible_host=192.168.10.2 ansible_user=root ansible_port=22
|
||||
|
||||
[aw_server]
|
||||
aw-ct ansible_host=10.20.30.13 ansible_user=root ansible_port=22
|
||||
|
||||
[aw_windows]
|
||||
win-node1 ansible_host=192.168.100.21 ansible_user=Administrator ansible_password=CHANGE_ME ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
- name: Provision single Proxmox CT and deploy AWatch-rus
|
||||
hosts: proxmox
|
||||
gather_facts: false
|
||||
|
||||
vars:
|
||||
proxmox_bootstrap_dir: "/tmp/aw-rus-bootstrap"
|
||||
aw_bootstrap_files:
|
||||
- install_aw_server.sh
|
||||
- apply_webui_ru_patch.sh
|
||||
- activitywatch-server.service
|
||||
- aw-server.env.example
|
||||
- aw-ru-patch.js
|
||||
- aw-sw-cleanup.js
|
||||
- aw-host-groups.json
|
||||
- settings/classes-worktime.json
|
||||
- settings/views-default.json
|
||||
|
||||
tasks:
|
||||
- name: Execute single-CT provisioning workflow
|
||||
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
|
||||
vars:
|
||||
ct_id: "{{ proxmox_ct_id }}"
|
||||
ct_hostname: "{{ proxmox_ct_hostname }}"
|
||||
ct_storage: "{{ proxmox_ct_storage }}"
|
||||
ct_template: "{{ proxmox_ct_template }}"
|
||||
ct_rootfs_size: "{{ proxmox_ct_rootfs_size }}"
|
||||
ct_cores: "{{ proxmox_ct_cores }}"
|
||||
ct_memory: "{{ proxmox_ct_memory }}"
|
||||
ct_swap: "{{ proxmox_ct_swap }}"
|
||||
ct_bridge: "{{ proxmox_ct_bridge }}"
|
||||
ct_ip: "{{ proxmox_ct_ip }}"
|
||||
ct_gw: "{{ proxmox_ct_gw }}"
|
||||
ct_vlan: "{{ proxmox_ct_vlan | default('') }}"
|
||||
ct_nameserver: "{{ proxmox_ct_nameserver | default('') }}"
|
||||
ct_searchdomain: "{{ proxmox_ct_searchdomain | default('') }}"
|
||||
ct_password: "{{ proxmox_ct_password }}"
|
||||
ct_unprivileged: "{{ proxmox_ct_unprivileged }}"
|
||||
ct_onboot: "{{ proxmox_ct_onboot }}"
|
||||
ct_features: "{{ proxmox_ct_features }}"
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
---
|
||||
- name: Provision Proxmox CT matrix and deploy AWatch-rus with RU patch
|
||||
hosts: proxmox
|
||||
gather_facts: false
|
||||
|
||||
vars:
|
||||
proxmox_bootstrap_dir: "/tmp/aw-rus-bootstrap"
|
||||
aw_bootstrap_files:
|
||||
- install_aw_server.sh
|
||||
- apply_webui_ru_patch.sh
|
||||
- activitywatch-server.service
|
||||
- aw-server.env.example
|
||||
- aw-ru-patch.js
|
||||
- aw-sw-cleanup.js
|
||||
- aw-host-groups.json
|
||||
- settings/classes-worktime.json
|
||||
- settings/views-default.json
|
||||
|
||||
tasks:
|
||||
- name: Validate CT matrix is provided
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- proxmox_ct_matrix is defined
|
||||
- proxmox_ct_matrix | length > 0
|
||||
fail_msg: "Define proxmox_ct_matrix in group_vars/proxmox-matrix.yml"
|
||||
|
||||
- name: Execute provisioning workflow for each CT
|
||||
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
|
||||
vars:
|
||||
ct_id: "{{ item.id }}"
|
||||
ct_hostname: "{{ item.hostname }}"
|
||||
ct_storage: "{{ item.storage }}"
|
||||
ct_template: "{{ item.template }}"
|
||||
ct_rootfs_size: "{{ item.rootfs_size }}"
|
||||
ct_cores: "{{ item.cores }}"
|
||||
ct_memory: "{{ item.memory }}"
|
||||
ct_swap: "{{ item.swap }}"
|
||||
ct_bridge: "{{ item.bridge }}"
|
||||
ct_ip: "{{ item.ip }}"
|
||||
ct_gw: "{{ item.gw }}"
|
||||
ct_vlan: "{{ item.vlan | default('') }}"
|
||||
ct_nameserver: "{{ item.nameserver | default('') }}"
|
||||
ct_searchdomain: "{{ item.searchdomain | default('') }}"
|
||||
ct_password: "{{ item.password }}"
|
||||
ct_unprivileged: "{{ item.unprivileged }}"
|
||||
ct_onboot: "{{ item.onboot }}"
|
||||
ct_features: "{{ item.features }}"
|
||||
loop: "{{ proxmox_ct_matrix }}"
|
||||
loop_control:
|
||||
label: "ct={{ item.id }} host={{ item.hostname }} ip={{ item.ip }}"
|
||||
@@ -0,0 +1,217 @@
|
||||
---
|
||||
- name: Validate required per-CT variables
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ct_id is defined
|
||||
- ct_hostname is defined
|
||||
- ct_storage is defined
|
||||
- ct_template is defined
|
||||
- ct_rootfs_size is defined
|
||||
- ct_cores is defined
|
||||
- ct_memory is defined
|
||||
- ct_swap is defined
|
||||
- ct_bridge is defined
|
||||
- ct_ip is defined
|
||||
- ct_gw is defined
|
||||
- ct_password is defined
|
||||
- ct_unprivileged is defined
|
||||
- ct_onboot is defined
|
||||
- ct_features is defined
|
||||
- aw_repo_root is defined
|
||||
- aw_server_version is defined
|
||||
- aw_server_download_url is defined
|
||||
- aw_server_bind_host is defined
|
||||
- aw_server_port is defined
|
||||
- aw_server_webui_dir is defined
|
||||
- aw_server_data_dir is defined
|
||||
- aw_server_log_dir is defined
|
||||
- aw_server_user is defined
|
||||
- aw_server_group is defined
|
||||
fail_msg: "Missing required variables for CT provisioning/deploy."
|
||||
|
||||
- name: Build CT network string
|
||||
ansible.builtin.set_fact:
|
||||
ct_net0: >-
|
||||
name=eth0,bridge={{ ct_bridge }},ip={{ ct_ip }},gw={{ ct_gw }}{% if (ct_vlan | default('') | string | length) > 0 %},tag={{ ct_vlan }}{% endif %}
|
||||
|
||||
- name: Check whether CT already exists
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- status
|
||||
- "{{ ct_id }}"
|
||||
register: ct_status_check
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Create CT when absent
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- create
|
||||
- "{{ ct_id }}"
|
||||
- "{{ ct_template }}"
|
||||
- --hostname
|
||||
- "{{ ct_hostname }}"
|
||||
- --cores
|
||||
- "{{ ct_cores }}"
|
||||
- --memory
|
||||
- "{{ ct_memory }}"
|
||||
- --swap
|
||||
- "{{ ct_swap }}"
|
||||
- --rootfs
|
||||
- "{{ ct_storage }}:{{ ct_rootfs_size }}"
|
||||
- --password
|
||||
- "{{ ct_password }}"
|
||||
- --unprivileged
|
||||
- "{{ ct_unprivileged }}"
|
||||
- --onboot
|
||||
- "{{ ct_onboot }}"
|
||||
- --features
|
||||
- "{{ ct_features }}"
|
||||
- --net0
|
||||
- "{{ ct_net0 }}"
|
||||
- --nameserver
|
||||
- "{{ ct_nameserver | default('') }}"
|
||||
- --searchdomain
|
||||
- "{{ ct_searchdomain | default('') }}"
|
||||
- --ostype
|
||||
- debian
|
||||
when: ct_status_check.rc != 0
|
||||
|
||||
- name: Check current CT runtime state
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- status
|
||||
- "{{ ct_id }}"
|
||||
register: ct_runtime_status
|
||||
changed_when: false
|
||||
|
||||
- name: Start CT when stopped
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- start
|
||||
- "{{ ct_id }}"
|
||||
when: "'stopped' in ct_runtime_status.stdout"
|
||||
|
||||
- name: Ensure bootstrap directory on Proxmox host
|
||||
ansible.builtin.file:
|
||||
path: "{{ proxmox_bootstrap_dir }}"
|
||||
state: directory
|
||||
mode: "0700"
|
||||
|
||||
- name: Copy AW bootstrap files to Proxmox host temp
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/{{ item }}"
|
||||
dest: "{{ proxmox_bootstrap_dir }}/{{ item }}"
|
||||
mode: "0644"
|
||||
loop: "{{ aw_bootstrap_files }}"
|
||||
|
||||
- name: Bootstrap CT OS dependencies
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- exec
|
||||
- "{{ ct_id }}"
|
||||
- --
|
||||
- bash
|
||||
- -lc
|
||||
- |
|
||||
set -euo pipefail
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y curl ca-certificates bash unzip xz-utils jq rsync openssh-server
|
||||
mkdir -p /root/bootstrap /etc/activitywatch
|
||||
systemctl enable ssh || true
|
||||
systemctl restart ssh || true
|
||||
|
||||
- name: Push bootstrap files into CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- push
|
||||
- "{{ ct_id }}"
|
||||
- "{{ proxmox_bootstrap_dir }}/{{ item }}"
|
||||
- "/root/bootstrap/{{ item }}"
|
||||
loop: "{{ aw_bootstrap_files }}"
|
||||
|
||||
- name: Write AW server env file on Proxmox host temp
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ proxmox_bootstrap_dir }}/aw-server.env"
|
||||
mode: "0600"
|
||||
content: |
|
||||
AW_SERVER_VERSION={{ aw_server_version }}
|
||||
AW_SERVER_DOWNLOAD_URL={{ aw_server_download_url }}
|
||||
AW_SERVER_BIND_HOST={{ aw_server_bind_host }}
|
||||
AW_SERVER_PORT={{ aw_server_port }}
|
||||
AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }}
|
||||
AW_SERVER_DATA_DIR={{ aw_server_data_dir }}
|
||||
AW_SERVER_LOG_DIR={{ aw_server_log_dir }}
|
||||
AW_SERVER_USER={{ aw_server_user }}
|
||||
AW_SERVER_GROUP={{ aw_server_group }}
|
||||
|
||||
- name: Push AW server env into CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- push
|
||||
- "{{ ct_id }}"
|
||||
- "{{ proxmox_bootstrap_dir }}/aw-server.env"
|
||||
- /etc/activitywatch/aw-server.env
|
||||
|
||||
- name: Set mode for env inside CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- exec
|
||||
- "{{ ct_id }}"
|
||||
- --
|
||||
- chmod
|
||||
- "0600"
|
||||
- /etc/activitywatch/aw-server.env
|
||||
|
||||
- name: Install server and apply RU patch inside CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- exec
|
||||
- "{{ ct_id }}"
|
||||
- --
|
||||
- bash
|
||||
- -lc
|
||||
- |
|
||||
set -euo pipefail
|
||||
chmod +x /root/bootstrap/install_aw_server.sh /root/bootstrap/apply_webui_ru_patch.sh
|
||||
bash /root/bootstrap/install_aw_server.sh
|
||||
bash /root/bootstrap/apply_webui_ru_patch.sh
|
||||
systemctl restart activitywatch-server.service
|
||||
|
||||
- name: Validate AW API from inside CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- exec
|
||||
- "{{ ct_id }}"
|
||||
- --
|
||||
- bash
|
||||
- -lc
|
||||
- "curl -fsS http://127.0.0.1:{{ aw_server_port }}/api/0/info >/dev/null"
|
||||
|
||||
- name: Validate RU patch hooks in index
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- exec
|
||||
- "{{ ct_id }}"
|
||||
- --
|
||||
- bash
|
||||
- -lc
|
||||
- "grep -q 'ru-patch-v5.js' {{ aw_server_webui_dir }}/index.html && grep -q 'sw-cleanup.js' {{ aw_server_webui_dir }}/index.html"
|
||||
|
||||
- name: Show final endpoint
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "CT {{ ct_id }} is provisioned and configured."
|
||||
- "ActivityWatch endpoint: http://{{ ct_ip | regex_replace('/[0-9]+$', '') }}:{{ aw_server_port }}"
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=ActivityWatch Server (Rust)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=/etc/activitywatch/aw-server.env
|
||||
User=__AW_SERVER_USER__
|
||||
Group=__AW_SERVER_GROUP__
|
||||
WorkingDirectory=__AW_SERVER_DATA_DIR__
|
||||
ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT"'
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
StateDirectory=activitywatch
|
||||
LogsDirectory=activitywatch
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
ENV_FILE="/etc/activitywatch/aw-server.env"
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "missing env file: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source "$ENV_FILE"
|
||||
|
||||
WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-${AW_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)
|
||||
PATCH_TARGET="$WEBUI_DIR/js/ru-patch-v5.js"
|
||||
SW_TARGET="$WEBUI_DIR/js/sw-cleanup.js"
|
||||
HOST_GROUPS_TARGET="$WEBUI_DIR/js/aw-host-groups.json"
|
||||
TRENDS_NEEDLE='this.activityStore.query_category_time_by_period(r)'
|
||||
TRENDS_REPLACEMENT='this.activityStore.ensure_loaded(r)'
|
||||
TIMESPIRAL_NEEDLE='start:new Date("2022-08-08")'
|
||||
TIMESPIRAL_REPLACEMENT='start:new Date(Date.now()-12*36e5)'
|
||||
CATEGORY_HELPER_NEEDLE='hostname:t.hostnameChoices[0]'
|
||||
CATEGORY_HELPER_REPLACEMENT='hostname:t.hostnameChoices.filter((function(t){return"unknown"!==t}))[0]||t.hostnameChoices[0]'
|
||||
|
||||
[[ -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" "$PATCH_TARGET"
|
||||
install -m 0644 "$SW_CLEANUP_SRC" "$SW_TARGET"
|
||||
install -m 0644 "$HOST_GROUPS_SRC" "$HOST_GROUPS_TARGET"
|
||||
cp "$INDEX_HTML" "$INDEX_HTML.bak.$TS"
|
||||
|
||||
patch_hash="$(sha1sum "$PATCH_TARGET" | awk '{print substr($1,1,12)}')"
|
||||
sw_hash="$(sha1sum "$SW_TARGET" | awk '{print substr($1,1,12)}')"
|
||||
|
||||
sed -i '/ru-patch-v5.js/d;/sw-cleanup.js/d;/aw-ru-patch.js/d;/aw-sw-cleanup.js/d' "$INDEX_HTML"
|
||||
sed -i "s#</head>#<script src=\"/js/sw-cleanup.js?v=$sw_hash\"></script></head>#" "$INDEX_HTML"
|
||||
sed -i "s#</body>#<script defer=\"defer\" src=\"/js/ru-patch-v5.js?v=$patch_hash\"></script></body>#" "$INDEX_HTML"
|
||||
cp "$SW_CLEANUP_SRC" "$SERVICE_WORKER"
|
||||
|
||||
trends_chunk="$(grep -Rsl "$TRENDS_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
|
||||
if [[ -n "$trends_chunk" ]]; then
|
||||
cp "$trends_chunk" "$trends_chunk.bak.$TS"
|
||||
python3 - "$trends_chunk" "$TRENDS_NEEDLE" "$TRENDS_REPLACEMENT" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
old = sys.argv[2]
|
||||
new = sys.argv[3]
|
||||
content = path.read_text()
|
||||
if old in content:
|
||||
path.write_text(content.replace(old, new, 1))
|
||||
print(f"Trends hotfix applied to {path}")
|
||||
else:
|
||||
print(f"Trends hotfix already present in {path}")
|
||||
PY
|
||||
else
|
||||
echo "Trends hotfix skipped: chunk not found"
|
||||
fi
|
||||
|
||||
timespiral_chunk="$(grep -Rsl "$TIMESPIRAL_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
|
||||
if [[ -n "$timespiral_chunk" ]]; then
|
||||
cp "$timespiral_chunk" "$timespiral_chunk.bak.$TS"
|
||||
python3 - "$timespiral_chunk" "$TIMESPIRAL_NEEDLE" "$TIMESPIRAL_REPLACEMENT" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
old = sys.argv[2]
|
||||
new = sys.argv[3]
|
||||
content = path.read_text()
|
||||
if old in content:
|
||||
path.write_text(content.replace(old, new, 1))
|
||||
print(f"Timespiral hotfix applied to {path}")
|
||||
else:
|
||||
print(f"Timespiral hotfix already present in {path}")
|
||||
PY
|
||||
else
|
||||
echo "Timespiral hotfix skipped: chunk not found"
|
||||
fi
|
||||
|
||||
category_helper_chunk="$(grep -Rsl "$CATEGORY_HELPER_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
|
||||
if [[ -n "$category_helper_chunk" ]]; then
|
||||
cp "$category_helper_chunk" "$category_helper_chunk.bak.$TS"
|
||||
python3 - "$category_helper_chunk" "$CATEGORY_HELPER_NEEDLE" "$CATEGORY_HELPER_REPLACEMENT" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
old = sys.argv[2]
|
||||
new = sys.argv[3]
|
||||
content = path.read_text()
|
||||
if old in content:
|
||||
path.write_text(content.replace(old, new, 1))
|
||||
print(f"Category helper host hotfix applied to {path}")
|
||||
else:
|
||||
print(f"Category helper host hotfix already present in {path}")
|
||||
PY
|
||||
else
|
||||
echo "Category helper host hotfix skipped: chunk not found"
|
||||
fi
|
||||
|
||||
echo "RU patch applied to $WEBUI_DIR (ru-patch-v5.js?v=$patch_hash)"
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"id": "pve-detmir",
|
||||
"name": "pve-detmir",
|
||||
"description": "Выделенный клиент DetMir в разделе Активность.",
|
||||
"patterns": [
|
||||
"^pve-detmir$"
|
||||
],
|
||||
"links": [
|
||||
{ "label": "Активность", "type": "activity", "view": "pve_audit" },
|
||||
{ "label": "Web-admin аудит", "type": "bucket", "bucket_prefix": "aw-pve-webadmin-events_" },
|
||||
{ "label": "PVE tasks", "type": "bucket", "bucket_prefix": "aw-pve-task-events_" },
|
||||
{ "label": "SSH сессии", "type": "bucket", "bucket_prefix": "aw-ssh-sessions_" },
|
||||
{ "label": "Команды shell", "type": "bucket", "bucket_prefix": "aw-console-commands_" },
|
||||
{ "label": "Web категории", "type": "bucket", "bucket_prefix": "aw-detmir-web-category_" },
|
||||
{ "label": "Все бакеты", "type": "buckets" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "Прочие хосты"
|
||||
}
|
||||
+1627
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
# Copy to /etc/activitywatch/aw-server.env and fill with real values.
|
||||
|
||||
AW_SERVER_VERSION=0.13.2
|
||||
AW_SERVER_DOWNLOAD_URL=https://github.com/ActivityWatch/aw-server-rust/releases/download/v0.13.2/aw-server-rust-linux-x86_64.zip
|
||||
AW_SERVER_BIND_HOST=0.0.0.0
|
||||
AW_SERVER_PORT=5600
|
||||
AW_SERVER_WEBUI_DIR=/opt/activitywatch/webui-ru
|
||||
AW_SERVER_DATA_DIR=/var/lib/activitywatch
|
||||
AW_SERVER_LOG_DIR=/var/log/activitywatch
|
||||
AW_SERVER_USER=activitywatch
|
||||
AW_SERVER_GROUP=activitywatch
|
||||
@@ -0,0 +1,18 @@
|
||||
self.addEventListener("install", function (event) {
|
||||
self.skipWaiting();
|
||||
event.waitUntil((async function () {
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(keys.map(function (key) { return caches.delete(key); }));
|
||||
})());
|
||||
});
|
||||
|
||||
self.addEventListener("activate", function (event) {
|
||||
event.waitUntil((async function () {
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(keys.map(function (key) { return caches.delete(key); }));
|
||||
await self.clients.claim();
|
||||
await self.registration.unregister();
|
||||
})());
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", function () {});
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
ENV_FILE="/etc/activitywatch/aw-server.env"
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "missing env file: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source "$ENV_FILE"
|
||||
|
||||
required_vars=(
|
||||
AW_SERVER_VERSION
|
||||
AW_SERVER_DOWNLOAD_URL
|
||||
AW_SERVER_BIND_HOST
|
||||
AW_SERVER_PORT
|
||||
AW_SERVER_WEBUI_DIR
|
||||
AW_SERVER_DATA_DIR
|
||||
AW_SERVER_LOG_DIR
|
||||
AW_SERVER_USER
|
||||
AW_SERVER_GROUP
|
||||
)
|
||||
|
||||
BOOTSTRAP_DIR="/root/bootstrap"
|
||||
VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json"
|
||||
CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json"
|
||||
|
||||
for var_name in "${required_vars[@]}"; do
|
||||
if [[ -z "${!var_name:-}" ]]; then
|
||||
echo "missing required variable: $var_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y curl ca-certificates unzip jq
|
||||
|
||||
if ! getent group "$AW_SERVER_GROUP" >/dev/null; then
|
||||
groupadd --system "$AW_SERVER_GROUP"
|
||||
fi
|
||||
|
||||
if ! id "$AW_SERVER_USER" >/dev/null 2>&1; then
|
||||
useradd --system --gid "$AW_SERVER_GROUP" --home-dir "$AW_SERVER_DATA_DIR" --shell /usr/sbin/nologin "$AW_SERVER_USER"
|
||||
fi
|
||||
|
||||
install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" /opt/activitywatch/bin
|
||||
install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" /opt/activitywatch/releases
|
||||
install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$AW_SERVER_WEBUI_DIR"
|
||||
install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$AW_SERVER_DATA_DIR"
|
||||
install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$AW_SERVER_LOG_DIR"
|
||||
|
||||
tmp_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
curl -fL "$AW_SERVER_DOWNLOAD_URL" -o "$tmp_dir/aw-server.zip"
|
||||
unzip -q "$tmp_dir/aw-server.zip" -d "$tmp_dir/unpacked"
|
||||
|
||||
server_bin=$(find "$tmp_dir/unpacked" -type f \( -name 'aw-server-rust' -o -name 'aw-server' \) | head -n 1)
|
||||
webui_dir=$(find "$tmp_dir/unpacked" -type d \( -name 'webui' -o -name 'aw-webui' \) | head -n 1 || true)
|
||||
|
||||
if [[ -z "$server_bin" || ! -f "$server_bin" ]]; then
|
||||
echo "aw-server binary not found in archive" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release_dir="/opt/activitywatch/releases/aw-server-rust-v${AW_SERVER_VERSION}"
|
||||
rm -rf "$release_dir"
|
||||
install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$release_dir"
|
||||
install -m 0755 -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$server_bin" "$release_dir/aw-server-rust"
|
||||
ln -sfn "$release_dir/aw-server-rust" /opt/activitywatch/bin/aw-server-rust
|
||||
|
||||
if [[ -n "$webui_dir" && -d "$webui_dir" ]]; then
|
||||
rm -rf "$AW_SERVER_WEBUI_DIR"
|
||||
mkdir -p "$AW_SERVER_WEBUI_DIR"
|
||||
cp -a "$webui_dir"/. "$AW_SERVER_WEBUI_DIR"/
|
||||
chown -R "$AW_SERVER_USER:$AW_SERVER_GROUP" "$AW_SERVER_WEBUI_DIR"
|
||||
fi
|
||||
|
||||
sed \
|
||||
-e "s#__AW_SERVER_USER__#$AW_SERVER_USER#g" \
|
||||
-e "s#__AW_SERVER_GROUP__#$AW_SERVER_GROUP#g" \
|
||||
-e "s#__AW_SERVER_DATA_DIR__#$AW_SERVER_DATA_DIR#g" \
|
||||
/root/bootstrap/activitywatch-server.service > /etc/systemd/system/activitywatch-server.service
|
||||
chmod 0644 /etc/systemd/system/activitywatch-server.service
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable activitywatch-server.service
|
||||
systemctl restart activitywatch-server.service
|
||||
systemctl --no-pager --full status activitywatch-server.service || true
|
||||
|
||||
for _ in $(seq 1 20); do
|
||||
if curl -fsS "http://127.0.0.1:${AW_SERVER_PORT}/api/0/info" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [[ -f "$CLASSES_JSON" ]]; then
|
||||
curl -fsS -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-binary @"$CLASSES_JSON" \
|
||||
"http://127.0.0.1:${AW_SERVER_PORT}/api/0/settings/classes" >/dev/null
|
||||
echo "Applied worktime classes from $CLASSES_JSON"
|
||||
else
|
||||
echo "Worktime classes bootstrap not found, skipped: $CLASSES_JSON"
|
||||
fi
|
||||
|
||||
if [[ -f "$VIEWS_JSON" ]]; then
|
||||
curl -fsS -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-binary @"$VIEWS_JSON" \
|
||||
"http://127.0.0.1:${AW_SERVER_PORT}/api/0/settings/views" >/dev/null
|
||||
echo "Applied baseline views from $VIEWS_JSON"
|
||||
else
|
||||
echo "Views bootstrap not found, skipped: $VIEWS_JSON"
|
||||
fi
|
||||
@@ -0,0 +1,90 @@
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"name": ["Работа"],
|
||||
"rule": { "type": "none" },
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": ["Работа", "1С"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "\\b(1cv8s?|1cv8c|1cestart)\\.exe\\b|1С:Предприятие|Запуск 1С:Предприятия|Загрузка конфигурационной информации|Доступ к информационной базе",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": { "color": "#194D33" }
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": ["Работа", "Документы"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64)\\.exe\\b|Adobe Reader|Acrobat",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": { "color": "#2E7D32" }
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": ["Работа", "Коммуникации"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "\\b(teams|telegram|slack|thunderbird|zoom|skype|whatsapp|viber|discord)\\.exe\\b|Mattermost|Element|Riot",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": { "color": "#1E88E5" }
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": ["Работа", "Администрирование"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd)\\.exe\\b",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": { "color": "#6D4C41" }
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": ["Интернет"],
|
||||
"rule": { "type": "none" },
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": ["Интернет", "Браузер"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser)\\.exe\\b",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": { "color": "#00897B" }
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": ["Система"],
|
||||
"rule": { "type": "none" },
|
||||
"data": {}
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": ["Система", "Windows"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "\\b(SearchHost|explorer|ShellExperienceHost|ApplicationFrameHost|RuntimeBroker|sihost|dwm|svchost|fontdrvhost|userinit)\\.exe\\b|\\\\Windows\\\\System32",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": { "color": "#607D8B" }
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": ["ActivityWatch"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
[
|
||||
{
|
||||
"id": "summary",
|
||||
"name": "Summary",
|
||||
"elements": [
|
||||
{ "type": "top_titles", "size": 3 },
|
||||
{ "type": "timeline_barchart", "size": 3 },
|
||||
{ "type": "top_categories", "size": 3 },
|
||||
{ "type": "category_tree", "size": 3 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "window",
|
||||
"name": "Window",
|
||||
"elements": [
|
||||
{ "type": "top_apps", "size": 3, "props": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "DLP",
|
||||
"name": "DLP",
|
||||
"elements": []
|
||||
},
|
||||
{
|
||||
"id": "worktime",
|
||||
"name": "Worktime",
|
||||
"elements": [
|
||||
{ "type": "top_categories", "size": 3, "props": {} },
|
||||
{ "type": "timeline_barchart", "size": 3, "props": {} },
|
||||
{ "type": "category_tree", "size": 3, "props": {} },
|
||||
{ "type": "top_apps", "size": 3, "props": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pve_audit",
|
||||
"name": "PVE Audit",
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
@@ -25,11 +25,16 @@ for file_name in \
|
||||
activitywatch-server.service \
|
||||
aw-server.env.example \
|
||||
aw-ru-patch.js \
|
||||
aw-sw-cleanup.js
|
||||
aw-sw-cleanup.js \
|
||||
aw-host-groups.json
|
||||
do
|
||||
pct push "$CT_ID" "$PROJECT_ROOT/aw-server/$file_name" "/root/bootstrap/$file_name"
|
||||
done
|
||||
|
||||
pct exec "$CT_ID" -- mkdir -p /root/bootstrap/settings
|
||||
pct push "$CT_ID" "$PROJECT_ROOT/aw-server/settings/classes-worktime.json" "/root/bootstrap/settings/classes-worktime.json"
|
||||
pct push "$CT_ID" "$PROJECT_ROOT/aw-server/settings/views-default.json" "/root/bootstrap/settings/views-default.json"
|
||||
|
||||
if [ -n "${AW_SERVER_VERSION:-}" ] &&
|
||||
[ -n "${AW_SERVER_DOWNLOAD_URL:-}" ] &&
|
||||
[ -n "${AW_SERVER_BIND_HOST:-}" ] &&
|
||||
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_DIR="windows/installkit/innosetup"
|
||||
TARGET_FILE="$TARGET_DIR/innosetup-rdp-package-filelist.md"
|
||||
POINTER_FILE="docs/windows/innosetup-rdp-package-filelist.md"
|
||||
ISS_FILE="$TARGET_DIR/AWatch-rus-InnoSetup.iss"
|
||||
PAYLOAD_DIR="$TARGET_DIR/payload"
|
||||
|
||||
mkdir -p "$TARGET_DIR" "$PAYLOAD_DIR" "docs/windows"
|
||||
|
||||
cat > "$TARGET_FILE" <<'DOC'
|
||||
# Inno Setup: файл-лист для Windows RDP deployment
|
||||
|
||||
Дата актуализации: 2026-05-02 (UTC).
|
||||
|
||||
## Что это за документ
|
||||
|
||||
Этот файл — **чеклист упаковки** для Inno Setup.
|
||||
|
||||
- Он описывает, **что класть** в инсталлятор.
|
||||
- Он описывает, **что не класть** (генерируется уже на целевом хосте).
|
||||
- Он **не меняет** текущие deploy-скрипты и логику проекта.
|
||||
|
||||
## Важное уточнение по `phase2`
|
||||
|
||||
Чтобы исключить путаницу:
|
||||
|
||||
1. В каталоге `windows/` нет файлов с именами `phase2-*`.
|
||||
2. `phase2` в проекте — это обозначение этапа/набора телеметрии (DLP + endpoint signals).
|
||||
3. Файлы вида `phase2-*.deployment-config.json` — это **примерные конфиги install-kit**, они лежат в `install-kit-*/server-configs-*`.
|
||||
|
||||
## 1) Обязательные файлы для Inno Setup пакета
|
||||
|
||||
### 1.1 PowerShell-модуль
|
||||
- `windows/ActivityWatch.Windows.Common.psd1`
|
||||
- `windows/ActivityWatch.Windows.Common.psm1`
|
||||
|
||||
### 1.2 Скрипты деплоя и сопровождения
|
||||
- `windows/deploy-single-user.ps1`
|
||||
- `windows/deploy-domain-users.ps1`
|
||||
- `windows/deploy-ensemble.ps1`
|
||||
- `windows/hardening-recovery.ps1`
|
||||
- `windows/validate-deployment.ps1`
|
||||
|
||||
### 1.3 Коллекторы
|
||||
- `windows/worktime-session-collector.ps1` (RDP/session presence)
|
||||
- `windows/browser-domains-native-collector.ps1`
|
||||
- `windows/dlp-endpoint-signals-collector.ps1`
|
||||
|
||||
### 1.4 Шаблоны конфигурации
|
||||
- `windows/web-category-rules.example.json`
|
||||
- `windows/dlp-policy.example.json`
|
||||
|
||||
## 2) Бинарный payload ActivityWatch
|
||||
|
||||
Для закрытой среды используется **offline-режим по умолчанию**:
|
||||
|
||||
- В комплект Inno Setup сразу включается `payload\activitywatch-v0.13.2-windows-x86_64.zip`.
|
||||
- Deploy запускается с параметром `-PackageZipPath` на локальный ZIP из `{app}\payload`.
|
||||
- Online-загрузка из GitHub Releases в закрытой среде не требуется.
|
||||
|
||||
## 3) Что НЕ включать в installer как статические файлы
|
||||
|
||||
Эти файлы/папки появляются на целевом Windows-хосте во время/после деплоя:
|
||||
|
||||
- `C:\ProgramData\ActivityWatch\deployment-config.json`
|
||||
- `C:\ProgramData\ActivityWatch\web-category-rules.json`
|
||||
- `C:\ProgramData\ActivityWatch\dlp-policy.json`
|
||||
- `C:\ProgramData\ActivityWatch\logs\*`
|
||||
- `%LOCALAPPDATA%\ActivityWatch-Phase2\incident-artifacts\*`
|
||||
|
||||
## 4) Опционально приложить в операторский install-kit
|
||||
|
||||
- `docs/windows/deployment.md`
|
||||
- `docs/windows/validation.md`
|
||||
- `docs/windows/troubleshooting.md`
|
||||
- `docs/windows/ensemble.md`
|
||||
|
||||
## 5) Рекомендуемая структура внутри пакета
|
||||
|
||||
- `windows\ActivityWatch.Windows.Common.psd1`
|
||||
- `windows\ActivityWatch.Windows.Common.psm1`
|
||||
- `windows\deploy-single-user.ps1`
|
||||
- `windows\deploy-domain-users.ps1`
|
||||
- `windows\deploy-ensemble.ps1`
|
||||
- `windows\hardening-recovery.ps1`
|
||||
- `windows\validate-deployment.ps1`
|
||||
- `windows\worktime-session-collector.ps1`
|
||||
- `windows\browser-domains-native-collector.ps1`
|
||||
- `windows\dlp-endpoint-signals-collector.ps1`
|
||||
- `windows\web-category-rules.example.json`
|
||||
- `windows\dlp-policy.example.json`
|
||||
- `payload\activitywatch-v0.13.2-windows-x86_64.zip`
|
||||
|
||||
## 6) Контроль перед сборкой .iss
|
||||
|
||||
1. Все файлы из раздела 1 присутствуют.
|
||||
2. ZIP `activitywatch-v0.13.2-windows-x86_64.zip` лежит в `windows/installkit/innosetup/payload/`.
|
||||
3. В .iss добавлен `Source: "payload\activitywatch-v0.13.2-windows-x86_64.zip"`.
|
||||
4. Deploy в .iss запускается с `-PackageZipPath` на локальный ZIP.
|
||||
5. После установки запускается `validate-deployment.ps1` с сохранением JSON-отчёта.
|
||||
DOC
|
||||
|
||||
cat > "$POINTER_FILE" <<'DOC'
|
||||
windows/installkit/innosetup/innosetup-rdp-package-filelist.md
|
||||
DOC
|
||||
|
||||
cat > "$ISS_FILE" <<'DOC'
|
||||
#define MyAppName "AWatch-rus InstallKit"
|
||||
#define MyAppVersion "1.0.0"
|
||||
#define MyAppPublisher "AWatch-rus"
|
||||
|
||||
[Setup]
|
||||
AppId={{6D6A1F74-0F4F-4A57-B5E3-1C2C2F56C0E9}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
DefaultDirName={autopf}\AWatch-rus
|
||||
DefaultGroupName=AWatch-rus
|
||||
OutputDir=.
|
||||
OutputBaseFilename=AWatch-rus-InstallKit-offline
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
PrivilegesRequired=admin
|
||||
|
||||
[Languages]
|
||||
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
|
||||
|
||||
[Files]
|
||||
Source: "..\..\ActivityWatch.Windows.Common.psd1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\ActivityWatch.Windows.Common.psm1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\deploy-single-user.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\deploy-domain-users.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\deploy-ensemble.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\hardening-recovery.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\validate-deployment.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\worktime-session-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\browser-domains-native-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\dlp-endpoint-signals-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\web-category-rules.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\dlp-policy.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "payload\activitywatch-v0.13.2-windows-x86_64.zip"; DestDir: "{app}\payload"; Flags: ignoreversion
|
||||
Source: "innosetup-rdp-package-filelist.md"; DestDir: "{app}\windows\installkit\innosetup"; Flags: ignoreversion
|
||||
|
||||
[Run]
|
||||
Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File \"{app}\windows\deploy-ensemble.ps1\" -PackageZipPath \"{app}\payload\activitywatch-v0.13.2-windows-x86_64.zip\""; Flags: runhidden
|
||||
Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File \"{app}\windows\validate-deployment.ps1\""; Flags: runhidden
|
||||
DOC
|
||||
|
||||
touch "$PAYLOAD_DIR/.gitkeep"
|
||||
|
||||
echo "$TARGET_FILE"
|
||||
echo "$POINTER_FILE"
|
||||
echo "$ISS_FILE"
|
||||
echo "$PAYLOAD_DIR/.gitkeep"
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
SERVER_HOST="10.10.10.13"
|
||||
SERVER_PORT="5600"
|
||||
POLL_INTERVAL="5"
|
||||
AW_VERSION="0.13.2"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: install_aw_linux_remote_worker.sh [options]
|
||||
|
||||
Options:
|
||||
--server-host HOST Remote AW server host (default: 10.10.10.13)
|
||||
--server-port PORT Remote AW server port (default: 5600)
|
||||
--poll-interval SEC Poll interval for Linux loggers (default: 5)
|
||||
--version VERSION ActivityWatch version for GUI watcher bundle (default: 0.13.2)
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--server-host)
|
||||
SERVER_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--server-port)
|
||||
SERVER_PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--poll-interval)
|
||||
POLL_INTERVAL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
AW_VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
|
||||
sh "${SCRIPT_DIR}/install_aw_linux_client.sh" \
|
||||
--server-host "${SERVER_HOST}" \
|
||||
--server-port "${SERVER_PORT}" \
|
||||
--version "${AW_VERSION}"
|
||||
|
||||
sh "${SCRIPT_DIR}/install_aw_console_ssh_logger.sh" \
|
||||
--server-host "${SERVER_HOST}" \
|
||||
--server-port "${SERVER_PORT}" \
|
||||
--poll-interval "${POLL_INTERVAL}"
|
||||
|
||||
sh "${SCRIPT_DIR}/install_aw_linux_web_category_logger.sh" \
|
||||
--server-host "${SERVER_HOST}" \
|
||||
--server-port "${SERVER_PORT}" \
|
||||
--poll-interval "${POLL_INTERVAL}"
|
||||
|
||||
echo "Linux remote worker full-stack install completed."
|
||||
echo "Expected buckets on AW server:"
|
||||
echo " - aw-watcher-window_$(hostname -s)"
|
||||
echo " - aw-watcher-afk_$(hostname -s)"
|
||||
echo " - aw-console-commands_$(hostname -s)"
|
||||
echo " - aw-ssh-sessions_$(hostname -s)"
|
||||
echo " - aw-linux-web-context_$(hostname -s)"
|
||||
echo " - aw-detmir-web-category_$(hostname -s)"
|
||||
@@ -0,0 +1,367 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
SERVER_HOST="10.10.10.13"
|
||||
SERVER_PORT="5600"
|
||||
POLL_INTERVAL="5"
|
||||
INSTALL_ROOT="${HOME}/.local/opt/aw-linux-web-category"
|
||||
BIN_DIR="${HOME}/.local/bin"
|
||||
STATE_DIR="${HOME}/.local/state/aw-linux-web-category"
|
||||
LOG_DIR="${HOME}/.local/state/aw-linux-web-category/logs"
|
||||
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/aw-linux-web-category"
|
||||
SYSTEMD_USER_DIR="${HOME}/.config/systemd/user"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: install_aw_linux_web_category_logger.sh [options]
|
||||
|
||||
Options:
|
||||
--server-host HOST AW server host (default: 10.10.10.13)
|
||||
--server-port PORT AW server port (default: 5600)
|
||||
--poll-interval SEC Poll interval in seconds (default: 5)
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--server-host)
|
||||
SERVER_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--server-port)
|
||||
SERVER_PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--poll-interval)
|
||||
POLL_INTERVAL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
mkdir -p "${INSTALL_ROOT}" "${BIN_DIR}" "${STATE_DIR}" "${LOG_DIR}" "${CONFIG_DIR}" "${SYSTEMD_USER_DIR}"
|
||||
|
||||
write_file() {
|
||||
target="$1"
|
||||
mkdir -p "$(dirname "${target}")"
|
||||
cat > "${target}"
|
||||
}
|
||||
|
||||
write_file "${CONFIG_DIR}/rules.json" <<'EOF'
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"id": "proxmox-webui",
|
||||
"categoryGroup": "work",
|
||||
"category": "Администрирование",
|
||||
"service": "proxmox",
|
||||
"interface": "https",
|
||||
"port": 8006,
|
||||
"rootDomain": "proxmox-webui",
|
||||
"windowClassRegex": "(?i)(firefox|chromium|chrome|brave|vivaldi|opera)",
|
||||
"titleRegex": "(?i)proxmox\\s+virtual\\s+environment|\\bproxmox\\b|\\bnode\\b.*\\bsummary\\b|\\bvirtual machine\\b"
|
||||
},
|
||||
{
|
||||
"id": "pfsense-webui",
|
||||
"categoryGroup": "work",
|
||||
"category": "Администрирование",
|
||||
"service": "pfsense",
|
||||
"interface": "https",
|
||||
"port": 443,
|
||||
"rootDomain": "pfsense-webui",
|
||||
"windowClassRegex": "(?i)(firefox|chromium|chrome|brave|vivaldi|opera)",
|
||||
"titleRegex": "(?i)\\bpfsense\\b|\\bfirewall\\b"
|
||||
},
|
||||
{
|
||||
"id": "grafana-webui",
|
||||
"categoryGroup": "work",
|
||||
"category": "Администрирование",
|
||||
"service": "grafana",
|
||||
"interface": "https",
|
||||
"port": 3000,
|
||||
"rootDomain": "grafana-webui",
|
||||
"windowClassRegex": "(?i)(firefox|chromium|chrome|brave|vivaldi|opera)",
|
||||
"titleRegex": "(?i)\\bgrafana\\b|dashboard"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
write_file "${INSTALL_ROOT}/config.json" <<EOF
|
||||
{
|
||||
"server_host": "${SERVER_HOST}",
|
||||
"server_port": ${SERVER_PORT},
|
||||
"poll_interval_seconds": ${POLL_INTERVAL},
|
||||
"hostname": "$(hostname -s)",
|
||||
"username": "$(id -un)",
|
||||
"state_dir": "${STATE_DIR}",
|
||||
"rules_path": "${CONFIG_DIR}/rules.json",
|
||||
"raw_bucket": "aw-linux-web-context_$(hostname -s)",
|
||||
"category_bucket": "aw-detmir-web-category_$(hostname -s)"
|
||||
}
|
||||
EOF
|
||||
|
||||
write_file "${INSTALL_ROOT}/collector.py" <<'EOF'
|
||||
#!/usr/bin/env python3
|
||||
import datetime as dt
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
def iso_now():
|
||||
return dt.datetime.now(tz=dt.timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
|
||||
|
||||
class Collector:
|
||||
def __init__(self, cfg):
|
||||
self.cfg = cfg
|
||||
self.server = f"http://{cfg['server_host']}:{cfg['server_port']}/api/0"
|
||||
self.host = cfg.get("hostname") or socket.gethostname().split(".")[0]
|
||||
self.user = cfg.get("username") or os.environ.get("USER", "unknown")
|
||||
self.poll_interval = max(1, int(cfg.get("poll_interval_seconds", 5)))
|
||||
self.state_dir = pathlib.Path(cfg["state_dir"]).expanduser()
|
||||
self.state_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.raw_bucket = cfg.get("raw_bucket", f"aw-linux-web-context_{self.host}")
|
||||
self.category_bucket = cfg.get("category_bucket", f"aw-detmir-web-category_{self.host}")
|
||||
self.rules = self._load_rules(pathlib.Path(cfg["rules_path"]).expanduser())
|
||||
self.ensured = set()
|
||||
self.last_raw_key = None
|
||||
self.last_category_key = None
|
||||
|
||||
def _load_rules(self, path):
|
||||
if not path.exists():
|
||||
return []
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
rules = []
|
||||
for item in payload.get("rules", []):
|
||||
rules.append({
|
||||
"id": item.get("id", "rule"),
|
||||
"categoryGroup": item.get("categoryGroup", "work"),
|
||||
"category": item.get("category", "Работа"),
|
||||
"service": item.get("service", ""),
|
||||
"interface": item.get("interface", "https"),
|
||||
"port": item.get("port"),
|
||||
"rootDomain": item.get("rootDomain", item.get("service", "web-ui")),
|
||||
"windowClassRegex": re.compile(item.get("windowClassRegex", ".*")),
|
||||
"titleRegex": re.compile(item.get("titleRegex", ".*"))
|
||||
})
|
||||
return rules
|
||||
|
||||
def _run(self, *cmd):
|
||||
try:
|
||||
return subprocess.check_output(cmd, text=True, stderr=subprocess.DEVNULL).strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def get_active_window(self):
|
||||
win_id_line = self._run("xprop", "-root", "_NET_ACTIVE_WINDOW")
|
||||
match = re.search(r"0x[0-9a-fA-F]+", win_id_line)
|
||||
if not match:
|
||||
return None
|
||||
win_id = match.group(0)
|
||||
title = self._run("xprop", "-id", win_id, "_NET_WM_NAME")
|
||||
if not title:
|
||||
title = self._run("xprop", "-id", win_id, "WM_NAME")
|
||||
klass = self._run("xprop", "-id", win_id, "WM_CLASS")
|
||||
|
||||
title_match = re.search(r'=\s*"(?P<value>.*)"\s*$', title)
|
||||
if title_match:
|
||||
title = title_match.group("value")
|
||||
else:
|
||||
title = title.split("=", 1)[-1].strip().strip('"')
|
||||
|
||||
class_values = re.findall(r'"([^"]+)"', klass)
|
||||
window_class = " ".join(class_values) if class_values else klass.split("=", 1)[-1].strip()
|
||||
|
||||
if not title and not window_class:
|
||||
return None
|
||||
|
||||
return {
|
||||
"windowId": win_id,
|
||||
"title": title,
|
||||
"windowClass": window_class
|
||||
}
|
||||
|
||||
def ensure_bucket(self, bucket_id, bucket_type):
|
||||
if bucket_id in self.ensured:
|
||||
return True
|
||||
payload = {"client": "aw-linux-web-category", "type": bucket_type, "hostname": self.host}
|
||||
req = urllib.request.Request(
|
||||
f"{self.server}/buckets/{bucket_id}",
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10):
|
||||
self.ensured.add(bucket_id)
|
||||
return True
|
||||
except urllib.error.HTTPError as err:
|
||||
if err.code in (304, 409):
|
||||
self.ensured.add(bucket_id)
|
||||
return True
|
||||
return False
|
||||
except urllib.error.URLError:
|
||||
return False
|
||||
|
||||
def heartbeat(self, bucket_id, payload, bucket_type, pulse=60):
|
||||
if not self.ensure_bucket(bucket_id, bucket_type):
|
||||
return False
|
||||
req = urllib.request.Request(
|
||||
f"{self.server}/buckets/{bucket_id}/heartbeat?pulsetime={pulse}",
|
||||
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10):
|
||||
return True
|
||||
except urllib.error.URLError:
|
||||
return False
|
||||
|
||||
def match_rule(self, window):
|
||||
title = window.get("title", "")
|
||||
window_class = window.get("windowClass", "")
|
||||
for rule in self.rules:
|
||||
if rule["windowClassRegex"].search(window_class) and rule["titleRegex"].search(title):
|
||||
return rule
|
||||
return None
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
window = self.get_active_window()
|
||||
if window:
|
||||
raw_key = f"{window.get('windowClass','')}|{window.get('title','')}"
|
||||
if raw_key != self.last_raw_key:
|
||||
raw_event = {
|
||||
"timestamp": iso_now(),
|
||||
"duration": 0,
|
||||
"data": {
|
||||
"source": "x11_active_window",
|
||||
"username": self.user,
|
||||
"host": self.host,
|
||||
"windowId": window.get("windowId", ""),
|
||||
"windowClass": window.get("windowClass", ""),
|
||||
"title": window.get("title", "")
|
||||
}
|
||||
}
|
||||
self.heartbeat(self.raw_bucket, raw_event, "aw.linux.web.context", pulse=20)
|
||||
self.last_raw_key = raw_key
|
||||
|
||||
rule = self.match_rule(window)
|
||||
if rule:
|
||||
category_key = rule["id"] + "|" + window.get("title", "")
|
||||
if category_key != self.last_category_key:
|
||||
event = {
|
||||
"timestamp": iso_now(),
|
||||
"duration": 0,
|
||||
"data": {
|
||||
"source": "linux_window_title_rule",
|
||||
"username": self.user,
|
||||
"host": self.host,
|
||||
"windowClass": window.get("windowClass", ""),
|
||||
"title": window.get("title", ""),
|
||||
"categoryGroup": rule["categoryGroup"],
|
||||
"category": rule["category"],
|
||||
"categoryRule": rule["id"],
|
||||
"service": rule["service"],
|
||||
"interface": rule["interface"],
|
||||
"port": rule["port"],
|
||||
"rootDomain": rule["rootDomain"]
|
||||
}
|
||||
}
|
||||
self.heartbeat(self.category_bucket, event, "aw.web.category", pulse=30)
|
||||
self.last_category_key = category_key
|
||||
|
||||
time.sleep(self.poll_interval)
|
||||
|
||||
|
||||
def main():
|
||||
config_path = pathlib.Path(os.environ.get("AW_LINUX_WEB_CATEGORY_CONFIG", "~/.local/opt/aw-linux-web-category/config.json")).expanduser()
|
||||
cfg = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
Collector(cfg).run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
EOF
|
||||
chmod 0755 "${INSTALL_ROOT}/collector.py"
|
||||
|
||||
write_file "${BIN_DIR}/aw-linux-web-category-start" <<EOF
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
export AW_LINUX_WEB_CATEGORY_CONFIG="${INSTALL_ROOT}/config.json"
|
||||
mkdir -p "${LOG_DIR}"
|
||||
if pgrep -u "$(id -u)" -f "aw-linux-web-category/collector.py" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
nohup "${INSTALL_ROOT}/collector.py" >> "${LOG_DIR}/collector.log" 2>&1 &
|
||||
EOF
|
||||
chmod 0755 "${BIN_DIR}/aw-linux-web-category-start"
|
||||
|
||||
write_file "${BIN_DIR}/aw-linux-web-category-stop" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
pkill -u "$(id -u)" -f "aw-linux-web-category/collector.py" || true
|
||||
EOF
|
||||
chmod 0755 "${BIN_DIR}/aw-linux-web-category-stop"
|
||||
|
||||
write_file "${BIN_DIR}/aw-linux-web-category-status" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
pgrep -a -u "$(id -u)" -f "aw-linux-web-category/collector.py" || true
|
||||
EOF
|
||||
chmod 0755 "${BIN_DIR}/aw-linux-web-category-status"
|
||||
|
||||
write_file "${SYSTEMD_USER_DIR}/aw-linux-web-category.service" <<EOF
|
||||
[Unit]
|
||||
Description=AW Linux web-category logger (user space)
|
||||
After=default.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment=AW_LINUX_WEB_CATEGORY_CONFIG=${INSTALL_ROOT}/config.json
|
||||
ExecStart=${INSTALL_ROOT}/collector.py
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
|
||||
started_with_systemd="0"
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||||
systemctl --user enable --now aw-linux-web-category.service >/dev/null 2>&1 || true
|
||||
if systemctl --user is-active --quiet aw-linux-web-category.service >/dev/null 2>&1; then
|
||||
started_with_systemd="1"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${started_with_systemd}" != "1" ]; then
|
||||
"${BIN_DIR}/aw-linux-web-category-start"
|
||||
fi
|
||||
|
||||
echo "Installed AW Linux web-category logger"
|
||||
echo "Server target: ${SERVER_HOST}:${SERVER_PORT}"
|
||||
echo "Raw bucket: aw-linux-web-context_$(hostname -s)"
|
||||
echo "Category bucket: aw-detmir-web-category_$(hostname -s)"
|
||||
echo "Rules: ${CONFIG_DIR}/rules.json"
|
||||
echo "Log file: ${LOG_DIR}/collector.log"
|
||||
Executable
+349
@@ -0,0 +1,349 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
SERVER_HOST="10.10.10.13"
|
||||
SERVER_PORT="5600"
|
||||
POLL_INTERVAL="5"
|
||||
INSTALL_ROOT="/opt/aw-pve-webadmin-logger"
|
||||
STATE_DIR="/var/lib/aw-pve-webadmin-logger"
|
||||
LOG_DIR="/var/log/aw-pve-webadmin-logger"
|
||||
CONFIG_PATH="/etc/aw-pve-webadmin-logger/config.json"
|
||||
SERVICE_PATH="/etc/systemd/system/aw-pve-webadmin-logger.service"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: install_aw_pve_webadmin_logger.sh [options]
|
||||
|
||||
Options:
|
||||
--server-host HOST AW server host (default: 10.10.10.13)
|
||||
--server-port PORT AW server port (default: 5600)
|
||||
--poll-interval SEC Poll interval in seconds (default: 5)
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--server-host)
|
||||
SERVER_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--server-port)
|
||||
SERVER_PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--poll-interval)
|
||||
POLL_INTERVAL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$INSTALL_ROOT" "$STATE_DIR" "$LOG_DIR" "$(dirname "$CONFIG_PATH")"
|
||||
|
||||
HOST_SHORT="$(hostname -s)"
|
||||
|
||||
cat > "$CONFIG_PATH" <<EOF
|
||||
{
|
||||
"server_host": "${SERVER_HOST}",
|
||||
"server_port": ${SERVER_PORT},
|
||||
"poll_interval_seconds": ${POLL_INTERVAL},
|
||||
"host": "${HOST_SHORT}",
|
||||
"state_dir": "${STATE_DIR}",
|
||||
"access_log": "/var/log/pveproxy/access.log",
|
||||
"tasks_index": "/var/log/pve/tasks/index",
|
||||
"web_bucket": "aw-pve-webadmin-events_${HOST_SHORT}",
|
||||
"task_bucket": "aw-pve-task-events_${HOST_SHORT}",
|
||||
"category_bucket": "aw-detmir-web-category_${HOST_SHORT}"
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > "${INSTALL_ROOT}/collector.py" <<'PY'
|
||||
#!/usr/bin/env python3
|
||||
import datetime as dt
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
ACCESS_RE = re.compile(
|
||||
r'^(?P<ip>\S+)\s+-\s+(?P<user>\S+)\s+\[(?P<ts>[^\]]+)\]\s+"(?P<method>\S+)\s+(?P<path>\S+)\s+(?P<proto>[^"]+)"\s+(?P<status>\d{3})\s+(?P<size>\S+)'
|
||||
)
|
||||
NOISE_GET_PATHS = [
|
||||
re.compile(r"^/api2/json/version$"),
|
||||
re.compile(r"^/api2/json/cluster/resources$"),
|
||||
re.compile(r"^/api2/json/cluster/tasks$"),
|
||||
re.compile(r"^/api2/json/nodes/[^/]+/(qemu|lxc)/\d+/status/current$"),
|
||||
re.compile(r"^/api2/json/nodes/[^/]+/(qemu|lxc)/\d+/interfaces$"),
|
||||
re.compile(r"^/api2/json/nodes/[^/]+/(qemu|lxc)/\d+/rrddata(\?.*)?$"),
|
||||
]
|
||||
|
||||
TASK_RE = re.compile(
|
||||
r'^UPID:(?P<node>[^:]+):(?P<pid>[^:]+):(?P<pstart>[^:]+):(?P<start>[^:]+):(?P<action>[^:]*):(?P<target>[^:]*):(?P<user>[^:]*):\s*(?P<msg>.*)$'
|
||||
)
|
||||
|
||||
|
||||
def iso_now():
|
||||
return dt.datetime.now(tz=dt.timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
|
||||
|
||||
def parse_access_ts(value: str) -> str:
|
||||
try:
|
||||
parsed = dt.datetime.strptime(value, "%d/%b/%Y:%H:%M:%S %z")
|
||||
return parsed.astimezone(dt.timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||
except Exception:
|
||||
return iso_now()
|
||||
|
||||
|
||||
class TailState:
|
||||
def __init__(self, path: pathlib.Path):
|
||||
self.path = path
|
||||
self.data = {"inode": None, "offset": 0}
|
||||
if path.exists():
|
||||
try:
|
||||
self.data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
self.data = {"inode": None, "offset": 0}
|
||||
|
||||
def save(self):
|
||||
self.path.write_text(json.dumps(self.data, ensure_ascii=True), encoding="utf-8")
|
||||
|
||||
|
||||
class Collector:
|
||||
def __init__(self, cfg):
|
||||
self.cfg = cfg
|
||||
self.server = f"http://{cfg['server_host']}:{cfg['server_port']}/api/0"
|
||||
self.host = cfg.get("host") or socket.gethostname().split(".")[0]
|
||||
self.poll = max(1, int(cfg.get("poll_interval_seconds", 5)))
|
||||
self.web_bucket = cfg["web_bucket"]
|
||||
self.task_bucket = cfg["task_bucket"]
|
||||
self.category_bucket = cfg.get("category_bucket", f"aw-detmir-web-category_{self.host}")
|
||||
self.access_log = pathlib.Path(cfg["access_log"])
|
||||
self.tasks_index = pathlib.Path(cfg["tasks_index"])
|
||||
self.state_dir = pathlib.Path(cfg["state_dir"])
|
||||
self.state_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.access_state = TailState(self.state_dir / "access_state.json")
|
||||
self.tasks_state = TailState(self.state_dir / "tasks_state.json")
|
||||
self.ensured = set()
|
||||
self.recent = {}
|
||||
|
||||
def ensure_bucket(self, bucket_id: str, bucket_type: str):
|
||||
if bucket_id in self.ensured:
|
||||
return True
|
||||
payload = {"client": "aw-pve-webadmin-logger", "type": bucket_type, "hostname": self.host}
|
||||
req = urllib.request.Request(
|
||||
f"{self.server}/buckets/{bucket_id}",
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10):
|
||||
self.ensured.add(bucket_id)
|
||||
return True
|
||||
except urllib.error.HTTPError as err:
|
||||
if err.code in (304, 409):
|
||||
self.ensured.add(bucket_id)
|
||||
return True
|
||||
return False
|
||||
except urllib.error.URLError:
|
||||
return False
|
||||
|
||||
def heartbeat(self, bucket_id: str, payload: dict, bucket_type: str):
|
||||
if not self.ensure_bucket(bucket_id, bucket_type):
|
||||
return False
|
||||
req = urllib.request.Request(
|
||||
f"{self.server}/buckets/{bucket_id}/heartbeat?pulsetime=60",
|
||||
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10):
|
||||
return True
|
||||
except urllib.error.URLError:
|
||||
return False
|
||||
|
||||
def read_new_lines(self, src: pathlib.Path, st: TailState):
|
||||
if not src.exists():
|
||||
return []
|
||||
fs = src.stat()
|
||||
inode = int(fs.st_ino)
|
||||
size = int(fs.st_size)
|
||||
prev_inode = st.data.get("inode")
|
||||
prev_off = int(st.data.get("offset", 0))
|
||||
if prev_inode != inode or prev_off > size:
|
||||
prev_off = 0
|
||||
with src.open("r", encoding="utf-8", errors="replace") as f:
|
||||
f.seek(prev_off)
|
||||
lines = f.readlines()
|
||||
st.data = {"inode": inode, "offset": f.tell()}
|
||||
st.save()
|
||||
return [ln.rstrip("\n") for ln in lines if ln.strip()]
|
||||
|
||||
def process_access(self):
|
||||
for line in self.read_new_lines(self.access_log, self.access_state):
|
||||
m = ACCESS_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
user = m.group("user")
|
||||
status = int(m.group("status"))
|
||||
method = m.group("method")
|
||||
path = m.group("path")
|
||||
if path == "/api2/json/version":
|
||||
continue
|
||||
if user == "-" and status < 400:
|
||||
continue
|
||||
# Proxmox UI does high-frequency read polling. Keep real actions and auth failures.
|
||||
if method == "GET" and status == 200 and any(rx.match(path) for rx in NOISE_GET_PATHS):
|
||||
continue
|
||||
event_kind = "auth_failed" if status in (401, 403) else "request"
|
||||
dedup_key = f"{event_kind}|{user}|{m.group('ip')}|{method}|{path}|{status}"
|
||||
now = time.time()
|
||||
if now - float(self.recent.get(dedup_key, 0)) < 30:
|
||||
continue
|
||||
self.recent[dedup_key] = now
|
||||
event = {
|
||||
"timestamp": parse_access_ts(m.group("ts")),
|
||||
"duration": 0,
|
||||
"data": {
|
||||
"source": "pveproxy_access",
|
||||
"event_kind": event_kind,
|
||||
"host": self.host,
|
||||
"user": user,
|
||||
"remote_ip": m.group("ip"),
|
||||
"method": method,
|
||||
"path": path,
|
||||
"status": status,
|
||||
"protocol": m.group("proto"),
|
||||
"raw": line,
|
||||
},
|
||||
}
|
||||
self.heartbeat(self.web_bucket, event, "app.pve.webadmin.event")
|
||||
category_event = self.classify_access_event(event)
|
||||
if category_event:
|
||||
self.heartbeat(self.category_bucket, category_event, "web.tab.current")
|
||||
|
||||
def process_tasks(self):
|
||||
for line in self.read_new_lines(self.tasks_index, self.tasks_state):
|
||||
m = TASK_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
msg = m.group("msg")
|
||||
event = {
|
||||
"timestamp": iso_now(),
|
||||
"duration": 0,
|
||||
"data": {
|
||||
"source": "pve_tasks_index",
|
||||
"host": self.host,
|
||||
"node": m.group("node"),
|
||||
"upid_pid": m.group("pid"),
|
||||
"action": m.group("action"),
|
||||
"target": m.group("target"),
|
||||
"user": m.group("user"),
|
||||
"message": msg,
|
||||
"result": "ok" if " OK" in msg else ("error" if "error" in msg.lower() else "info"),
|
||||
"raw": line,
|
||||
},
|
||||
}
|
||||
self.heartbeat(self.task_bucket, event, "app.pve.task.event")
|
||||
|
||||
def classify_access_event(self, source_event: dict):
|
||||
data = source_event.get("data", {})
|
||||
path = str(data.get("path") or "")
|
||||
user = str(data.get("user") or "")
|
||||
remote_ip = str(data.get("remote_ip") or "")
|
||||
method = str(data.get("method") or "")
|
||||
status = int(data.get("status") or 0)
|
||||
# Keep only meaningful API/UI actions for worktime.
|
||||
if not path.startswith("/api2/"):
|
||||
return None
|
||||
if method == "GET" and ("/cluster/resources" in path or "/status/current" in path):
|
||||
return None
|
||||
category = "Администрирование"
|
||||
category_group = "work"
|
||||
if status in (401, 403):
|
||||
category = "Безопасность"
|
||||
title = f"Proxmox API {method} {path}"
|
||||
return {
|
||||
"timestamp": source_event.get("timestamp") or iso_now(),
|
||||
"duration": 0,
|
||||
"data": {
|
||||
"source": "pve-webadmin-bridge",
|
||||
"service": "proxmox",
|
||||
"host": self.host,
|
||||
"app": "proxmox-webui",
|
||||
"title": title[:512],
|
||||
"url": f"https://{self.host}:8006{path}",
|
||||
"domain": self.host,
|
||||
"rootDomain": "proxmox-webui",
|
||||
"categoryGroup": category_group,
|
||||
"category": category,
|
||||
"user": user,
|
||||
"remote_ip": remote_ip,
|
||||
"method": method,
|
||||
"status": status,
|
||||
},
|
||||
}
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
self.process_access()
|
||||
self.process_tasks()
|
||||
time.sleep(self.poll)
|
||||
|
||||
|
||||
def main():
|
||||
cfg = json.loads(pathlib.Path("/etc/aw-pve-webadmin-logger/config.json").read_text(encoding="utf-8"))
|
||||
Collector(cfg).run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
PY
|
||||
|
||||
chmod 0755 "${INSTALL_ROOT}/collector.py"
|
||||
|
||||
cat > "$SERVICE_PATH" <<EOF
|
||||
[Unit]
|
||||
Description=AW PVE web-admin activity logger
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=${INSTALL_ROOT}/collector.py
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
WorkingDirectory=${INSTALL_ROOT}
|
||||
StandardOutput=append:${LOG_DIR}/collector.log
|
||||
StandardError=append:${LOG_DIR}/collector.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now aw-pve-webadmin-logger.service
|
||||
systemctl --no-pager --full status aw-pve-webadmin-logger.service || true
|
||||
|
||||
echo "Installed aw-pve-webadmin-logger"
|
||||
echo "Config: ${CONFIG_PATH}"
|
||||
echo "Buckets: aw-pve-webadmin-events_${HOST_SHORT}, aw-pve-task-events_${HOST_SHORT}, aw-detmir-web-category_${HOST_SHORT}"
|
||||
@@ -243,6 +243,8 @@ function Copy-ActivityWatchCollectorAssets {
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$EndpointCollectorScriptSource,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SessionCollectorScriptSource,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ExampleRulesSource,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ExamplePolicySource,
|
||||
@@ -256,6 +258,7 @@ function Copy-ActivityWatchCollectorAssets {
|
||||
|
||||
$collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1'
|
||||
$endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1'
|
||||
$sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1'
|
||||
$exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json'
|
||||
$rulesTarget = Join-Path $StateRoot 'web-category-rules.json'
|
||||
$examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json'
|
||||
@@ -263,6 +266,7 @@ function Copy-ActivityWatchCollectorAssets {
|
||||
|
||||
Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force
|
||||
Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force
|
||||
Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force
|
||||
Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force
|
||||
Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force
|
||||
|
||||
@@ -282,6 +286,7 @@ function Copy-ActivityWatchCollectorAssets {
|
||||
return [pscustomobject]@{
|
||||
CollectorScript = $collectorTarget
|
||||
EndpointCollectorScript = $endpointCollectorTarget
|
||||
SessionCollectorScript = $sessionCollectorTarget
|
||||
ExampleRules = $exampleRulesTarget
|
||||
ActiveRules = $rulesTarget
|
||||
ExamplePolicy = $examplePolicyTarget
|
||||
@@ -308,6 +313,8 @@ function New-ActivityWatchDeploymentConfig {
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$EndpointCollectorScript,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SessionCollectorScript,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RulesPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PolicyPath,
|
||||
@@ -349,6 +356,7 @@ function New-ActivityWatchDeploymentConfig {
|
||||
logsRoot = $LogsRoot
|
||||
collectorScript = $CollectorScript
|
||||
endpointCollectorScript = $EndpointCollectorScript
|
||||
sessionCollectorScript = $SessionCollectorScript
|
||||
rulesPath = $RulesPath
|
||||
policyPath = $PolicyPath
|
||||
launchScript = $LaunchScriptPath
|
||||
@@ -631,6 +639,7 @@ function Start-CollectorScriptIfNeeded {
|
||||
`$script:KnownBuckets = @{}
|
||||
`$collectorScript = [string]`$config.paths.collectorScript
|
||||
`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { '' }
|
||||
`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { '' }
|
||||
`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe'
|
||||
`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe'
|
||||
`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port)
|
||||
@@ -661,6 +670,7 @@ catch {
|
||||
}
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
"@
|
||||
|
||||
Set-Content -LiteralPath $Path -Value $content -Encoding UTF8
|
||||
|
||||
@@ -44,6 +44,7 @@ $launchScriptPath = Join-Path $StateRoot 'launch-watchers.ps1'
|
||||
$recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1'
|
||||
$collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1'
|
||||
$endpointCollectorSource = Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1'
|
||||
$sessionCollectorSource = Join-Path $PSScriptRoot 'worktime-session-collector.ps1'
|
||||
$exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json'
|
||||
$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json'
|
||||
|
||||
@@ -57,6 +58,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null
|
||||
$assetResult = Copy-ActivityWatchCollectorAssets `
|
||||
-CollectorScriptSource $collectorSource `
|
||||
-EndpointCollectorScriptSource $endpointCollectorSource `
|
||||
-SessionCollectorScriptSource $sessionCollectorSource `
|
||||
-ExampleRulesSource $exampleRulesSource `
|
||||
-ExamplePolicySource $examplePolicySource `
|
||||
-StateRoot $StateRoot `
|
||||
@@ -76,6 +78,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-LogsRoot $logsRoot `
|
||||
-CollectorScript $assetResult.CollectorScript `
|
||||
-EndpointCollectorScript $assetResult.EndpointCollectorScript `
|
||||
-SessionCollectorScript $assetResult.SessionCollectorScript `
|
||||
-RulesPath $assetResult.ActiveRules `
|
||||
-PolicyPath $assetResult.ActivePolicy `
|
||||
-PollSeconds $PollSeconds `
|
||||
|
||||
@@ -42,6 +42,7 @@ $launchScriptPath = Join-Path $StateRoot 'launch-watchers.ps1'
|
||||
$recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1'
|
||||
$collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1'
|
||||
$endpointCollectorSource = Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1'
|
||||
$sessionCollectorSource = Join-Path $PSScriptRoot 'worktime-session-collector.ps1'
|
||||
$exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json'
|
||||
$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json'
|
||||
|
||||
@@ -55,6 +56,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null
|
||||
$assetResult = Copy-ActivityWatchCollectorAssets `
|
||||
-CollectorScriptSource $collectorSource `
|
||||
-EndpointCollectorScriptSource $endpointCollectorSource `
|
||||
-SessionCollectorScriptSource $sessionCollectorSource `
|
||||
-ExampleRulesSource $exampleRulesSource `
|
||||
-ExamplePolicySource $examplePolicySource `
|
||||
-StateRoot $StateRoot `
|
||||
@@ -74,6 +76,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-LogsRoot $logsRoot `
|
||||
-CollectorScript $assetResult.CollectorScript `
|
||||
-EndpointCollectorScript $assetResult.EndpointCollectorScript `
|
||||
-SessionCollectorScript $assetResult.SessionCollectorScript `
|
||||
-RulesPath $assetResult.ActiveRules `
|
||||
-PolicyPath $assetResult.ActivePolicy `
|
||||
-PollSeconds $PollSeconds `
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#define MyAppName "AWatch-rus InstallKit"
|
||||
#define MyAppVersion "1.0.0"
|
||||
#define MyAppPublisher "AWatch-rus"
|
||||
|
||||
[Setup]
|
||||
AppId={{6D6A1F74-0F4F-4A57-B5E3-1C2C2F56C0E9}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
DefaultDirName={autopf}\AWatch-rus
|
||||
DefaultGroupName=AWatch-rus
|
||||
OutputDir=.
|
||||
OutputBaseFilename=AWatch-rus-InstallKit-offline
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
PrivilegesRequired=admin
|
||||
|
||||
[Languages]
|
||||
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
|
||||
|
||||
[Files]
|
||||
Source: "..\..\ActivityWatch.Windows.Common.psd1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\ActivityWatch.Windows.Common.psm1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\deploy-single-user.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\deploy-domain-users.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\deploy-ensemble.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\hardening-recovery.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\validate-deployment.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\worktime-session-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\browser-domains-native-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\dlp-endpoint-signals-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\web-category-rules.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\dlp-policy.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "payload\activitywatch-v0.13.2-windows-x86_64.zip"; DestDir: "{app}\payload"; Flags: ignoreversion
|
||||
Source: "innosetup-rdp-package-filelist.md"; DestDir: "{app}\windows\installkit\innosetup"; Flags: ignoreversion
|
||||
|
||||
[Run]
|
||||
Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File \"{app}\windows\deploy-ensemble.ps1\" -PackageZipPath \"{app}\payload\activitywatch-v0.13.2-windows-x86_64.zip\""; Flags: runhidden
|
||||
Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File \"{app}\windows\validate-deployment.ps1\""; Flags: runhidden
|
||||
@@ -0,0 +1,90 @@
|
||||
# Inno Setup: файл-лист для Windows RDP deployment
|
||||
|
||||
Дата актуализации: 2026-05-02 (UTC).
|
||||
|
||||
## Что это за документ
|
||||
|
||||
Этот файл — **чеклист упаковки** для Inno Setup.
|
||||
|
||||
- Он описывает, **что класть** в инсталлятор.
|
||||
- Он описывает, **что не класть** (генерируется уже на целевом хосте).
|
||||
- Он **не меняет** текущие deploy-скрипты и логику проекта.
|
||||
|
||||
## Важное уточнение по `phase2`
|
||||
|
||||
Чтобы исключить путаницу:
|
||||
|
||||
1. В каталоге `windows/` нет файлов с именами `phase2-*`.
|
||||
2. `phase2` в проекте — это обозначение этапа/набора телеметрии (DLP + endpoint signals).
|
||||
3. Файлы вида `phase2-*.deployment-config.json` — это **примерные конфиги install-kit**, они лежат в `install-kit-*/server-configs-*`.
|
||||
|
||||
## 1) Обязательные файлы для Inno Setup пакета
|
||||
|
||||
### 1.1 PowerShell-модуль
|
||||
- `windows/ActivityWatch.Windows.Common.psd1`
|
||||
- `windows/ActivityWatch.Windows.Common.psm1`
|
||||
|
||||
### 1.2 Скрипты деплоя и сопровождения
|
||||
- `windows/deploy-single-user.ps1`
|
||||
- `windows/deploy-domain-users.ps1`
|
||||
- `windows/deploy-ensemble.ps1`
|
||||
- `windows/hardening-recovery.ps1`
|
||||
- `windows/validate-deployment.ps1`
|
||||
|
||||
### 1.3 Коллекторы
|
||||
- `windows/worktime-session-collector.ps1` (RDP/session presence)
|
||||
- `windows/browser-domains-native-collector.ps1`
|
||||
- `windows/dlp-endpoint-signals-collector.ps1`
|
||||
|
||||
### 1.4 Шаблоны конфигурации
|
||||
- `windows/web-category-rules.example.json`
|
||||
- `windows/dlp-policy.example.json`
|
||||
|
||||
## 2) Бинарный payload ActivityWatch
|
||||
|
||||
Для закрытой среды используется **offline-режим по умолчанию**:
|
||||
|
||||
- В комплект Inno Setup сразу включается `payload\activitywatch-v0.13.2-windows-x86_64.zip`.
|
||||
- Deploy запускается с параметром `-PackageZipPath` на локальный ZIP из `{app}\payload`.
|
||||
- Online-загрузка из GitHub Releases в закрытой среде не требуется.
|
||||
|
||||
## 3) Что НЕ включать в installer как статические файлы
|
||||
|
||||
Эти файлы/папки появляются на целевом Windows-хосте во время/после деплоя:
|
||||
|
||||
- `C:\ProgramData\ActivityWatch\deployment-config.json`
|
||||
- `C:\ProgramData\ActivityWatch\web-category-rules.json`
|
||||
- `C:\ProgramData\ActivityWatch\dlp-policy.json`
|
||||
- `C:\ProgramData\ActivityWatch\logs\*`
|
||||
- `%LOCALAPPDATA%\ActivityWatch-Phase2\incident-artifacts\*`
|
||||
|
||||
## 4) Опционально приложить в операторский install-kit
|
||||
|
||||
- `docs/windows/deployment.md`
|
||||
- `docs/windows/validation.md`
|
||||
- `docs/windows/troubleshooting.md`
|
||||
- `docs/windows/ensemble.md`
|
||||
|
||||
## 5) Рекомендуемая структура внутри пакета
|
||||
|
||||
- `windows\ActivityWatch.Windows.Common.psd1`
|
||||
- `windows\ActivityWatch.Windows.Common.psm1`
|
||||
- `windows\deploy-single-user.ps1`
|
||||
- `windows\deploy-domain-users.ps1`
|
||||
- `windows\deploy-ensemble.ps1`
|
||||
- `windows\hardening-recovery.ps1`
|
||||
- `windows\validate-deployment.ps1`
|
||||
- `windows\worktime-session-collector.ps1`
|
||||
- `windows\browser-domains-native-collector.ps1`
|
||||
- `windows\dlp-endpoint-signals-collector.ps1`
|
||||
- `windows\web-category-rules.example.json`
|
||||
- `windows\dlp-policy.example.json`
|
||||
- `payload\activitywatch-v0.13.2-windows-x86_64.zip` (только для offline-режима)
|
||||
|
||||
## 6) Контроль перед сборкой .iss
|
||||
|
||||
1. Все файлы из раздела 1 присутствуют.
|
||||
2. ZIP `activitywatch-v0.13.2-windows-x86_64.zip` лежит в `windows/installkit/innosetup/payload/`.
|
||||
3. В .iss добавлен `Source: "payload\activitywatch-v0.13.2-windows-x86_64.zip"`.
|
||||
4. Deploy в .iss запускается с `-PackageZipPath` на локальный ZIP.
|
||||
5. После установки запускается `validate-deployment.ps1` с сохранением JSON-отчёта.
|
||||
@@ -14,6 +14,7 @@ $installRoot = [string]$config.paths.installRoot
|
||||
$stateRoot = [string]$config.paths.stateRoot
|
||||
$collectorScript = [string]$config.paths.collectorScript
|
||||
$endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' }
|
||||
$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' }
|
||||
$rulesPath = [string]$config.paths.rulesPath
|
||||
$policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' }
|
||||
$launchScript = [string]$config.paths.launchScript
|
||||
@@ -24,6 +25,7 @@ $requiredFiles = @(
|
||||
(Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'),
|
||||
$collectorScript,
|
||||
$endpointCollectorScript,
|
||||
$sessionCollectorScript,
|
||||
$rulesPath,
|
||||
$policyPath,
|
||||
$launchScript,
|
||||
@@ -37,6 +39,12 @@ $missingFiles = @(
|
||||
|
||||
$processNames = @('aw-watcher-afk', 'aw-watcher-window')
|
||||
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
|
||||
$sessionCollectorProcesses = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and
|
||||
$_.CommandLine -match [Regex]::Escape($sessionCollectorScript)
|
||||
} |
|
||||
Select-Object Name, ProcessId, SessionId, CommandLine
|
||||
|
||||
$taskNames = @()
|
||||
if ($config.userTasks) {
|
||||
@@ -81,7 +89,11 @@ $result = [ordered]@{
|
||||
}
|
||||
processes = [ordered]@{
|
||||
list = @($runningProcesses)
|
||||
ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2)
|
||||
sessionCollectors = @($sessionCollectorProcesses)
|
||||
ok = [bool](
|
||||
(($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2) -and
|
||||
($sessionCollectorProcesses.Count -ge 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
|
||||
[string]$Hostname,
|
||||
[int]$PollSeconds = 30
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-Config {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "Config not found: $Path"
|
||||
}
|
||||
|
||||
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$Json
|
||||
)
|
||||
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||
}
|
||||
|
||||
function Ensure-Bucket {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||
)
|
||||
|
||||
try {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
return
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
$body = @{
|
||||
client = 'aw-worktime-session-collector'
|
||||
type = 'aw.worktime.session'
|
||||
hostname = $HostnameValue
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||
}
|
||||
|
||||
function Get-SessionRecords {
|
||||
$records = @()
|
||||
|
||||
try {
|
||||
$lines = quser 2>$null
|
||||
if (-not $lines) {
|
||||
return @()
|
||||
}
|
||||
|
||||
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||
if (-not $clean) {
|
||||
continue
|
||||
}
|
||||
|
||||
$parts = $clean -split '\s+'
|
||||
if ($parts.Count -lt 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
$sessionId = 0
|
||||
if ($parts[2] -match '^\d+$') {
|
||||
$sessionId = [int]$parts[2]
|
||||
}
|
||||
|
||||
$records += [pscustomobject]@{
|
||||
username = $parts[0]
|
||||
sessionName = $parts[1]
|
||||
sessionId = $sessionId
|
||||
state = $parts[3]
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
return $records
|
||||
}
|
||||
|
||||
$cfg = Get-Config -Path $ConfigPath
|
||||
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||
$pulse = 120
|
||||
$sleepSec = if ($PollSeconds -gt 0) {
|
||||
$PollSeconds
|
||||
}
|
||||
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||
[int]$cfg.collector.pollSeconds
|
||||
}
|
||||
else {
|
||||
30
|
||||
}
|
||||
|
||||
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||
|
||||
while ($true) {
|
||||
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||
$records = Get-SessionRecords
|
||||
if (-not $records -or $records.Count -eq 0) {
|
||||
$records = @([pscustomobject]@{
|
||||
username = $env:USERNAME
|
||||
sessionName = ''
|
||||
sessionId = (Get-Process -Id $PID).SessionId
|
||||
state = 'Unknown'
|
||||
})
|
||||
}
|
||||
|
||||
foreach ($rec in $records) {
|
||||
$payload = @{
|
||||
timestamp = $now
|
||||
duration = 0
|
||||
data = @{
|
||||
username = [string]$rec.username
|
||||
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||
sessionId = [int]$rec.sessionId
|
||||
sessionName = [string]$rec.sessionName
|
||||
state = [string]$rec.state
|
||||
active = ($rec.state -match 'Active')
|
||||
hostname = $hostValue
|
||||
source = 'worktime-session-collector'
|
||||
}
|
||||
} | ConvertTo-Json -Depth 6 -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleepSec
|
||||
}
|
||||
Reference in New Issue
Block a user