Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43f16213bd | ||
|
|
3e0f3576e3 | ||
|
|
9b02d65f76 | ||
|
|
8278d51840 |
@@ -10,16 +10,19 @@
|
|||||||
- `docs/operations.md` — регламент сопровождения, бэкапов, обновлений и rollback.
|
- `docs/operations.md` — регламент сопровождения, бэкапов, обновлений и rollback.
|
||||||
- `docs/windows/ensemble.md` — orchestration-пакет для Windows-деплоя и проверки.
|
- `docs/windows/ensemble.md` — orchestration-пакет для Windows-деплоя и проверки.
|
||||||
- `docs/linux-client.md` — user-space rollout Linux-клиента ActivityWatch на удалённый `AW server`.
|
- `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/console-ssh-logger.md` — логирование только консольных команд и SSH-сессий в AW.
|
||||||
- `docs/dlp-gap-analysis.md` — разрыв до enterprise DLP и roadmap.
|
- `docs/dlp-gap-analysis.md` — разрыв до enterprise DLP и roadmap.
|
||||||
- `proxmox/` — шаблонные скрипты подготовки и наполнения CT на стороне Proxmox.
|
- `proxmox/` — шаблонные скрипты подготовки и наполнения CT на стороне Proxmox.
|
||||||
- `aw-server/` — установочные скрипты, env-шаблон, systemd unit и RU patch для Web UI.
|
- `aw-server/` — установочные скрипты, env-шаблон, systemd unit и RU patch для Web UI.
|
||||||
- `ansible/` — Ansible-ensemble для автоматизированного сервера (Debian/CT).
|
- `ansible/` — Ansible-ensemble для автоматизированного сервера (Debian/CT).
|
||||||
- `pfsense/` — внешний poller для pfSense API и systemd unit под Debian/Ubuntu utility VM.
|
- `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/quality-gate.sh` — локальный preflight-пайплайн проверок.
|
||||||
- `scripts/install_aw_linux_client.sh` — установка Linux bundle + autostart для remote AW server.
|
- `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_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`
|
- `docs/linux-client.md`
|
||||||
- `scripts/install_aw_linux_client.sh`
|
- `scripts/install_aw_linux_client.sh`
|
||||||
|
|
||||||
|
Для полного Linux remote-worker сценария:
|
||||||
|
|
||||||
|
- `docs/linux-remote-worker.md`
|
||||||
|
- `scripts/install_aw_linux_remote_worker.sh`
|
||||||
|
|
||||||
Для режима “только консоль/ssh” без GUI watcher'ов:
|
Для режима “только консоль/ssh” без GUI watcher'ов:
|
||||||
|
|
||||||
- `docs/console-ssh-logger.md`
|
- `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"
|
||||||
@@ -29,6 +29,21 @@
|
|||||||
{ "label": "DLP", "type": "bucket", "bucket_prefix": "aw-dlp-endpoint-signals_" }
|
{ "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",
|
"id": "virtual-infra",
|
||||||
"name": "Virtual servers + Proxmox",
|
"name": "Virtual servers + Proxmox",
|
||||||
|
|||||||
@@ -680,6 +680,19 @@
|
|||||||
{ label: "DLP", type: "bucket", bucket_prefix: "aw-dlp-endpoint-signals_" }
|
{ 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",
|
id: "virtual-infra",
|
||||||
name: "Virtual servers + Proxmox",
|
name: "Virtual servers + Proxmox",
|
||||||
@@ -740,7 +753,15 @@
|
|||||||
const prefixes = [
|
const prefixes = [
|
||||||
"aw-watcher-window_",
|
"aw-watcher-window_",
|
||||||
"aw-watcher-afk_",
|
"aw-watcher-afk_",
|
||||||
|
"aw-console-commands_",
|
||||||
|
"aw-ssh-sessions_",
|
||||||
|
"aw-linux-web-context_",
|
||||||
|
"aw-detmir-web-category_",
|
||||||
"aw-dlp-endpoint-signals_",
|
"aw-dlp-endpoint-signals_",
|
||||||
|
"aw-session-events_",
|
||||||
|
"aw-worktime-sessions_",
|
||||||
|
"aw-pve-webadmin-events_",
|
||||||
|
"aw-pve-task-events_",
|
||||||
"aw-dlp-incidents_",
|
"aw-dlp-incidents_",
|
||||||
"aw-pfsense-health_",
|
"aw-pfsense-health_",
|
||||||
"aw-pfsense-gateways_",
|
"aw-pfsense-gateways_",
|
||||||
@@ -770,7 +791,27 @@
|
|||||||
return result;
|
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) {
|
for (const group of groups) {
|
||||||
const patterns = Array.isArray(group.patterns) ? group.patterns : [];
|
const patterns = Array.isArray(group.patterns) ? group.patterns : [];
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
@@ -813,7 +854,7 @@
|
|||||||
grouped.set("__ungrouped__", []);
|
grouped.set("__ungrouped__", []);
|
||||||
|
|
||||||
Array.from(hostBuckets.keys()).sort().forEach(function (host) {
|
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);
|
grouped.get(groupId).push(host);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -869,7 +910,7 @@
|
|||||||
center.setAttribute("data-aw-ru-host-groups", "1");
|
center.setAttribute("data-aw-ru-host-groups", "1");
|
||||||
center.innerHTML =
|
center.innerHTML =
|
||||||
'<h4>Разделы хостов</h4>' +
|
'<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>';
|
'<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);
|
heading.parentElement.insertBefore(center, heading.nextSibling);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"name": ["Работа", "Документы"],
|
"name": ["Работа", "Документы"],
|
||||||
"rule": {
|
"rule": {
|
||||||
"type": "regex",
|
"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
|
"ignore_case": true
|
||||||
},
|
},
|
||||||
"data": { "color": "#2E7D32" }
|
"data": { "color": "#2E7D32" }
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"name": ["Работа", "Администрирование"],
|
"name": ["Работа", "Администрирование"],
|
||||||
"rule": {
|
"rule": {
|
||||||
"type": "regex",
|
"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
|
"ignore_case": true
|
||||||
},
|
},
|
||||||
"data": { "color": "#6D4C41" }
|
"data": { "color": "#6D4C41" }
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
"name": ["Интернет", "Браузер"],
|
"name": ["Интернет", "Браузер"],
|
||||||
"rule": {
|
"rule": {
|
||||||
"type": "regex",
|
"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
|
"ignore_case": true
|
||||||
},
|
},
|
||||||
"data": { "color": "#00897B" }
|
"data": { "color": "#00897B" }
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
"name": ["ActivityWatch"],
|
"name": ["ActivityWatch"],
|
||||||
"rule": {
|
"rule": {
|
||||||
"type": "regex",
|
"type": "regex",
|
||||||
"regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b",
|
"regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b|\\baw-(watcher|qt)\\b",
|
||||||
"ignore_case": true
|
"ignore_case": true
|
||||||
},
|
},
|
||||||
"data": {}
|
"data": {}
|
||||||
|
|||||||
@@ -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 активной вкладки.
|
||||||
@@ -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 и писал в:
|
Требует, чтобы на клиенте работал browser collector и писал в:
|
||||||
`aw-detmir-web-category_<HOST>` поля `categoryGroup`, `rootDomain`.
|
`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
|
```javascript
|
||||||
web = flood(query_bucket("aw-detmir-web-category_SHARKON2025"));
|
web = flood(query_bucket("aw-detmir-web-category_SHARKON2025"));
|
||||||
not_afk = flood(query_bucket("aw-watcher-afk_SHARKON2025"));
|
not_afk = flood(query_bucket("aw-watcher-afk_SHARKON2025"));
|
||||||
@@ -68,3 +72,24 @@ RETURN = sort_by_duration(events);
|
|||||||
- Если web-поток пустой, рабочее время в браузере корректно посчитать по доменам не получится. Тогда либо:
|
- Если web-поток пустой, рабочее время в браузере корректно посчитать по доменам не получится. Тогда либо:
|
||||||
- чинить/запускать browser collector;
|
- чинить/запускать browser collector;
|
||||||
- либо временно считать браузер в `window` как «Интернет/Браузер» без разделения на work/personal.
|
- либо временно считать браузер в `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.
|
||||||
|
|||||||
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"
|
||||||
@@ -67,7 +67,8 @@ cat > "$CONFIG_PATH" <<EOF
|
|||||||
"access_log": "/var/log/pveproxy/access.log",
|
"access_log": "/var/log/pveproxy/access.log",
|
||||||
"tasks_index": "/var/log/pve/tasks/index",
|
"tasks_index": "/var/log/pve/tasks/index",
|
||||||
"web_bucket": "aw-pve-webadmin-events_${HOST_SHORT}",
|
"web_bucket": "aw-pve-webadmin-events_${HOST_SHORT}",
|
||||||
"task_bucket": "aw-pve-task-events_${HOST_SHORT}"
|
"task_bucket": "aw-pve-task-events_${HOST_SHORT}",
|
||||||
|
"category_bucket": "aw-detmir-web-category_${HOST_SHORT}"
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -133,6 +134,7 @@ class Collector:
|
|||||||
self.poll = max(1, int(cfg.get("poll_interval_seconds", 5)))
|
self.poll = max(1, int(cfg.get("poll_interval_seconds", 5)))
|
||||||
self.web_bucket = cfg["web_bucket"]
|
self.web_bucket = cfg["web_bucket"]
|
||||||
self.task_bucket = cfg["task_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.access_log = pathlib.Path(cfg["access_log"])
|
||||||
self.tasks_index = pathlib.Path(cfg["tasks_index"])
|
self.tasks_index = pathlib.Path(cfg["tasks_index"])
|
||||||
self.state_dir = pathlib.Path(cfg["state_dir"])
|
self.state_dir = pathlib.Path(cfg["state_dir"])
|
||||||
@@ -235,6 +237,9 @@ class Collector:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
self.heartbeat(self.web_bucket, event, "app.pve.webadmin.event")
|
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):
|
def process_tasks(self):
|
||||||
for line in self.read_new_lines(self.tasks_index, self.tasks_state):
|
for line in self.read_new_lines(self.tasks_index, self.tasks_state):
|
||||||
@@ -260,6 +265,44 @@ class Collector:
|
|||||||
}
|
}
|
||||||
self.heartbeat(self.task_bucket, event, "app.pve.task.event")
|
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):
|
def run(self):
|
||||||
while True:
|
while True:
|
||||||
self.process_access()
|
self.process_access()
|
||||||
@@ -303,4 +346,4 @@ systemctl --no-pager --full status aw-pve-webadmin-logger.service || true
|
|||||||
|
|
||||||
echo "Installed aw-pve-webadmin-logger"
|
echo "Installed aw-pve-webadmin-logger"
|
||||||
echo "Config: ${CONFIG_PATH}"
|
echo "Config: ${CONFIG_PATH}"
|
||||||
echo "Buckets: aw-pve-webadmin-events_${HOST_SHORT}, aw-pve-task-events_${HOST_SHORT}"
|
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)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$EndpointCollectorScriptSource,
|
[string]$EndpointCollectorScriptSource,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SessionCollectorScriptSource,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$ExampleRulesSource,
|
[string]$ExampleRulesSource,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$ExamplePolicySource,
|
[string]$ExamplePolicySource,
|
||||||
@@ -256,6 +258,7 @@ function Copy-ActivityWatchCollectorAssets {
|
|||||||
|
|
||||||
$collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1'
|
$collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1'
|
||||||
$endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-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'
|
$exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json'
|
||||||
$rulesTarget = Join-Path $StateRoot 'web-category-rules.json'
|
$rulesTarget = Join-Path $StateRoot 'web-category-rules.json'
|
||||||
$examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.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 $CollectorScriptSource -Destination $collectorTarget -Force
|
||||||
Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -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 $ExampleRulesSource -Destination $exampleRulesTarget -Force
|
||||||
Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force
|
Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force
|
||||||
|
|
||||||
@@ -282,6 +286,7 @@ function Copy-ActivityWatchCollectorAssets {
|
|||||||
return [pscustomobject]@{
|
return [pscustomobject]@{
|
||||||
CollectorScript = $collectorTarget
|
CollectorScript = $collectorTarget
|
||||||
EndpointCollectorScript = $endpointCollectorTarget
|
EndpointCollectorScript = $endpointCollectorTarget
|
||||||
|
SessionCollectorScript = $sessionCollectorTarget
|
||||||
ExampleRules = $exampleRulesTarget
|
ExampleRules = $exampleRulesTarget
|
||||||
ActiveRules = $rulesTarget
|
ActiveRules = $rulesTarget
|
||||||
ExamplePolicy = $examplePolicyTarget
|
ExamplePolicy = $examplePolicyTarget
|
||||||
@@ -308,6 +313,8 @@ function New-ActivityWatchDeploymentConfig {
|
|||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$EndpointCollectorScript,
|
[string]$EndpointCollectorScript,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SessionCollectorScript,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$RulesPath,
|
[string]$RulesPath,
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$PolicyPath,
|
[string]$PolicyPath,
|
||||||
@@ -349,6 +356,7 @@ function New-ActivityWatchDeploymentConfig {
|
|||||||
logsRoot = $LogsRoot
|
logsRoot = $LogsRoot
|
||||||
collectorScript = $CollectorScript
|
collectorScript = $CollectorScript
|
||||||
endpointCollectorScript = $EndpointCollectorScript
|
endpointCollectorScript = $EndpointCollectorScript
|
||||||
|
sessionCollectorScript = $SessionCollectorScript
|
||||||
rulesPath = $RulesPath
|
rulesPath = $RulesPath
|
||||||
policyPath = $PolicyPath
|
policyPath = $PolicyPath
|
||||||
launchScript = $LaunchScriptPath
|
launchScript = $LaunchScriptPath
|
||||||
@@ -631,6 +639,7 @@ function Start-CollectorScriptIfNeeded {
|
|||||||
`$script:KnownBuckets = @{}
|
`$script:KnownBuckets = @{}
|
||||||
`$collectorScript = [string]`$config.paths.collectorScript
|
`$collectorScript = [string]`$config.paths.collectorScript
|
||||||
`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { '' }
|
`$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'
|
`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe'
|
||||||
`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe'
|
`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe'
|
||||||
`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port)
|
`$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 `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||||
Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -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
|
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'
|
$recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1'
|
||||||
$collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1'
|
$collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1'
|
||||||
$endpointCollectorSource = Join-Path $PSScriptRoot 'dlp-endpoint-signals-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'
|
$exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json'
|
||||||
$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json'
|
$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json'
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null
|
|||||||
$assetResult = Copy-ActivityWatchCollectorAssets `
|
$assetResult = Copy-ActivityWatchCollectorAssets `
|
||||||
-CollectorScriptSource $collectorSource `
|
-CollectorScriptSource $collectorSource `
|
||||||
-EndpointCollectorScriptSource $endpointCollectorSource `
|
-EndpointCollectorScriptSource $endpointCollectorSource `
|
||||||
|
-SessionCollectorScriptSource $sessionCollectorSource `
|
||||||
-ExampleRulesSource $exampleRulesSource `
|
-ExampleRulesSource $exampleRulesSource `
|
||||||
-ExamplePolicySource $examplePolicySource `
|
-ExamplePolicySource $examplePolicySource `
|
||||||
-StateRoot $StateRoot `
|
-StateRoot $StateRoot `
|
||||||
@@ -76,6 +78,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
|||||||
-LogsRoot $logsRoot `
|
-LogsRoot $logsRoot `
|
||||||
-CollectorScript $assetResult.CollectorScript `
|
-CollectorScript $assetResult.CollectorScript `
|
||||||
-EndpointCollectorScript $assetResult.EndpointCollectorScript `
|
-EndpointCollectorScript $assetResult.EndpointCollectorScript `
|
||||||
|
-SessionCollectorScript $assetResult.SessionCollectorScript `
|
||||||
-RulesPath $assetResult.ActiveRules `
|
-RulesPath $assetResult.ActiveRules `
|
||||||
-PolicyPath $assetResult.ActivePolicy `
|
-PolicyPath $assetResult.ActivePolicy `
|
||||||
-PollSeconds $PollSeconds `
|
-PollSeconds $PollSeconds `
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ $launchScriptPath = Join-Path $StateRoot 'launch-watchers.ps1'
|
|||||||
$recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1'
|
$recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1'
|
||||||
$collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1'
|
$collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1'
|
||||||
$endpointCollectorSource = Join-Path $PSScriptRoot 'dlp-endpoint-signals-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'
|
$exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json'
|
||||||
$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json'
|
$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json'
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null
|
|||||||
$assetResult = Copy-ActivityWatchCollectorAssets `
|
$assetResult = Copy-ActivityWatchCollectorAssets `
|
||||||
-CollectorScriptSource $collectorSource `
|
-CollectorScriptSource $collectorSource `
|
||||||
-EndpointCollectorScriptSource $endpointCollectorSource `
|
-EndpointCollectorScriptSource $endpointCollectorSource `
|
||||||
|
-SessionCollectorScriptSource $sessionCollectorSource `
|
||||||
-ExampleRulesSource $exampleRulesSource `
|
-ExampleRulesSource $exampleRulesSource `
|
||||||
-ExamplePolicySource $examplePolicySource `
|
-ExamplePolicySource $examplePolicySource `
|
||||||
-StateRoot $StateRoot `
|
-StateRoot $StateRoot `
|
||||||
@@ -74,6 +76,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
|||||||
-LogsRoot $logsRoot `
|
-LogsRoot $logsRoot `
|
||||||
-CollectorScript $assetResult.CollectorScript `
|
-CollectorScript $assetResult.CollectorScript `
|
||||||
-EndpointCollectorScript $assetResult.EndpointCollectorScript `
|
-EndpointCollectorScript $assetResult.EndpointCollectorScript `
|
||||||
|
-SessionCollectorScript $assetResult.SessionCollectorScript `
|
||||||
-RulesPath $assetResult.ActiveRules `
|
-RulesPath $assetResult.ActiveRules `
|
||||||
-PolicyPath $assetResult.ActivePolicy `
|
-PolicyPath $assetResult.ActivePolicy `
|
||||||
-PollSeconds $PollSeconds `
|
-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
|
$stateRoot = [string]$config.paths.stateRoot
|
||||||
$collectorScript = [string]$config.paths.collectorScript
|
$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' }
|
$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
|
$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' }
|
$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
|
$launchScript = [string]$config.paths.launchScript
|
||||||
@@ -24,6 +25,7 @@ $requiredFiles = @(
|
|||||||
(Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'),
|
(Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'),
|
||||||
$collectorScript,
|
$collectorScript,
|
||||||
$endpointCollectorScript,
|
$endpointCollectorScript,
|
||||||
|
$sessionCollectorScript,
|
||||||
$rulesPath,
|
$rulesPath,
|
||||||
$policyPath,
|
$policyPath,
|
||||||
$launchScript,
|
$launchScript,
|
||||||
@@ -37,6 +39,12 @@ $missingFiles = @(
|
|||||||
|
|
||||||
$processNames = @('aw-watcher-afk', 'aw-watcher-window')
|
$processNames = @('aw-watcher-afk', 'aw-watcher-window')
|
||||||
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
|
$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 = @()
|
$taskNames = @()
|
||||||
if ($config.userTasks) {
|
if ($config.userTasks) {
|
||||||
@@ -81,7 +89,11 @@ $result = [ordered]@{
|
|||||||
}
|
}
|
||||||
processes = [ordered]@{
|
processes = [ordered]@{
|
||||||
list = @($runningProcesses)
|
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