Compare commits

...
77 changed files with 3940 additions and 1083 deletions
+4 -2
View File
@@ -18,7 +18,7 @@ jobs:
- name: Run shellcheck
run: |
find . -type f -name "*.sh" -print0 | xargs -0 -r shellcheck
find . -type f -name "*.sh" -print0 | xargs -0 -r shellcheck -e SC1007,SC1090,SC2016
powershell-analyzer:
runs-on: ubuntu-latest
@@ -40,7 +40,9 @@ jobs:
"windows/*.psm1",
"windows/*.psd1"
)
$issues = Invoke-ScriptAnalyzer -Path $targets -Recurse -Severity Error,Warning
$issues = $targets | ForEach-Object {
Invoke-ScriptAnalyzer -Path $_ -Recurse -Severity Error
}
if ($issues) {
$issues | Format-Table -AutoSize
throw "PSScriptAnalyzer detected issues."
+11 -3
View File
@@ -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, Windows/RDP 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.
## Базовый сценарий
@@ -38,9 +41,9 @@
- `ansible/provision_proxmox_ct_and_deploy_aw.yml`
- `ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml` (массово по матрице CT)
Для централизованного phase-2 деплоя Windows-клиентов через WinRM:
Для централизованного деплоя Windows/RDP-клиентов через WinRM:
- `ansible/deploy_aw_windows_phase2.yml`
- `ansible/deploy_aw_windows.yml`
Для внешнего pfSense poller'а:
@@ -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`
+17
View File
@@ -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
View File
@@ -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"
+48 -39
View File
@@ -1,37 +1,33 @@
# Ansible ensemble for AWatch-rus
Эта директория содержит Ansible-ensemble для двух сценариев:
Эта директория содержит Ansible-ensemble для полного развёртывания AWatch-rus:
- деплой на уже существующий Debian host/CT;
- полный цикл с нуля в Proxmox: создание CT + bootstrap + установка ActivityWatch + RU patch.
- централизованный деплой Windows phase-2 collectors по WinRM.
- deployment внешнего pfSense poller'а на Debian/Ubuntu utility VM.
- полный цикл с нуля в Proxmox: создание CT + bootstrap + установка ActivityWatch + RU patch;
- централизованное развёртывание Windows/RDP collector'ов по WinRM;
- развёртывание внешнего pfSense poller'а на Debian/Ubuntu utility VM.
## Файлы
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_server.yml` — основной playbook.
- `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_and_deploy_aw.yml`full-stack playbook для Proxmox.
- `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml` — массовый full-stack playbook (несколько CT).
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_windows_phase2.yml` — WinRM playbook для развёртывания phase-2 Windows collector'ов.
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_pfsense_poller.yml`deployment pfSense poller'а.
- `/home/igor/tmp/AWatch-rus/ansible/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.
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox-matrix.example.yml` — шаблон матрицы CT.
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/windows.example.yml` — шаблон переменных Windows phase-2.
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.example.yml` — шаблон переменных pfSense poller'а.
- `ansible/deploy_aw_server.yml` — основной playbook для уже существующего Debian/CT host.
- `ansible/provision_proxmox_ct_and_deploy_aw.yml`полный playbook для Proxmox.
- `ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml` — массовый полный playbook (несколько CT).
- `ansible/deploy_aw_windows.yml` — WinRM playbook для развёртывания Windows/RDP collector'ов.
- `ansible/deploy_aw_pfsense_poller.yml`развёртывание pfSense poller'а.
- `ansible/install_full_stack.yml` — полный установочный playbook (оркестратор всех этапов).
- `ansible/inventory.example.ini` — шаблон inventory.
- `ansible/group_vars/*.example.yml` — шаблоны переменных.
## Быстрый запуск
1. Скопируйте шаблоны:
- `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/all.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/all.yml`
- `cp ansible/inventory.example.ini ansible/inventory.ini`
- `cp ansible/group_vars/all.example.yml ansible/group_vars/all.yml`
2. Заполните значения в `inventory.ini` и `group_vars/all.yml`.
3. Запустите:
```bash
cd /home/igor/tmp/AWatch-rus/ansible
cd ansible
ansible-playbook -i inventory.ini deploy_aw_server.yml
```
@@ -40,7 +36,7 @@ ansible-playbook -i inventory.ini deploy_aw_server.yml
Если нужно прогнать полный цикл одной командой:
```bash
cd /home/igor/tmp/AWatch-rus/ansible
cd ansible
ansible-playbook -i inventory.ini install_full_stack.yml
```
@@ -48,7 +44,7 @@ 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_windows.yml` (группа `[aw_windows]`);
- `deploy_aw_pfsense_poller.yml` (группа `[aw_pfsense_pollers]`).
Пустые группы в `inventory.ini` безопасны: соответствующий play будет пропущен.
@@ -56,50 +52,51 @@ ansible-playbook -i inventory.ini install_full_stack.yml
## Полный запуск с нуля в Proxmox
1. Подготовьте inventory и vars:
- `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/all.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/all.yml`
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox.yml`
- `cp ansible/inventory.example.ini ansible/inventory.ini`
- `cp ansible/group_vars/all.example.yml ansible/group_vars/all.yml`
- `cp ansible/group_vars/proxmox.example.yml ansible/group_vars/proxmox.yml`
2. Заполните `group_vars/proxmox.yml` и `group_vars/all.yml`.
3. Запустите playbook:
```bash
cd /home/igor/tmp/AWatch-rus/ansible
cd ansible
ansible-playbook -i inventory.ini provision_proxmox_ct_and_deploy_aw.yml
```
## Массовый запуск (матрица CT)
1. Подготовьте матрицу:
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox-matrix.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox-matrix.yml`
- `cp ansible/group_vars/proxmox-matrix.example.yml ansible/group_vars/proxmox-matrix.yml`
2. Заполните `proxmox-matrix.yml`.
3. Запустите:
```bash
cd /home/igor/tmp/AWatch-rus/ansible
cd ansible
ansible-playbook -i inventory.ini provision_proxmox_ct_matrix_and_deploy_aw.yml
```
## Windows phase-2 rollout (WinRM)
## Windows/RDP rollout (WinRM)
1. Подготовьте inventory и vars:
- `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`
- `cp ansible/inventory.example.ini ansible/inventory.ini`
- `cp ansible/group_vars/windows.example.yml 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
cd /home/igor/tmp/AWatch-rus/ansible
ansible-playbook -i inventory.ini deploy_aw_windows_phase2.yml
cd ansible
ansible-playbook -i inventory.ini deploy_aw_windows.yml
```
Playbook:
- выгружает `windows/*` toolkit на целевой хост в `C:\Deploy\AWatch-rus\windows`;
- выполняет `deploy-ensemble.ps1` (deploy + hardening/recovery) с phase-2 policy/rules;
- выгружает полный `windows/*` toolkit на целевой хост в InnoSetup-compatible каталог `C:\Program Files\AWatch-rus\windows`, включая DLP и `worktime-session-collector.ps1`;
- если найден legacy config `C:\ProgramData\ActivityWatch-Phase2\deployment-config.json`, выполняет безопасную миграцию через `migrate-awatch-rus-paths.ps1`: backup, остановка задач, перенос данных, переписывание путей, пересоздание scheduled tasks и validation;
- выполняет `deploy-ensemble.ps1` (deploy + hardening/recovery) с policy/rules из AWatch-rus toolkit;
- после deploy принудительно запускает `ActivityWatch Recovery` и все `ActivityWatch Launch *` задачи;
- выполняет API smoke-check bucket `aw-watcher-afk_SHARKON2025` и ожидает свежие `not-afk` события;
- выполняет API smoke-check bucket `aw-watcher-afk_<COMPUTERNAME>` и ожидает свежие `not-afk` события;
- запускает `validate-deployment.ps1`;
- забирает JSON-отчёт в локальную директорию (`/tmp/aw-rus-validation` по умолчанию).
@@ -110,17 +107,27 @@ Playbook:
- `aw_windows_incident_capture_enabled: false` — отключить блок incidentCapture;
- `aw_windows_incident_screenshot_enabled: false` — не делать скриншот при DLP-инциденте;
- `aw_windows_incident_artifacts_root: 'C:\...\incident-artifacts'` — переопределить путь артефактов;
- `aw_windows_deploy_root: 'C:\Program Files\AWatch-rus'` — каталог toolkit, совпадает с InnoSetup `{app}`;
- `aw_windows_install_root: 'C:\Program Files\AWatch-rus\bin'` — каталог бинарников, совпадает с InnoSetup `AwDefaultInstallRoot`;
- `aw_windows_state_root: 'C:\ProgramData\AWatch-rus'` — каталог состояния/отчётов, совпадает с InnoSetup `AwDefaultStateRoot`;
- `aw_windows_validation_remote_path: '{{ aw_windows_state_root }}\aw_validate_ansible.json'` — отчёт Ansible-валидации хранится рядом с `ensemble-report-*.json`;
- `aw_windows_migration_enabled: true` — включить guard миграции текущего production из `ActivityWatch-Phase2` в единый `AWatch-rus`;
- `aw_windows_legacy_install_root` / `aw_windows_legacy_state_root` — старые production paths, откуда выполняется перенос;
- `aw_windows_migration_report_remote_path` — JSON-отчёт о миграции на Windows-хосте;
- `aw_windows_package_version`, `aw_windows_package_url`, `aw_windows_package_zip_path` — версия и источник Windows-пакета ActivityWatch;
- `aw_windows_api_smoke_check_bucket: ""` — автоматически использовать `aw-watcher-afk_<COMPUTERNAME>`;
- `aw_windows_fail_on_validation_error: true` — завершать playbook ошибкой, если `validate-deployment.ps1` возвращает `overallOk=false`;
- `aw_windows_skip_hardening: true` — пропустить `hardening-recovery.ps1` внутри ensemble-скрипта.
## pfSense poller rollout
## Развёртывание pfSense poller
1. Подготовьте vars:
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.yml`
- `cp ansible/group_vars/pfsense-poller.example.yml ansible/group_vars/pfsense-poller.yml`
2. Добавьте inventory group `[aw_pfsense_pollers]`.
3. Запустите:
```bash
cd /home/igor/tmp/AWatch-rus/ansible
cd ansible
ansible-playbook -i inventory.ini deploy_aw_pfsense_poller.yml
```
@@ -139,4 +146,6 @@ Playbook:
- Для Web UI используется checksum-based cache-bust для `ru-patch-v5.js` и `sw-cleanup.js`, чтобы браузер не держал старую DLP/русскую статику после деплоя.
- На `#/home` Web UI делит хосты на `Windows RDP` и `Virtual servers + Proxmox`.
- Выполнена валидация API `http://127.0.0.1:5600/api/0/info`.
- Для full-stack сценария CT создаётся автоматически через `pct create`.
- Для полного сценария CT создаётся автоматически через `pct create`.
- На Windows/RDP host развёрнуты AFK/window watchers, browser domain collector, DLP endpoint collector и worktime session collector.
- Проверочный JSON-отчёт Windows playbook должен иметь `overallOk=true`.
+11 -11
View File
@@ -1,5 +1,5 @@
---
- name: Deploy pfSense ActivityWatch poller
- name: Развернуть pfSense ActivityWatch poller
hosts: aw_pfsense_pollers
become: true
gather_facts: true
@@ -10,14 +10,14 @@
aw_pfsense_service_name: "aw-pfsense-poller.service"
tasks:
- name: Install required packages
- name: Установить обязательные пакеты
ansible.builtin.apt:
name:
- python3
state: present
update_cache: true
- name: Ensure directories exist
- name: Создать каталоги
ansible.builtin.file:
path: "{{ item }}"
state: directory
@@ -26,29 +26,29 @@
- "{{ aw_pfsense_install_root }}"
- "{{ aw_pfsense_config_dir }}"
- name: Install pfSense poller script
- name: Установить скрипт pfSense poller
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
- name: Установить 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
- Перезагрузить systemd
- name: Write pfSense poller config
- name: Записать конфигурацию pfSense poller
ansible.builtin.copy:
dest: "{{ aw_pfsense_config_dir }}/poller.json"
mode: "0600"
content: "{{ aw_pfsense_poller_config | to_nice_json }}"
notify:
- Restart pfSense poller
- Перезапустить pfSense poller
- name: Enable and start pfSense poller
- name: Включить и запустить pfSense poller
ansible.builtin.systemd:
name: "{{ aw_pfsense_service_name }}"
enabled: true
@@ -56,11 +56,11 @@
daemon_reload: true
handlers:
- name: Reload systemd
- name: Перезагрузить systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart pfSense poller
- name: Перезапустить pfSense poller
ansible.builtin.systemd:
name: "{{ aw_pfsense_service_name }}"
state: restarted
+159 -49
View File
@@ -1,5 +1,5 @@
---
- name: Deploy AWatch-rus server
- name: Развернуть сервер AWatch-rus
hosts: aw_server
become: true
gather_facts: true
@@ -9,22 +9,29 @@
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_release_install_dir: "{{ aw_release_root }}/aw-server-rust-{{ aw_server_version }}"
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
- name: Установить базовые пакеты
ansible.builtin.apt:
name:
- curl
- rsync
- unzip
state: present
update_cache: true
- name: Ensure service account exists
- name: Создать системную группу сервиса
ansible.builtin.group:
name: "{{ aw_server_group }}"
system: true
state: present
- name: Создать системную учётную запись сервиса
ansible.builtin.user:
name: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
@@ -33,7 +40,25 @@
system: true
create_home: false
- name: Ensure required directories
- name: Создать обязательные каталоги
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ aw_release_root }}"
- "{{ aw_release_dir }}"
- "{{ aw_release_install_dir }}"
- /opt/activitywatch
- /opt/activitywatch/bin
- "{{ aw_server_webui_dir }}"
- "{{ aw_server_webui_dir }}/js"
- "{{ aw_server_data_dir }}"
- "{{ aw_server_log_dir }}"
- /etc/activitywatch
- "{{ aw_bootstrap_dir }}"
- name: Настроить каталоги ActivityWatch с владельцем сервиса
ansible.builtin.file:
path: "{{ item }}"
state: directory
@@ -41,103 +66,188 @@
group: "{{ aw_server_group }}"
mode: "0755"
loop:
- /opt/activitywatch
- /opt/activitywatch/bin
- "{{ aw_release_root }}"
- "{{ aw_release_dir }}"
- "{{ aw_release_install_dir }}"
- "{{ aw_server_webui_dir }}"
- "{{ aw_server_webui_dir }}/js"
- "{{ aw_server_data_dir }}"
- "{{ aw_server_log_dir }}"
- /etc/activitywatch
- "{{ aw_bootstrap_dir }}"
- name: Download ActivityWatch release archive
- name: Скачать архив релиза ActivityWatch
ansible.builtin.get_url:
url: "{{ aw_server_download_url }}"
dest: "{{ aw_archive_path }}"
mode: "0644"
- name: Unpack ActivityWatch release
- name: Распаковать релиз ActivityWatch
ansible.builtin.unarchive:
src: "{{ aw_archive_path }}"
dest: "{{ aw_release_dir }}"
remote_src: true
extra_opts: ["-o"]
- name: Discover extracted AW directory
- name: Найти распакованный каталог ActivityWatch
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: Найти бинарный файл AW server
ansible.builtin.find:
paths: "{{ aw_release_dir }}"
file_type: file
patterns:
- aw-server-rust
- aw-server
register: aw_server_binary_find
- name: Verify extracted directory exists
- name: Найти каталог WebUI
ansible.builtin.find:
paths: "{{ aw_release_dir }}"
file_type: directory
patterns:
- aw-webui
- webui
register: aw_webui_dir_find
- name: Сохранить пути распакованного релиза
ansible.builtin.set_fact:
aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
aw_server_binary_path: "{{ (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
aw_webui_source_path: "{{ (aw_webui_dir_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
- name: Проверить, что компоненты релиза найдены
ansible.builtin.assert:
that:
- aw_release_extracted is defined
- aw_release_extracted | length > 0
fail_msg: "Cannot locate extracted ActivityWatch release directory."
- aw_server_binary_path is defined
- aw_server_binary_path | length > 0
- aw_webui_source_path is defined
- aw_webui_source_path | length > 0
fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch."
- name: Sync release content to /opt/activitywatch
- name: Создать каталог установленного релиза
ansible.builtin.file:
path: "{{ aw_release_install_dir }}"
state: directory
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0755"
- name: Установить бинарный файл AW server
ansible.builtin.copy:
remote_src: true
src: "{{ aw_server_binary_path }}"
dest: "{{ aw_release_install_dir }}/aw-server-rust"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0755"
- name: Создать ссылку на активный бинарный файл AW server
ansible.builtin.file:
src: "{{ aw_release_install_dir }}/aw-server-rust"
dest: /opt/activitywatch/bin/aw-server-rust
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
state: link
force: true
- name: Синхронизировать WebUI в RU каталог
ansible.builtin.command:
cmd: "rsync -a --delete {{ aw_release_extracted }}/ /opt/activitywatch/"
cmd: "rsync -a {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/"
- name: Copy bootstrap files from repository
- name: Настроить владельца файлов /opt/activitywatch
ansible.builtin.file:
path: /opt/activitywatch
state: directory
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
recurse: true
- name: Установить systemd service из шаблона репозитория
ansible.builtin.copy:
dest: /etc/systemd/system/activitywatch-server.service
mode: "0644"
content: >-
{{
lookup('file', aw_repo_root + '/aw-server/activitywatch-server.service')
| replace('__AW_SERVER_USER__', aw_server_user)
| replace('__AW_SERVER_GROUP__', aw_server_group)
| replace('__AW_SERVER_DATA_DIR__', aw_server_data_dir)
}}
notify:
- Перезагрузить systemd
- Перезапустить activitywatch
- name: Скопировать RU patch файлы WebUI из репозитория
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "{{ item.mode }}"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
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: Проверить наличие index.html после копирования
ansible.builtin.stat:
path: "{{ aw_server_webui_dir }}/index.html"
register: aw_webui_ru_index
- name: Insert RU patch scripts into index.html
- name: Проверить, что index.html доступен для RU patch
ansible.builtin.assert:
that:
- aw_webui_ru_index.stat.exists
fail_msg: "Не найден index.html WebUI для применения RU patch."
- name: Удалить старые теги RU patch из index.html
ansible.builtin.replace:
path: "{{ aw_server_webui_dir }}/index.html"
regexp: '<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*></script>'
replace: ''
- name: Добавить cleanup script RU patch в 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
- name: Добавить загрузчик RU patch перед закрытием body
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
- name: Записать /etc/activitywatch/aw-server.env
ansible.builtin.copy:
dest: /etc/activitywatch/aw-server.env
mode: "0640"
owner: root
group: root
content: |
AW_SERVER_HOST={{ aw_server_bind_host }}
AW_SERVER_BIND_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_DATA_DIR={{ aw_server_data_dir }}
AW_SERVER_LOG_DIR={{ aw_server_log_dir }}
AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }}
AW_SERVER_USER={{ aw_server_user }}
AW_SERVER_GROUP={{ aw_server_group }}
- name: Enable and start service
- name: Включить и запустить сервис
ansible.builtin.systemd:
name: activitywatch-server.service
enabled: true
state: restarted
daemon_reload: true
- name: Wait for API
- name: Дождаться ответа API
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info"
method: GET
@@ -147,25 +257,25 @@
delay: 3
until: aw_api.status == 200
- name: Apply baseline worktime settings (classes)
- name: Применить базовые 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
status_code: [200, 201]
when: aw_apply_worktime_settings | default(false) | bool
- name: Apply baseline views (include DLP and worktime)
- name: Применить базовые views для DLP и 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
status_code: [200, 201]
when: aw_apply_worktime_settings | default(false) | bool
- name: Derive worktime durationDefault from aw_worktime_from/to
- name: Вычислить worktime durationDefault из 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 }}"
@@ -182,7 +292,7 @@
}}
when: aw_apply_worktime_settings | default(false) | bool
- name: Normalize derived durationDefault for overnight shifts
- name: Нормализовать durationDefault для ночных смен
ansible.builtin.set_fact:
aw_worktime_duration_default_effective: >-
{{
@@ -192,15 +302,15 @@
}}
when: aw_apply_worktime_settings | default(false) | bool
- name: Validate derived durationDefault is sane
- name: Проверить корректность durationDefault
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 }}"
fail_msg: "Некорректный интервал рабочего времени: {{ aw_worktime_from }}..{{ aw_worktime_to }}"
when: aw_apply_worktime_settings | default(false) | bool
- name: Apply baseline worktime period (startOfDay)
- name: Применить базовый период worktime (startOfDay)
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/startOfDay"
method: POST
@@ -209,7 +319,7 @@
status_code: 200
when: aw_apply_worktime_settings | default(false) | bool
- name: Apply baseline worktime period (durationDefault seconds)
- name: Применить базовый период worktime (durationDefault seconds)
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/durationDefault"
method: POST
@@ -219,11 +329,11 @@
when: aw_apply_worktime_settings | default(false) | bool
handlers:
- name: Reload systemd
- name: Перезагрузить systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart activitywatch
- name: Перезапустить activitywatch
ansible.builtin.systemd:
name: activitywatch-server.service
state: restarted
+232
View File
@@ -0,0 +1,232 @@
---
- name: Развернуть Windows/RDP collector'ы AWatch-rus
hosts: aw_windows
gather_facts: false
vars:
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
aw_windows_package_zip_path: ""
aw_windows_domain: "SHARKON2025"
aw_windows_users:
- user1
- user2
- user3
- user4
- user5
aw_windows_extra_users: []
aw_windows_users_effective: "{{ (aw_windows_users + aw_windows_extra_users) | unique }}"
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
aw_windows_afk_enabled: true
aw_windows_window_enabled: true
aw_windows_local_agent_logs_enabled: false
aw_windows_incident_capture_enabled: true
aw_windows_incident_screenshot_enabled: true
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
aw_windows_logon_marker_enabled: true
aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_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_windows_api_smoke_check_limit: 10
aw_windows_fail_on_validation_error: true
aw_windows_migration_enabled: true
aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2"
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
tasks:
- name: Проверить обязательные переменные
ansible.builtin.assert:
that:
- aw_windows_server_host is defined
- aw_windows_server_port is defined
- aw_windows_server_scheme is defined
- aw_windows_domain is defined
- aw_windows_users_effective | length > 0
- aw_windows_install_root is defined
- aw_windows_state_root is defined
fail_msg: "Не заданы обязательные переменные Windows-развёртывания."
- name: Создать каталоги развёртывания
ansible.windows.win_file:
path: "{{ item }}"
state: directory
loop:
- "{{ aw_windows_deploy_root }}"
- "{{ aw_windows_deploy_root }}\\windows"
- name: Загрузить Windows toolkit развёртывания
ansible.windows.win_copy:
src: "{{ aw_windows_repo_root }}/windows/{{ item }}"
dest: "{{ aw_windows_deploy_root }}\\windows\\{{ item }}"
loop:
- ActivityWatch.Windows.Common.psd1
- ActivityWatch.Windows.Common.psm1
- browser-domains-native-collector.ps1
- dlp-endpoint-signals-collector.ps1
- worktime-session-collector.ps1
- migrate-awatch-rus-paths.ps1
- deploy-domain-users.ps1
- deploy-ensemble.ps1
- hardening-recovery.ps1
- validate-deployment.ps1
- web-category-rules.example.json
- dlp-policy.example.json
- name: Загрузить список пользователей для доменного развёртывания
ansible.windows.win_copy:
dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
content: |
{% for user in aw_windows_users_effective -%}
{{ user }}
{% endfor -%}
- name: Проверить нужен ли migration с legacy ActivityWatch путей
when: aw_windows_migration_enabled | bool
ansible.windows.win_stat:
path: "{{ aw_windows_legacy_state_root }}\\deployment-config.json"
register: aw_windows_legacy_config
- name: Выполнить безопасную migration legacy prod в AWatch-rus
when:
- aw_windows_migration_enabled | bool
- aw_windows_legacy_config.stat.exists | default(false)
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$result = & "{{ aw_windows_deploy_root }}\windows\migrate-awatch-rus-paths.ps1" `
-OldInstallRoot "{{ aw_windows_legacy_install_root }}" `
-OldStateRoot "{{ aw_windows_legacy_state_root }}" `
-NewInstallRoot "{{ aw_windows_install_root }}" `
-NewStateRoot "{{ aw_windows_state_root }}" `
-ToolkitRoot "{{ aw_windows_deploy_root }}\windows"
$result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_migration_report_remote_path }}" -Encoding utf8
- name: Запустить Windows/RDP ensemble развёртывание
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$params = @{
ServerScheme = "{{ aw_windows_server_scheme }}"
ServerHost = "{{ aw_windows_server_host }}"
ServerPort = {{ aw_windows_server_port }}
Version = "{{ aw_windows_package_version }}"
Domain = "{{ aw_windows_domain }}"
UserListPath = "{{ aw_windows_deploy_root }}\windows\users.txt"
InstallRoot = "{{ aw_windows_install_root }}"
StateRoot = "{{ aw_windows_state_root }}"
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
IncidentArtifactsRoot = "{{ aw_windows_incident_artifacts_root }}"
LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
CustomRulesPath = "{{ aw_windows_rules_path }}"
CustomPolicyPath = "{{ aw_windows_policy_path }}"
}
{% if (aw_windows_package_url | default('') | string | length) > 0 %}
$params.PackageUrl = "{{ aw_windows_package_url }}"
{% endif %}
{% if (aw_windows_package_zip_path | default('') | string | length) > 0 %}
$params.PackageZipPath = "{{ aw_windows_package_zip_path }}"
{% endif %}
{% if aw_windows_skip_hardening | bool %}
$params.SkipHardening = $true
{% endif %}
& "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params
- name: Принудительно запустить ActivityWatch recovery и 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: Получить Windows hostname для AW smoke-check bucket
when:
- aw_windows_api_smoke_check_enabled | bool
- aw_windows_afk_enabled | bool
ansible.windows.win_command: powershell.exe -NoProfile -Command "$env:COMPUTERNAME"
register: aw_windows_hostname_result
changed_when: false
- name: Вычислить AW AFK smoke-check bucket
when:
- aw_windows_api_smoke_check_enabled | bool
- aw_windows_afk_enabled | bool
ansible.builtin.set_fact:
aw_windows_api_smoke_check_bucket_effective: >-
{{
aw_windows_api_smoke_check_bucket
if (aw_windows_api_smoke_check_bucket | default('') | string | length) > 0
else 'aw-watcher-afk_' ~ (aw_windows_hostname_result.stdout | trim)
}}
- name: Дождаться свежих AFK событий на AW server
when:
- aw_windows_api_smoke_check_enabled | bool
- aw_windows_afk_enabled | bool
delegate_to: localhost
ansible.builtin.uri:
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/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: Выполнить валидацию и сохранить отчёт на целевом Windows host
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
$report | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
if ({{ '$true' if (aw_windows_fail_on_validation_error | bool) else '$false' }} -and -not [bool]$report.overallOk) {
throw "Проверка развёртывания ActivityWatch завершилась ошибкой. Отчёт: {{ aw_windows_validation_remote_path }}"
}
- name: Создать локальный каталог для validation reports
ansible.builtin.file:
path: "{{ aw_windows_validation_local_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
- name: Забрать validation report
ansible.builtin.fetch:
src: "{{ aw_windows_validation_remote_path }}"
dest: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
flat: true
- name: Показать путь к отчёту
ansible.builtin.debug:
msg:
- "Windows/RDP развёртывание завершено на {{ inventory_hostname }}."
- "Отчёт проверки: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
-169
View File
@@ -1,169 +0,0 @@
---
- name: Deploy AWatch-rus Windows phase2 collectors
hosts: aw_windows
gather_facts: false
vars:
aw_windows_repo_root: "/home/igor/tmp/AWatch-rus"
aw_windows_deploy_root: "C:\\Deploy\\AWatch-rus"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_domain: "SHARKON2025"
aw_windows_users:
- user1
- user2
- user3
- user4
- user5
aw_windows_extra_users: []
aw_windows_users_effective: "{{ (aw_windows_users + aw_windows_extra_users) | unique }}"
aw_windows_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_state_root: "C:\\ProgramData\\ActivityWatch"
aw_windows_afk_enabled: true
aw_windows_window_enabled: true
aw_windows_local_agent_logs_enabled: false
aw_windows_incident_capture_enabled: true
aw_windows_incident_screenshot_enabled: true
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
aw_windows_logon_marker_enabled: true
aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
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
ansible.builtin.assert:
that:
- aw_windows_server_host is defined
- aw_windows_server_port is defined
- aw_windows_domain is defined
- aw_windows_users_effective | length > 0
- aw_windows_install_root is defined
- aw_windows_state_root is defined
fail_msg: "Missing required Windows deployment variables."
- name: Ensure deploy directories exist
ansible.windows.win_file:
path: "{{ item }}"
state: directory
loop:
- "{{ aw_windows_deploy_root }}"
- "{{ aw_windows_deploy_root }}\\windows"
- name: Upload Windows deployment toolkit
ansible.windows.win_copy:
src: "{{ aw_windows_repo_root }}/windows/{{ item }}"
dest: "{{ aw_windows_deploy_root }}\\windows\\{{ item }}"
loop:
- ActivityWatch.Windows.Common.psd1
- ActivityWatch.Windows.Common.psm1
- browser-domains-native-collector.ps1
- dlp-endpoint-signals-collector.ps1
- deploy-domain-users.ps1
- deploy-ensemble.ps1
- hardening-recovery.ps1
- validate-deployment.ps1
- web-category-rules.example.json
- dlp-policy.example.json
- name: Upload user list for domain deploy
ansible.windows.win_copy:
dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
content: |
{% for user in aw_windows_users -%}
{{ user }}
{% endfor -%}
{% for user in aw_windows_extra_users -%}
{{ user }}
{% endfor -%}
- name: Run phase2 ensemble deployment
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$params = @{
ServerHost = "{{ aw_windows_server_host }}"
ServerPort = {{ aw_windows_server_port }}
Domain = "{{ aw_windows_domain }}"
UserListPath = "{{ aw_windows_deploy_root }}\windows\users.txt"
InstallRoot = "{{ aw_windows_install_root }}"
StateRoot = "{{ aw_windows_state_root }}"
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
IncidentArtifactsRoot = "{{ aw_windows_incident_artifacts_root }}"
LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
CustomRulesPath = "{{ aw_windows_rules_path }}"
CustomPolicyPath = "{{ aw_windows_policy_path }}"
}
{% if aw_windows_skip_hardening | bool %}
$params.SkipHardening = $true
{% 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: |
$ErrorActionPreference = 'Stop'
$report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
$report | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
- name: Ensure local validation directory exists
ansible.builtin.file:
path: "{{ aw_windows_validation_local_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
- name: Fetch validation report
ansible.builtin.fetch:
src: "{{ aw_windows_validation_remote_path }}"
dest: "{{ aw_windows_validation_local_dir }}/"
flat: false
- name: Show report location
ansible.builtin.debug:
msg:
- "Windows phase2 deploy completed on {{ inventory_hostname }}."
- "Validation report: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}/C$/Windows/Temp/aw_validate_phase2_ansible.json"
+7 -7
View File
@@ -8,17 +8,17 @@ aw_server_log_dir: "/var/log/activitywatch"
aw_server_user: "activitywatch"
aw_server_group: "activitywatch"
aw_repo_root: "/home/igor/tmp/AWatch-rus"
aw_repo_root: "{{ playbook_dir | dirname }}"
# Optional: apply a baseline worktime-focused categorization and views via AW settings API.
# WARNING: this overwrites existing server-side settings/classes/views.
# Опционально: применить базовые категории и views для рабочего времени через AW settings API.
# Внимание: это перезаписывает существующие 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.
# Опциональные значения периода рабочего времени в Web UI.
# startOfDay задаёт границу дня и стартовое время окна отчёта.
# durationDefault задаёт диапазон по умолчанию в секундах.
#
# Recommended: set worktime window explicitly and let the playbook derive duration.
# Рекомендуется явно задать рабочий интервал и дать playbook вычислить duration.
aw_worktime_from: "08:00"
aw_worktime_to: "17:00"
aw_worktime_start_of_day: "{{ aw_worktime_from }}"
+23 -6
View File
@@ -1,7 +1,11 @@
aw_windows_repo_root: "/home/igor/tmp/AWatch-rus"
aw_windows_deploy_root: "C:\\Deploy\\AWatch-rus"
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
aw_windows_package_zip_path: ""
aw_windows_domain: "SHARKON2025"
aw_windows_users:
- user1
@@ -14,9 +18,9 @@ aw_windows_extra_users: []
# aw_windows_extra_users:
# - Администратор
# Рекомендуемый изолированный профиль для фазового раската.
aw_windows_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_state_root: "C:\\ProgramData\\ActivityWatch"
# Единые Windows/RDP пути: те же, что использует InnoSetup.
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
aw_windows_afk_enabled: true
aw_windows_window_enabled: true
aw_windows_local_agent_logs_enabled: false
@@ -29,5 +33,18 @@ aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
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_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ansible.json"
aw_windows_validation_local_dir: "/tmp/aw-rus-validation"
aw_windows_fail_on_validation_error: true
# Безопасная миграция текущего прода со старых путей в единый профиль AWatch-rus.
aw_windows_migration_enabled: true
aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2"
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
# По умолчанию AFK bucket вычисляется как aw-watcher-afk_<COMPUTERNAME>.
# Задайте явное значение только если watcher пишет в нестандартный bucket.
aw_windows_api_smoke_check_enabled: true
aw_windows_api_smoke_check_bucket: ""
aw_windows_api_smoke_check_limit: 10
+10 -10
View File
@@ -1,16 +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
# Полный установщик AWatch-rus.
# Выполняет развёртывание одной командой:
# 1) создание Proxmox CT + bootstrap AW (если в inventory есть [proxmox])
# 2) развёртывание AW server на хостах [aw_server]
# 3) развёртывание Windows/RDP collector'ов на [aw_windows]
# 4) развёртывание pfSense poller'а на [aw_pfsense_pollers]
#
# Notes:
# - Keep only relevant inventory groups filled for your environment.
# - Plays with unmatched host groups are skipped automatically by Ansible.
# Примечания:
# - Заполняйте только нужные группы inventory для своего окружения.
# - Play без совпадающих host groups 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_windows.yml
- import_playbook: deploy_aw_pfsense_poller.yml
+4 -1
View File
@@ -5,5 +5,8 @@ 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]
# NOTE: in RU-localized installs this account is often "Администратор" instead of "Administrator".
# Примечание: в русифицированных Windows часто нужен "Администратор", а не "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
[aw_pfsense_pollers]
# pfsense-poller1 ansible_host=192.168.100.30 ansible_user=root ansible_port=22
@@ -1,5 +1,5 @@
---
- name: Provision single Proxmox CT and deploy AWatch-rus
- name: Создать один Proxmox CT и развернуть AWatch-rus
hosts: proxmox
gather_facts: false
@@ -17,7 +17,7 @@
- settings/views-default.json
tasks:
- name: Execute single-CT provisioning workflow
- name: Выполнить workflow создания одного CT
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
vars:
ct_id: "{{ proxmox_ct_id }}"
@@ -1,5 +1,5 @@
---
- name: Provision Proxmox CT matrix and deploy AWatch-rus with RU patch
- name: Создать матрицу Proxmox CT и развернуть AWatch-rus с RU patch
hosts: proxmox
gather_facts: false
@@ -17,14 +17,14 @@
- settings/views-default.json
tasks:
- name: Validate CT matrix is provided
- name: Проверить, что матрица CT задана
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"
fail_msg: "Задайте proxmox_ct_matrix в group_vars/proxmox-matrix.yml"
- name: Execute provisioning workflow for each CT
- name: Выполнить workflow создания для каждого CT
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
vars:
ct_id: "{{ item.id }}"
+33 -23
View File
@@ -1,5 +1,5 @@
---
- name: Validate required per-CT variables
- name: Проверить обязательные переменные CT
ansible.builtin.assert:
that:
- ct_id is defined
@@ -27,14 +27,14 @@
- 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."
fail_msg: "Не заданы обязательные переменные для создания CT и развёртывания."
- name: Build CT network string
- name: Сформировать сетевую строку CT
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
- name: Проверить, существует ли CT
ansible.builtin.command:
argv:
- pct
@@ -44,7 +44,7 @@
failed_when: false
changed_when: false
- name: Create CT when absent
- name: Создать CT, если он отсутствует
ansible.builtin.command:
argv:
- pct
@@ -78,8 +78,9 @@
- --ostype
- debian
when: ct_status_check.rc != 0
no_log: true
- name: Check current CT runtime state
- name: Проверить текущее состояние CT
ansible.builtin.command:
argv:
- pct
@@ -88,7 +89,7 @@
register: ct_runtime_status
changed_when: false
- name: Start CT when stopped
- name: Запустить CT, если он остановлен
ansible.builtin.command:
argv:
- pct
@@ -96,20 +97,23 @@
- "{{ ct_id }}"
when: "'stopped' in ct_runtime_status.stdout"
- name: Ensure bootstrap directory on Proxmox host
- name: Создать bootstrap каталог на Proxmox host
ansible.builtin.file:
path: "{{ proxmox_bootstrap_dir }}"
path: "{{ item }}"
state: directory
mode: "0700"
loop:
- "{{ proxmox_bootstrap_dir }}"
- "{{ proxmox_bootstrap_dir }}/settings"
- name: Copy AW bootstrap files to Proxmox host temp
- name: Скопировать AW bootstrap файлы во временный каталог Proxmox host
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
- name: Установить базовые зависимости ОС внутри CT
ansible.builtin.command:
argv:
- pct
@@ -122,12 +126,16 @@
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
apt-get install -y curl ca-certificates bash unzip xz-utils jq rsync openssh-server python3
mkdir -p /root/bootstrap/settings /etc/activitywatch
systemctl enable ssh || true
systemctl restart ssh || true
register: ct_bootstrap_result
retries: 10
delay: 6
until: ct_bootstrap_result.rc == 0
- name: Push bootstrap files into CT
- name: Передать bootstrap файлы внутрь CT
ansible.builtin.command:
argv:
- pct
@@ -137,7 +145,7 @@
- "/root/bootstrap/{{ item }}"
loop: "{{ aw_bootstrap_files }}"
- name: Write AW server env file on Proxmox host temp
- name: Записать AW server env во временный каталог Proxmox host
ansible.builtin.copy:
dest: "{{ proxmox_bootstrap_dir }}/aw-server.env"
mode: "0600"
@@ -151,8 +159,9 @@
AW_SERVER_LOG_DIR={{ aw_server_log_dir }}
AW_SERVER_USER={{ aw_server_user }}
AW_SERVER_GROUP={{ aw_server_group }}
no_log: true
- name: Push AW server env into CT
- name: Передать AW server env внутрь CT
ansible.builtin.command:
argv:
- pct
@@ -160,8 +169,9 @@
- "{{ ct_id }}"
- "{{ proxmox_bootstrap_dir }}/aw-server.env"
- /etc/activitywatch/aw-server.env
no_log: true
- name: Set mode for env inside CT
- name: Настроить права env файла внутри CT
ansible.builtin.command:
argv:
- pct
@@ -172,7 +182,7 @@
- "0600"
- /etc/activitywatch/aw-server.env
- name: Install server and apply RU patch inside CT
- name: Установить сервер и применить RU patch внутри CT
ansible.builtin.command:
argv:
- pct
@@ -188,7 +198,7 @@
bash /root/bootstrap/apply_webui_ru_patch.sh
systemctl restart activitywatch-server.service
- name: Validate AW API from inside CT
- name: Проверить AW API изнутри CT
ansible.builtin.command:
argv:
- pct
@@ -199,7 +209,7 @@
- -lc
- "curl -fsS http://127.0.0.1:{{ aw_server_port }}/api/0/info >/dev/null"
- name: Validate RU patch hooks in index
- name: Проверить hooks RU patch в index.html
ansible.builtin.command:
argv:
- pct
@@ -210,8 +220,8 @@
- -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
- name: Показать итоговый endpoint
ansible.builtin.debug:
msg:
- "CT {{ ct_id }} is provisioned and configured."
- "ActivityWatch endpoint: http://{{ ct_ip | regex_replace('/[0-9]+$', '') }}:{{ aw_server_port }}"
- "CT {{ ct_id }} создан и настроен."
- "Endpoint ActivityWatch: http://{{ ct_ip | regex_replace('/[0-9]+$', '') }}:{{ aw_server_port }}"
+15
View File
@@ -29,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",
+44 -3
View File
@@ -680,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",
@@ -740,7 +753,15 @@
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_",
@@ -770,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) {
@@ -813,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);
});
@@ -869,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);
}
+4 -4
View File
@@ -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": {}
+32 -19
View File
@@ -21,7 +21,7 @@
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_server.yml`
- `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_and_deploy_aw.yml`
- `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml`
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_windows_phase2.yml`
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_windows.yml`
---
@@ -177,7 +177,7 @@ grep -n 'aw-ru-patch\|aw-sw-cleanup' /opt/activitywatch/webui-ru/index.html
например в:
- `C:\Deploy\ActivityWatch-Russian\windows`
- `C:\Program Files\AWatch-rus\windows`
Откройте **elevated PowerShell**:
@@ -187,15 +187,28 @@ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
### 3.2 Массовое доменное развёртывание (рекомендуется)
Если текущий production ещё работает в старых каталогах
`C:\Program Files\ActivityWatch-Phase2` и `C:\ProgramData\ActivityWatch-Phase2`,
сначала выполните безопасную миграцию:
```powershell
C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1 -WhatIf
C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1
```
Скрипт остановит `ActivityWatch Recovery`/`ActivityWatch Launch *`, создаст backup в
`C:\ProgramData\AWatch-rus\migration-backups\...`, перенесёт файлы в единые пути,
пересоздаст `deployment-config.json`/scheduled tasks и запустит validation.
Пример со списком пользователей:
```powershell
C:\Deploy\ActivityWatch-Russian\windows\deploy-domain-users.ps1 `
C:\Program Files\AWatch-rus\windows\deploy-domain-users.ps1 `
-ServerHost aw.example.local `
-ServerPort 5600 `
-Domain CONTOSO `
-UserListPath C:\Deploy\aw-users.txt `
-CustomRulesPath C:\Deploy\ActivityWatch-Russian\windows\web-category-rules.example.json
-CustomRulesPath C:\Program Files\AWatch-rus\windows\web-category-rules.example.json
```
Поддерживаемые варианты:
@@ -207,7 +220,7 @@ C:\Deploy\ActivityWatch-Russian\windows\deploy-domain-users.ps1 `
### 3.2.1 Ensemble orchestration (рекомендуется для production)
```powershell
C:\Deploy\ActivityWatch-Russian\windows\deploy-ensemble.ps1 `
C:\Program Files\AWatch-rus\windows\deploy-ensemble.ps1 `
-ServerHost aw.example.local `
-ServerPort 5600 `
-Domain CONTOSO `
@@ -217,30 +230,30 @@ C:\Deploy\ActivityWatch-Russian\windows\deploy-ensemble.ps1 `
Отчёт сохраняется в:
- `C:\ProgramData\ActivityWatch\ensemble-report-YYYYMMDD-HHMMSS.json`
- `C:\ProgramData\AWatch-rus\ensemble-report-YYYYMMDD-HHMMSS.json`
### 3.3 Single-user развёртывание
```powershell
C:\Deploy\ActivityWatch-Russian\windows\deploy-single-user.ps1 `
C:\Program Files\AWatch-rus\windows\deploy-single-user.ps1 `
-ServerHost aw.example.local `
-ServerPort 5600 `
-TargetUser 'CONTOSO\user01' `
-CustomRulesPath C:\Deploy\ActivityWatch-Russian\windows\web-category-rules.example.json
-CustomRulesPath C:\Program Files\AWatch-rus\windows\web-category-rules.example.json
```
### 3.4 Recovery / hardening
```powershell
C:\Deploy\ActivityWatch-Russian\windows\hardening-recovery.ps1 `
-ConfigPath C:\ProgramData\ActivityWatch\deployment-config.json
C:\Program Files\AWatch-rus\windows\hardening-recovery.ps1 `
-ConfigPath C:\ProgramData\AWatch-rus\deployment-config.json
```
### 3.5 Валидация deployment-а (PowerShell report)
```powershell
$report = C:\Deploy\ActivityWatch-Russian\windows\validate-deployment.ps1 `
-ConfigPath C:\ProgramData\ActivityWatch\deployment-config.json
$report = C:\Program Files\AWatch-rus\windows\validate-deployment.ps1 `
-ConfigPath C:\ProgramData\AWatch-rus\deployment-config.json
$report | ConvertTo-Json -Depth 12
```
@@ -248,13 +261,13 @@ $report | ConvertTo-Json -Depth 12
## 4) Что должно появиться на Windows после установки
- `C:\Program Files\ActivityWatch`
- `C:\ProgramData\ActivityWatch\deployment-config.json`
- `C:\ProgramData\ActivityWatch\launch-watchers.ps1`
- `C:\ProgramData\ActivityWatch\recovery-loop.ps1`
- `C:\ProgramData\ActivityWatch\browser-domains-native-collector.ps1`
- `C:\ProgramData\ActivityWatch\web-category-rules.json`
- `C:\ProgramData\ActivityWatch\logs\`
- `C:\Program Files\AWatch-rus\bin`
- `C:\ProgramData\AWatch-rus\deployment-config.json`
- `C:\ProgramData\AWatch-rus\launch-watchers.ps1`
- `C:\ProgramData\AWatch-rus\recovery-loop.ps1`
- `C:\ProgramData\AWatch-rus\browser-domains-native-collector.ps1`
- `C:\ProgramData\AWatch-rus\web-category-rules.json`
- `C:\ProgramData\AWatch-rus\logs\`
Задачи планировщика:
+85
View File
@@ -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 шагов.
- Для статуса работ: что сделано, что проверено, что дальше.
+55 -27
View File
@@ -8,21 +8,21 @@
- `windows/hardening-recovery.ps1` — повторная регистрация задач, ACL и recovery-loop.
- `windows/validate-deployment.ps1` — машинная проверка состояния и JSON-отчёт.
- `windows/browser-domains-native-collector.ps1` — native collector доменов браузера с категоризацией.
- `windows/dlp-endpoint-signals-collector.ps1`phase-2 collector (clipboard/USB/print signals).
- `windows/dlp-endpoint-signals-collector.ps1`Windows/RDP collector (clipboard/USB/print signals).
- `windows/web-category-rules.example.json` — пример кастомных правил категоризации.
- `windows/dlp-policy.example.json` — пример DLP-политики (phase-1: alerting incidents).
## Что делает пакет
- Ставит `aw-watcher-afk` и `aw-watcher-window` из официального Windows ZIP ActivityWatch.
- Копирует browser-domain collector в `C:\ProgramData\ActivityWatch`.
- Копирует DLP policy в `C:\ProgramData\ActivityWatch\dlp-policy.json`.
- Копирует browser-domain collector в `C:\ProgramData\AWatch-rus`.
- Копирует DLP policy в `C:\ProgramData\AWatch-rus\dlp-policy.json`.
- Включает `incidentCapture` в `deployment-config.json` для DLP-инцидентов:
- `incidentCapture.screenshotEnabled = true`
- `incidentCapture.artifactsRoot = <StateRoot>\incident-artifacts`
- Создаёт per-user задачи `ActivityWatch Launch [...]` с запуском при логоне.
- Создаёт системную задачу `ActivityWatch Recovery`, которая циклически перезапускает per-user launch tasks.
- Применяет ACL к `C:\Program Files\ActivityWatch`, `C:\ProgramData\ActivityWatch` и каталогу логов.
- Применяет ACL к `C:\Program Files\AWatch-rus\bin`, `C:\ProgramData\AWatch-rus` и каталогу логов.
- Не содержит хардкодов инфраструктуры: сервер, домен, список пользователей и правила передаются параметрами.
- Корректно регистрирует задачи через `-LogonType Interactive` (совместимо с Windows Server, где `InteractiveToken` не поддерживается).
- Поддерживает отключение шумных watcher'ов через `-AfkEnabled:$false` и `-WindowEnabled:$false`.
@@ -143,9 +143,37 @@ CSV-формат: колонка `User`, `Username`, `SamAccountName` или `Lo
Если список уже содержит `DOMAIN\user`, параметр `-Domain` не нужен.
## Рекомендуемый phased rollout (изолированный профиль)
## Безопасная миграция текущего production
Для безопасного параллельного запуска рядом с legacy-инсталляцией используйте отдельные пути:
Если текущий RDP production уже работает в `C:\Program Files\ActivityWatch-Phase2` и
`C:\ProgramData\ActivityWatch-Phase2`, не запускайте обычный update без миграции.
Сначала выполните перенос в единый профиль AWatch-rus:
```powershell
C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1 -WhatIf
C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1
```
Скрипт делает безопасный порядок:
1. Находит старый `deployment-config.json`.
2. Останавливает `ActivityWatch Recovery` и `ActivityWatch Launch *`.
3. Создаёт backup старых и новых каталогов в
`C:\ProgramData\AWatch-rus\migration-backups\YYYYMMDD-HHMMSS`.
4. Копирует бинарники/состояние в единые пути:
- `C:\Program Files\AWatch-rus\bin`
- `C:\ProgramData\AWatch-rus`
5. Переписывает пути в `deployment-config.json`.
6. Пересоздаёт launcher/recovery scripts и scheduled tasks.
7. Запускает `validate-deployment.ps1`; при ошибке оставляет backup path в сообщении.
Ansible playbook `ansible/deploy_aw_windows.yml` выполняет этот migration guard
автоматически, если на хосте найден
`C:\ProgramData\ActivityWatch-Phase2\deployment-config.json`.
## Рекомендуемый rollout
Для запуска после миграции используйте единые пути:
```powershell
.\windows\deploy-domain-users.ps1 `
@@ -153,10 +181,10 @@ CSV-формат: колонка `User`, `Username`, `SamAccountName` или `Lo
-ServerPort 5600 `
-Domain SHARKON2025 `
-Users user2,user3,user4,user5 `
-InstallRoot 'C:\Program Files\ActivityWatch-Phase2-u2u5' `
-StateRoot 'C:\ProgramData\ActivityWatch-Phase2-u2u5' `
-CustomRulesPath C:\Deploy\AWatch-rus\windows\web-category-rules.example.json `
-CustomPolicyPath C:\Deploy\AWatch-rus\windows\dlp-policy.example.json
-InstallRoot 'C:\Program Files\AWatch-rus\bin' `
-StateRoot 'C:\ProgramData\AWatch-rus' `
-CustomRulesPath C:\Program Files\AWatch-rus\windows\web-category-rules.example.json `
-CustomPolicyPath C:\Program Files\AWatch-rus\windows\dlp-policy.example.json
```
Single-user pilot в таком же стиле:
@@ -166,10 +194,10 @@ Single-user pilot в таком же стиле:
-ServerHost 10.10.10.13 `
-ServerPort 5600 `
-TargetUser 'SHARKON2025\user1' `
-InstallRoot 'C:\Program Files\ActivityWatch-Phase2' `
-StateRoot 'C:\ProgramData\ActivityWatch-Phase2-user1' `
-CustomRulesPath C:\Deploy\AWatch-rus\windows\web-category-rules.example.json `
-CustomPolicyPath C:\Deploy\AWatch-rus\windows\dlp-policy.example.json
-InstallRoot 'C:\Program Files\AWatch-rus\bin' `
-StateRoot 'C:\ProgramData\AWatch-rus' `
-CustomRulesPath C:\Program Files\AWatch-rus\windows\web-category-rules.example.json `
-CustomPolicyPath C:\Program Files\AWatch-rus\windows\dlp-policy.example.json
```
## Ensemble deploy (production workflow)
@@ -186,31 +214,31 @@ Single-user pilot в таком же стиле:
Итоговый отчёт:
- `C:\ProgramData\ActivityWatch\ensemble-report-YYYYMMDD-HHMMSS.json`
- `C:\ProgramData\AWatch-rus\ensemble-report-YYYYMMDD-HHMMSS.json`
## Категоризация доменов
- Встроенные категории покрывают базовые рабочие, нейтральные и личные домены.
- Для кастомизации скопируйте `windows/web-category-rules.example.json` и отредактируйте домены.
- Передайте файл через `-CustomRulesPath`; он будет сохранён как `C:\ProgramData\ActivityWatch\web-category-rules.json`.
- Передайте файл через `-CustomRulesPath`; он будет сохранён как `C:\ProgramData\AWatch-rus\web-category-rules.json`.
- Пользовательские правила имеют приоритет над встроенными.
## Структура после установки
- `C:\Program Files\ActivityWatch` — бинарники watcher'ов.
- `C:\ProgramData\ActivityWatch\deployment-config.json` — итоговая конфигурация.
- `C:\ProgramData\ActivityWatch\incident-artifacts\` — скриншоты DLP-инцидентов (если `incidentCapture.screenshotEnabled=true`).
- `C:\ProgramData\ActivityWatch\launch-watchers.ps1` — per-user launcher.
- `C:\ProgramData\ActivityWatch\recovery-loop.ps1` — system recovery loop.
- `C:\ProgramData\ActivityWatch\browser-domains-native-collector.ps1` — runtime collector.
- `C:\ProgramData\ActivityWatch\dlp-endpoint-signals-collector.ps1` — runtime endpoint collector.
- `C:\ProgramData\ActivityWatch\dlp-policy.json` — активная DLP-политика.
- `C:\ProgramData\ActivityWatch\logs\` — логи collector'а.
- `C:\Program Files\AWatch-rus\bin` — бинарники watcher'ов.
- `C:\ProgramData\AWatch-rus\deployment-config.json` — итоговая конфигурация.
- `C:\ProgramData\AWatch-rus\incident-artifacts\` — скриншоты DLP-инцидентов (если `incidentCapture.screenshotEnabled=true`).
- `C:\ProgramData\AWatch-rus\launch-watchers.ps1` — per-user launcher.
- `C:\ProgramData\AWatch-rus\recovery-loop.ps1` — system recovery loop.
- `C:\ProgramData\AWatch-rus\browser-domains-native-collector.ps1` — runtime collector.
- `C:\ProgramData\AWatch-rus\dlp-endpoint-signals-collector.ps1` — runtime endpoint collector.
- `C:\ProgramData\AWatch-rus\dlp-policy.json` — активная DLP-политика.
- `C:\ProgramData\AWatch-rus\logs\` — логи collector'а.
Для phased rollout те же файлы формируются в каталоге `StateRoot`, переданном параметром.
При переопределении `StateRoot` те же файлы формируются в указанном каталоге.
## Повторный прогон
- Скрипты идемпотентны: переустанавливают задачи и обновляют runtime-файлы.
- Предыдущая установка ActivityWatch бэкапится в `C:\ProgramData\ActivityWatch\backups\install-YYYYMMDD-HHMMSS`.
- Предыдущая установка ActivityWatch бэкапится в `C:\ProgramData\AWatch-rus\backups\install-YYYYMMDD-HHMMSS`.
- Для жёсткого восстановления запускайте `windows/hardening-recovery.ps1`.
+6 -6
View File
@@ -17,15 +17,15 @@
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
C:\Deploy\AWatch-rus\windows\deploy-ensemble.ps1 `
C:\Program Files\AWatch-rus\windows\deploy-ensemble.ps1 `
-ServerHost 10.10.10.13 `
-ServerPort 5600 `
-Domain SHARKON2025 `
-Users user1,user2,user3,user4,user5 `
-InstallRoot 'C:\Program Files\ActivityWatch-Phase2' `
-StateRoot 'C:\ProgramData\ActivityWatch-Phase2' `
-InstallRoot 'C:\Program Files\AWatch-rus\bin' `
-StateRoot 'C:\ProgramData\AWatch-rus' `
-AfkEnabled:$false `
-CustomPolicyPath C:\Deploy\AWatch-rus\windows\dlp-policy.example.json `
-CustomPolicyPath C:\Program Files\AWatch-rus\windows\dlp-policy.example.json `
-ValidateAfterDeploy
```
@@ -42,8 +42,8 @@ C:\Deploy\AWatch-rus\windows\deploy-ensemble.ps1 `
## Быстрый health-check
```powershell
$report = C:\Deploy\AWatch-rus\windows\validate-deployment.ps1 `
-ConfigPath C:\ProgramData\ActivityWatch-Phase2\deployment-config.json
$report = C:\Program Files\AWatch-rus\windows\validate-deployment.ps1 `
-ConfigPath C:\ProgramData\AWatch-rus\deployment-config.json
$report | ConvertTo-Json -Depth 12
```
@@ -0,0 +1,7 @@
# Moved
Документ перенесён в новую структуру install-kit:
- `windows/installkit/innosetup/innosetup-rdp-package-filelist.md`
Этот файл оставлен как совместимый указатель, чтобы не ломать существующие ссылки в документации/автоматизации.
+3 -3
View File
@@ -23,7 +23,7 @@ Start-ScheduledTask -TaskName 'ActivityWatch Launch [CONTOSO_user01]'
- Скрипт работает через UI Automation и foreground window.
- Некоторые браузеры/страницы могут скрывать адресную строку или блокировать UIA.
- Проверьте лог `C:\ProgramData\ActivityWatch\logs\browser-domains-<user>.log`.
- Проверьте лог `C:\ProgramData\AWatch-rus\logs\browser-domains-<user>.log`.
- Убедитесь, что активное окно — поддерживаемый браузер: Edge, Chrome, Brave, Vivaldi, Opera, Firefox.
### Сервер недоступен
@@ -44,7 +44,7 @@ Invoke-WebRequest http://aw.example.local:5600/api/0/info
### Неправильная категоризация домена
- Проверьте содержимое `C:\ProgramData\ActivityWatch\web-category-rules.json`.
- Проверьте содержимое `C:\ProgramData\AWatch-rus\web-category-rules.json`.
- Пользовательские правила должны быть валидным JSON.
- Один и тот же домен лучше определять только в одной категории.
- После изменения правил достаточно перезапустить collector или задачу пользователя:
@@ -112,7 +112,7 @@ Get-CimInstance Win32_Process |
Проверить конфиг:
```powershell
Get-Content C:\ProgramData\ActivityWatch\deployment-config.json -Raw
Get-Content C:\ProgramData\AWatch-rus\deployment-config.json -Raw
```
## Когда запускать hardening/recovery
+13 -13
View File
@@ -4,7 +4,7 @@
```powershell
$report = .\windows\validate-deployment.ps1 `
-ConfigPath C:\ProgramData\ActivityWatch\deployment-config.json
-ConfigPath C:\ProgramData\AWatch-rus\deployment-config.json
$report | ConvertTo-Json -Depth 12
```
@@ -19,11 +19,11 @@ $report | ConvertTo-Json -Depth 12
### 1. Проверить установленные файлы
```powershell
Test-Path 'C:\Program Files\ActivityWatch\aw-watcher-afk\aw-watcher-afk.exe'
Test-Path 'C:\Program Files\ActivityWatch\aw-watcher-window\aw-watcher-window.exe'
Test-Path 'C:\ProgramData\ActivityWatch\browser-domains-native-collector.ps1'
Test-Path 'C:\ProgramData\ActivityWatch\dlp-policy.json'
Test-Path 'C:\ProgramData\ActivityWatch\deployment-config.json'
Test-Path 'C:\Program Files\AWatch-rus\bin\aw-watcher-afk\aw-watcher-afk.exe'
Test-Path 'C:\Program Files\AWatch-rus\bin\aw-watcher-window\aw-watcher-window.exe'
Test-Path 'C:\ProgramData\AWatch-rus\browser-domains-native-collector.ps1'
Test-Path 'C:\ProgramData\AWatch-rus\dlp-policy.json'
Test-Path 'C:\ProgramData\AWatch-rus\deployment-config.json'
```
Ожидаемый результат — везде `True`.
@@ -50,7 +50,7 @@ Get-ScheduledTask | Where-Object TaskName -eq 'ActivityWatch Recovery'
### 2.1 Проверить incidentCapture в конфиге
```powershell
$cfg = Get-Content 'C:\ProgramData\ActivityWatch\deployment-config.json' -Raw | ConvertFrom-Json
$cfg = Get-Content 'C:\ProgramData\AWatch-rus\deployment-config.json' -Raw | ConvertFrom-Json
$cfg.incidentCapture
```
@@ -119,7 +119,7 @@ Invoke-WebRequest http://aw.example.local:5600/api/0/buckets | Select-Object -Ex
4. Проверьте локальный лог:
```powershell
Get-Content "C:\ProgramData\ActivityWatch\logs\dlp-incidents-$env:USERNAME.log" -Tail 50
Get-Content "C:\ProgramData\AWatch-rus\logs\dlp-incidents-$env:USERNAME.log" -Tail 50
```
Если `screenshotEnabled = True`, проверьте наличие скриншота в инциденте:
@@ -175,7 +175,7 @@ Invoke-RestMethod -Method Post `
- в API появился `ruleId=selftest-dlp-incident`;
- в UI (`#/buckets/aw-dlp-incidents_<hostname>`) событие видно в `Events`.
## Проверка phase-2 endpoint signals
## Проверка endpoint signals
1. Скопируйте любой текст в буфер обмена.
2. Отправьте тестовую печать (любой принтер/виртуальный PDF).
@@ -188,7 +188,7 @@ Invoke-WebRequest http://aw.example.local:5600/api/0/buckets/aw-dlp-endpoint-sig
4. Проверьте локальный лог:
```powershell
Get-Content "C:\ProgramData\ActivityWatch\logs\endpoint-signals-$env:USERNAME.log" -Tail 50
Get-Content "C:\ProgramData\AWatch-rus\logs\endpoint-signals-$env:USERNAME.log" -Tail 50
```
## Проверка восстановления
@@ -206,9 +206,9 @@ Start-ScheduledTask -TaskName 'ActivityWatch Recovery'
## Проверка ACL
```powershell
icacls 'C:\Program Files\ActivityWatch'
icacls 'C:\ProgramData\ActivityWatch'
icacls 'C:\ProgramData\ActivityWatch\logs'
icacls 'C:\Program Files\AWatch-rus\bin'
icacls 'C:\ProgramData\AWatch-rus'
icacls 'C:\ProgramData\AWatch-rus\logs'
```
Ожидаемо:
+25
View File
@@ -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.
@@ -1,17 +1,18 @@
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
0754dcba7c651d67a40e09446868d2fcae623a100d4fb01794dd96272d353b49 install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt
089595753398c8b82980919d230dafac548c3ba36817f5c96a68051582f9faa3 install-kit-awindows-20260427-211240/ansible/README.md
412bb766bbf0791c3593f38daa771d5d0aa58cc1f2d3c9010fcd4588d0fe87df install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml
90ac38a33918fcd3620f078f51fbf7c6a9d7f8fd1a34d16b38cb3ac45678b0d7 install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml
a649eeb57472fb259d248983c486f0474bfb7317600feb98f76a80e7af7e4549 install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml
531bfec24f86d28a06e5c0d73005489a818e2c7b1cce1d98f524a3f76802b8ee 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
7c468f252e328fd3bb7ee776a45feea88efc4b438dfef55b832da4eea867aaf2 install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml
195e7dbdb91f4e77db3263bd0812301ddc912a37ba1519688768bb64a2887567 install-kit-awindows-20260427-211240/ansible/install_full_stack.yml
2e4e94d90143923fefd3ec1257d0ec57daa3e96450d85471bc2c418aae37e105 install-kit-awindows-20260427-211240/ansible/inventory.example.ini
d9e43352fd6bdb647db9754ab2c557b6bb27f88f51b2bf23d8c19227e535e7b9 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml
f3d34547f345ad1c635ee44613a60e7f08479f9ae4d1111810bc7e5968bfb084 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml
ef4ed198745777bd1227170241b2db21dc853685f962d6116241c55216b466d6 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
@@ -21,17 +22,19 @@ a50dbadbf619342c2178e255b68f69a36503756daf80eedd9170311c63964f2e install-kit-aw
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
1654cf688560465fcce629468a0be869b0c81b056179e1b5e9bcc7a2d5ed6ce0 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-admin.deployment-config.json
ac022b9a074c542ade66d18af8db385f6c14376ac4eadd54ef033dfa7f60fb50 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-u2u5.deployment-config.json
98cf4c54d494318b74cfbd3c8892830a34928bd76a2296d5333e5e176c1f3f49 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-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
0cb9cd8d8b612429f79899c126f4141ab4fbbb6d919425c0b8fd8d0a74b1bd44 install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1
7db2d3767ae81c877e8f04ecbc77a2fc26b2d9bbbf77d0e774fba0a7956bd0a6 install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1
b497400a1ba57cddf28dc8e217115dc85eccb67150cbdbb6a81abd804ed20109 install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1
973db51854fc744539a7b75e13c6749e822a08a79f47447485611f07e8f902a8 install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1
0d66dcb551889e6b7bc21b29d53b77e46f41d61dd2e4e0d9913dbf0f8bd5eb18 install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1
8857f3e17f3f3ed6f211ce7f0a0c46c586a2548541078401ee3befaa20924b3d 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
f03886caf56c6838e8a163d6b48d1f229e83a5682aeeb447c3a65f13d62dbca4 install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1
71911cd53ad0abd8bf83994f8a79bbbfe2c0eaf4f4c5f6c2d636dd7786191c2f install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1
5dcf249742bd82fa0c803c878bfa0a1344b7b14df85c05f12e8c205663aea158 install-kit-awindows-20260427-211240/windows/validate-deployment.ps1
731098681d89b9af6f3872abd586ac3b1faba2d7f9340211e503f52ad0243b3f install-kit-awindows-20260427-211240/windows/web-category-rules.example.json
41171f0d7ed1e8b00dd0faf1a4b75c9cb063fd09aba8d333851a7a31fb297de1 install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1
@@ -4,7 +4,7 @@ Includes:
- windows/* (deploy scripts, collectors, common module, configs/examples)
- 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)
- server-configs-192.168.100.21/* (working Windows/RDP config snapshots)
Source:
- Local project snapshot at build time.
@@ -1,82 +1,102 @@
# Ansible ensemble for AWatch-rus
Эта директория содержит Ansible-ensemble для двух сценариев:
Эта директория содержит Ansible-ensemble для полного развёртывания AWatch-rus:
- деплой на уже существующий Debian host/CT;
- полный цикл с нуля в Proxmox: создание CT + bootstrap + установка ActivityWatch + RU patch.
- централизованный деплой Windows phase-2 collectors по WinRM.
- deployment внешнего pfSense poller'а на Debian/Ubuntu utility VM.
- полный цикл с нуля в Proxmox: создание CT + bootstrap + установка ActivityWatch + RU patch;
- централизованное развёртывание Windows/RDP collector'ов по WinRM;
- развёртывание внешнего pfSense poller'а на Debian/Ubuntu utility VM.
## Файлы
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_server.yml` — основной playbook.
- `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_and_deploy_aw.yml`full-stack playbook для Proxmox.
- `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml` — массовый full-stack playbook (несколько CT).
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_windows_phase2.yml` — WinRM playbook для развёртывания phase-2 Windows collector'ов.
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_pfsense_poller.yml`deployment pfSense poller'а.
- `/home/igor/tmp/AWatch-rus/ansible/inventory.example.ini` — шаблон inventory.
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/all.example.yml` — шаблон переменных.
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox.example.yml` — шаблон переменных CT в Proxmox.
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox-matrix.example.yml` — шаблон матрицы CT.
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/windows.example.yml` — шаблон переменных Windows phase-2.
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.example.yml` — шаблон переменных pfSense poller'а.
- `ansible/deploy_aw_server.yml` — основной playbook для уже существующего Debian/CT host.
- `ansible/provision_proxmox_ct_and_deploy_aw.yml`полный playbook для Proxmox.
- `ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml` — массовый полный playbook (несколько CT).
- `ansible/deploy_aw_windows.yml` — WinRM playbook для развёртывания Windows/RDP collector'ов.
- `ansible/deploy_aw_pfsense_poller.yml`развёртывание pfSense poller'а.
- `ansible/install_full_stack.yml` — полный установочный playbook (оркестратор всех этапов).
- `ansible/inventory.example.ini` — шаблон inventory.
- `ansible/group_vars/*.example.yml` — шаблоны переменных.
## Быстрый запуск
1. Скопируйте шаблоны:
- `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/all.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/all.yml`
- `cp ansible/inventory.example.ini ansible/inventory.ini`
- `cp ansible/group_vars/all.example.yml ansible/group_vars/all.yml`
2. Заполните значения в `inventory.ini` и `group_vars/all.yml`.
3. Запустите:
```bash
cd /home/igor/tmp/AWatch-rus/ansible
cd ansible
ansible-playbook -i inventory.ini deploy_aw_server.yml
```
## Полный установочный playbook (всё за один запуск)
Если нужно прогнать полный цикл одной командой:
```bash
cd 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.yml` (группа `[aw_windows]`);
- `deploy_aw_pfsense_poller.yml` (группа `[aw_pfsense_pollers]`).
Пустые группы в `inventory.ini` безопасны: соответствующий play будет пропущен.
## Полный запуск с нуля в Proxmox
1. Подготовьте inventory и vars:
- `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/all.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/all.yml`
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox.yml`
- `cp ansible/inventory.example.ini ansible/inventory.ini`
- `cp ansible/group_vars/all.example.yml ansible/group_vars/all.yml`
- `cp ansible/group_vars/proxmox.example.yml ansible/group_vars/proxmox.yml`
2. Заполните `group_vars/proxmox.yml` и `group_vars/all.yml`.
3. Запустите playbook:
```bash
cd /home/igor/tmp/AWatch-rus/ansible
cd ansible
ansible-playbook -i inventory.ini provision_proxmox_ct_and_deploy_aw.yml
```
## Массовый запуск (матрица CT)
1. Подготовьте матрицу:
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox-matrix.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox-matrix.yml`
- `cp ansible/group_vars/proxmox-matrix.example.yml ansible/group_vars/proxmox-matrix.yml`
2. Заполните `proxmox-matrix.yml`.
3. Запустите:
```bash
cd /home/igor/tmp/AWatch-rus/ansible
cd ansible
ansible-playbook -i inventory.ini provision_proxmox_ct_matrix_and_deploy_aw.yml
```
## Windows phase-2 rollout (WinRM)
## Windows/RDP rollout (WinRM)
1. Подготовьте inventory и vars:
- `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`
- `cp ansible/inventory.example.ini ansible/inventory.ini`
- `cp ansible/group_vars/windows.example.yml 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
cd /home/igor/tmp/AWatch-rus/ansible
ansible-playbook -i inventory.ini deploy_aw_windows_phase2.yml
cd ansible
ansible-playbook -i inventory.ini deploy_aw_windows.yml
```
Playbook:
- выгружает `windows/*` toolkit на целевой хост в `C:\Deploy\AWatch-rus\windows`;
- выполняет `deploy-ensemble.ps1` (deploy + hardening/recovery) с phase-2 policy/rules;
- выгружает полный `windows/*` toolkit на целевой хост в InnoSetup-compatible каталог `C:\Program Files\AWatch-rus\windows`, включая DLP и `worktime-session-collector.ps1`;
- если найден legacy config `C:\ProgramData\ActivityWatch-Phase2\deployment-config.json`, выполняет безопасную миграцию через `migrate-awatch-rus-paths.ps1`: backup, остановка задач, перенос данных, переписывание путей, пересоздание scheduled tasks и validation;
- выполняет `deploy-ensemble.ps1` (deploy + hardening/recovery) с policy/rules из AWatch-rus toolkit;
- после deploy принудительно запускает `ActivityWatch Recovery` и все `ActivityWatch Launch *` задачи;
- выполняет API smoke-check bucket `aw-watcher-afk_<COMPUTERNAME>` и ожидает свежие `not-afk` события;
- запускает `validate-deployment.ps1`;
- забирает JSON-отчёт в локальную директорию (`/tmp/aw-rus-validation` по умолчанию).
@@ -87,17 +107,27 @@ Playbook:
- `aw_windows_incident_capture_enabled: false` — отключить блок incidentCapture;
- `aw_windows_incident_screenshot_enabled: false` — не делать скриншот при DLP-инциденте;
- `aw_windows_incident_artifacts_root: 'C:\...\incident-artifacts'` — переопределить путь артефактов;
- `aw_windows_deploy_root: 'C:\Program Files\AWatch-rus'` — каталог toolkit, совпадает с InnoSetup `{app}`;
- `aw_windows_install_root: 'C:\Program Files\AWatch-rus\bin'` — каталог бинарников, совпадает с InnoSetup `AwDefaultInstallRoot`;
- `aw_windows_state_root: 'C:\ProgramData\AWatch-rus'` — каталог состояния/отчётов, совпадает с InnoSetup `AwDefaultStateRoot`;
- `aw_windows_validation_remote_path: '{{ aw_windows_state_root }}\aw_validate_ansible.json'` — отчёт Ansible-валидации хранится рядом с `ensemble-report-*.json`;
- `aw_windows_migration_enabled: true` — включить guard миграции текущего production из `ActivityWatch-Phase2` в единый `AWatch-rus`;
- `aw_windows_legacy_install_root` / `aw_windows_legacy_state_root` — старые production paths, откуда выполняется перенос;
- `aw_windows_migration_report_remote_path` — JSON-отчёт о миграции на Windows-хосте;
- `aw_windows_package_version`, `aw_windows_package_url`, `aw_windows_package_zip_path` — версия и источник Windows-пакета ActivityWatch;
- `aw_windows_api_smoke_check_bucket: ""` — автоматически использовать `aw-watcher-afk_<COMPUTERNAME>`;
- `aw_windows_fail_on_validation_error: true` — завершать playbook ошибкой, если `validate-deployment.ps1` возвращает `overallOk=false`;
- `aw_windows_skip_hardening: true` — пропустить `hardening-recovery.ps1` внутри ensemble-скрипта.
## pfSense poller rollout
## Развёртывание pfSense poller
1. Подготовьте vars:
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.yml`
- `cp ansible/group_vars/pfsense-poller.example.yml ansible/group_vars/pfsense-poller.yml`
2. Добавьте inventory group `[aw_pfsense_pollers]`.
3. Запустите:
```bash
cd /home/igor/tmp/AWatch-rus/ansible
cd ansible
ansible-playbook -i inventory.ini deploy_aw_pfsense_poller.yml
```
@@ -116,4 +146,6 @@ Playbook:
- Для Web UI используется checksum-based cache-bust для `ru-patch-v5.js` и `sw-cleanup.js`, чтобы браузер не держал старую DLP/русскую статику после деплоя.
- На `#/home` Web UI делит хосты на `Windows RDP` и `Virtual servers + Proxmox`.
- Выполнена валидация API `http://127.0.0.1:5600/api/0/info`.
- Для full-stack сценария CT создаётся автоматически через `pct create`.
- Для полного сценария CT создаётся автоматически через `pct create`.
- На Windows/RDP host развёрнуты AFK/window watchers, browser domain collector, DLP endpoint collector и worktime session collector.
- Проверочный JSON-отчёт Windows playbook должен иметь `overallOk=true`.
@@ -1,5 +1,5 @@
---
- name: Deploy pfSense ActivityWatch poller
- name: Развернуть pfSense ActivityWatch poller
hosts: aw_pfsense_pollers
become: true
gather_facts: true
@@ -10,14 +10,14 @@
aw_pfsense_service_name: "aw-pfsense-poller.service"
tasks:
- name: Install required packages
- name: Установить обязательные пакеты
ansible.builtin.apt:
name:
- python3
state: present
update_cache: true
- name: Ensure directories exist
- name: Создать каталоги
ansible.builtin.file:
path: "{{ item }}"
state: directory
@@ -26,29 +26,29 @@
- "{{ aw_pfsense_install_root }}"
- "{{ aw_pfsense_config_dir }}"
- name: Install pfSense poller script
- name: Установить скрипт pfSense poller
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
- name: Установить 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
- Перезагрузить systemd
- name: Write pfSense poller config
- name: Записать конфигурацию pfSense poller
ansible.builtin.copy:
dest: "{{ aw_pfsense_config_dir }}/poller.json"
mode: "0600"
content: "{{ aw_pfsense_poller_config | to_nice_json }}"
notify:
- Restart pfSense poller
- Перезапустить pfSense poller
- name: Enable and start pfSense poller
- name: Включить и запустить pfSense poller
ansible.builtin.systemd:
name: "{{ aw_pfsense_service_name }}"
enabled: true
@@ -56,11 +56,11 @@
daemon_reload: true
handlers:
- name: Reload systemd
- name: Перезагрузить systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart pfSense poller
- name: Перезапустить pfSense poller
ansible.builtin.systemd:
name: "{{ aw_pfsense_service_name }}"
state: restarted
@@ -1,5 +1,5 @@
---
- name: Deploy AWatch-rus server
- name: Развернуть сервер AWatch-rus
hosts: aw_server
become: true
gather_facts: true
@@ -9,22 +9,29 @@
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_release_install_dir: "{{ aw_release_root }}/aw-server-rust-{{ aw_server_version }}"
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
- name: Установить базовые пакеты
ansible.builtin.apt:
name:
- curl
- rsync
- unzip
state: present
update_cache: true
- name: Ensure service account exists
- name: Создать системную группу сервиса
ansible.builtin.group:
name: "{{ aw_server_group }}"
system: true
state: present
- name: Создать системную учётную запись сервиса
ansible.builtin.user:
name: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
@@ -33,7 +40,25 @@
system: true
create_home: false
- name: Ensure required directories
- name: Создать обязательные каталоги
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ aw_release_root }}"
- "{{ aw_release_dir }}"
- "{{ aw_release_install_dir }}"
- /opt/activitywatch
- /opt/activitywatch/bin
- "{{ aw_server_webui_dir }}"
- "{{ aw_server_webui_dir }}/js"
- "{{ aw_server_data_dir }}"
- "{{ aw_server_log_dir }}"
- /etc/activitywatch
- "{{ aw_bootstrap_dir }}"
- name: Настроить каталоги ActivityWatch с владельцем сервиса
ansible.builtin.file:
path: "{{ item }}"
state: directory
@@ -41,103 +66,188 @@
group: "{{ aw_server_group }}"
mode: "0755"
loop:
- /opt/activitywatch
- /opt/activitywatch/bin
- "{{ aw_release_root }}"
- "{{ aw_release_dir }}"
- "{{ aw_release_install_dir }}"
- "{{ aw_server_webui_dir }}"
- "{{ aw_server_webui_dir }}/js"
- "{{ aw_server_data_dir }}"
- "{{ aw_server_log_dir }}"
- /etc/activitywatch
- "{{ aw_bootstrap_dir }}"
- name: Download ActivityWatch release archive
- name: Скачать архив релиза ActivityWatch
ansible.builtin.get_url:
url: "{{ aw_server_download_url }}"
dest: "{{ aw_archive_path }}"
mode: "0644"
- name: Unpack ActivityWatch release
- name: Распаковать релиз ActivityWatch
ansible.builtin.unarchive:
src: "{{ aw_archive_path }}"
dest: "{{ aw_release_dir }}"
remote_src: true
extra_opts: ["-o"]
- name: Discover extracted AW directory
- name: Найти распакованный каталог ActivityWatch
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: Найти бинарный файл AW server
ansible.builtin.find:
paths: "{{ aw_release_dir }}"
file_type: file
patterns:
- aw-server-rust
- aw-server
register: aw_server_binary_find
- name: Verify extracted directory exists
- name: Найти каталог WebUI
ansible.builtin.find:
paths: "{{ aw_release_dir }}"
file_type: directory
patterns:
- aw-webui
- webui
register: aw_webui_dir_find
- name: Сохранить пути распакованного релиза
ansible.builtin.set_fact:
aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
aw_server_binary_path: "{{ (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
aw_webui_source_path: "{{ (aw_webui_dir_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
- name: Проверить, что компоненты релиза найдены
ansible.builtin.assert:
that:
- aw_release_extracted is defined
- aw_release_extracted | length > 0
fail_msg: "Cannot locate extracted ActivityWatch release directory."
- aw_server_binary_path is defined
- aw_server_binary_path | length > 0
- aw_webui_source_path is defined
- aw_webui_source_path | length > 0
fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch."
- name: Sync release content to /opt/activitywatch
- name: Создать каталог установленного релиза
ansible.builtin.file:
path: "{{ aw_release_install_dir }}"
state: directory
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0755"
- name: Установить бинарный файл AW server
ansible.builtin.copy:
remote_src: true
src: "{{ aw_server_binary_path }}"
dest: "{{ aw_release_install_dir }}/aw-server-rust"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0755"
- name: Создать ссылку на активный бинарный файл AW server
ansible.builtin.file:
src: "{{ aw_release_install_dir }}/aw-server-rust"
dest: /opt/activitywatch/bin/aw-server-rust
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
state: link
force: true
- name: Синхронизировать WebUI в RU каталог
ansible.builtin.command:
cmd: "rsync -a --delete {{ aw_release_extracted }}/ /opt/activitywatch/"
cmd: "rsync -a {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/"
- name: Copy bootstrap files from repository
- name: Настроить владельца файлов /opt/activitywatch
ansible.builtin.file:
path: /opt/activitywatch
state: directory
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
recurse: true
- name: Установить systemd service из шаблона репозитория
ansible.builtin.copy:
dest: /etc/systemd/system/activitywatch-server.service
mode: "0644"
content: >-
{{
lookup('file', aw_repo_root + '/aw-server/activitywatch-server.service')
| replace('__AW_SERVER_USER__', aw_server_user)
| replace('__AW_SERVER_GROUP__', aw_server_group)
| replace('__AW_SERVER_DATA_DIR__', aw_server_data_dir)
}}
notify:
- Перезагрузить systemd
- Перезапустить activitywatch
- name: Скопировать RU patch файлы WebUI из репозитория
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "{{ item.mode }}"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
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: Проверить наличие index.html после копирования
ansible.builtin.stat:
path: "{{ aw_server_webui_dir }}/index.html"
register: aw_webui_ru_index
- name: Insert RU patch scripts into index.html
- name: Проверить, что index.html доступен для RU patch
ansible.builtin.assert:
that:
- aw_webui_ru_index.stat.exists
fail_msg: "Не найден index.html WebUI для применения RU patch."
- name: Удалить старые теги RU patch из index.html
ansible.builtin.replace:
path: "{{ aw_server_webui_dir }}/index.html"
regexp: '<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*></script>'
replace: ''
- name: Добавить cleanup script RU patch в 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
- name: Добавить загрузчик RU patch перед закрытием body
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
- name: Записать /etc/activitywatch/aw-server.env
ansible.builtin.copy:
dest: /etc/activitywatch/aw-server.env
mode: "0640"
owner: root
group: root
content: |
AW_SERVER_HOST={{ aw_server_bind_host }}
AW_SERVER_BIND_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_DATA_DIR={{ aw_server_data_dir }}
AW_SERVER_LOG_DIR={{ aw_server_log_dir }}
AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }}
AW_SERVER_USER={{ aw_server_user }}
AW_SERVER_GROUP={{ aw_server_group }}
- name: Enable and start service
- name: Включить и запустить сервис
ansible.builtin.systemd:
name: activitywatch-server.service
enabled: true
state: restarted
daemon_reload: true
- name: Wait for API
- name: Дождаться ответа API
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info"
method: GET
@@ -147,25 +257,25 @@
delay: 3
until: aw_api.status == 200
- name: Apply baseline worktime settings (classes)
- name: Применить базовые 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
status_code: [200, 201]
when: aw_apply_worktime_settings | default(false) | bool
- name: Apply baseline views (include DLP and worktime)
- name: Применить базовые views для DLP и 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
status_code: [200, 201]
when: aw_apply_worktime_settings | default(false) | bool
- name: Derive worktime durationDefault from aw_worktime_from/to
- name: Вычислить worktime durationDefault из 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 }}"
@@ -182,7 +292,7 @@
}}
when: aw_apply_worktime_settings | default(false) | bool
- name: Normalize derived durationDefault for overnight shifts
- name: Нормализовать durationDefault для ночных смен
ansible.builtin.set_fact:
aw_worktime_duration_default_effective: >-
{{
@@ -192,15 +302,15 @@
}}
when: aw_apply_worktime_settings | default(false) | bool
- name: Validate derived durationDefault is sane
- name: Проверить корректность durationDefault
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 }}"
fail_msg: "Некорректный интервал рабочего времени: {{ aw_worktime_from }}..{{ aw_worktime_to }}"
when: aw_apply_worktime_settings | default(false) | bool
- name: Apply baseline worktime period (startOfDay)
- name: Применить базовый период worktime (startOfDay)
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/startOfDay"
method: POST
@@ -209,7 +319,7 @@
status_code: 200
when: aw_apply_worktime_settings | default(false) | bool
- name: Apply baseline worktime period (durationDefault seconds)
- name: Применить базовый период worktime (durationDefault seconds)
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/durationDefault"
method: POST
@@ -219,11 +329,11 @@
when: aw_apply_worktime_settings | default(false) | bool
handlers:
- name: Reload systemd
- name: Перезагрузить systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart activitywatch
- name: Перезапустить activitywatch
ansible.builtin.systemd:
name: activitywatch-server.service
state: restarted
@@ -0,0 +1,232 @@
---
- name: Развернуть Windows/RDP collector'ы AWatch-rus
hosts: aw_windows
gather_facts: false
vars:
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
aw_windows_package_zip_path: ""
aw_windows_domain: "SHARKON2025"
aw_windows_users:
- user1
- user2
- user3
- user4
- user5
aw_windows_extra_users: []
aw_windows_users_effective: "{{ (aw_windows_users + aw_windows_extra_users) | unique }}"
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
aw_windows_afk_enabled: true
aw_windows_window_enabled: true
aw_windows_local_agent_logs_enabled: false
aw_windows_incident_capture_enabled: true
aw_windows_incident_screenshot_enabled: true
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
aw_windows_logon_marker_enabled: true
aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_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_windows_api_smoke_check_limit: 10
aw_windows_fail_on_validation_error: true
aw_windows_migration_enabled: true
aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2"
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
tasks:
- name: Проверить обязательные переменные
ansible.builtin.assert:
that:
- aw_windows_server_host is defined
- aw_windows_server_port is defined
- aw_windows_server_scheme is defined
- aw_windows_domain is defined
- aw_windows_users_effective | length > 0
- aw_windows_install_root is defined
- aw_windows_state_root is defined
fail_msg: "Не заданы обязательные переменные Windows-развёртывания."
- name: Создать каталоги развёртывания
ansible.windows.win_file:
path: "{{ item }}"
state: directory
loop:
- "{{ aw_windows_deploy_root }}"
- "{{ aw_windows_deploy_root }}\\windows"
- name: Загрузить Windows toolkit развёртывания
ansible.windows.win_copy:
src: "{{ aw_windows_repo_root }}/windows/{{ item }}"
dest: "{{ aw_windows_deploy_root }}\\windows\\{{ item }}"
loop:
- ActivityWatch.Windows.Common.psd1
- ActivityWatch.Windows.Common.psm1
- browser-domains-native-collector.ps1
- dlp-endpoint-signals-collector.ps1
- worktime-session-collector.ps1
- migrate-awatch-rus-paths.ps1
- deploy-domain-users.ps1
- deploy-ensemble.ps1
- hardening-recovery.ps1
- validate-deployment.ps1
- web-category-rules.example.json
- dlp-policy.example.json
- name: Загрузить список пользователей для доменного развёртывания
ansible.windows.win_copy:
dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
content: |
{% for user in aw_windows_users_effective -%}
{{ user }}
{% endfor -%}
- name: Проверить нужен ли migration с legacy ActivityWatch путей
when: aw_windows_migration_enabled | bool
ansible.windows.win_stat:
path: "{{ aw_windows_legacy_state_root }}\\deployment-config.json"
register: aw_windows_legacy_config
- name: Выполнить безопасную migration legacy prod в AWatch-rus
when:
- aw_windows_migration_enabled | bool
- aw_windows_legacy_config.stat.exists | default(false)
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$result = & "{{ aw_windows_deploy_root }}\windows\migrate-awatch-rus-paths.ps1" `
-OldInstallRoot "{{ aw_windows_legacy_install_root }}" `
-OldStateRoot "{{ aw_windows_legacy_state_root }}" `
-NewInstallRoot "{{ aw_windows_install_root }}" `
-NewStateRoot "{{ aw_windows_state_root }}" `
-ToolkitRoot "{{ aw_windows_deploy_root }}\windows"
$result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_migration_report_remote_path }}" -Encoding utf8
- name: Запустить Windows/RDP ensemble развёртывание
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$params = @{
ServerScheme = "{{ aw_windows_server_scheme }}"
ServerHost = "{{ aw_windows_server_host }}"
ServerPort = {{ aw_windows_server_port }}
Version = "{{ aw_windows_package_version }}"
Domain = "{{ aw_windows_domain }}"
UserListPath = "{{ aw_windows_deploy_root }}\windows\users.txt"
InstallRoot = "{{ aw_windows_install_root }}"
StateRoot = "{{ aw_windows_state_root }}"
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
IncidentArtifactsRoot = "{{ aw_windows_incident_artifacts_root }}"
LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
CustomRulesPath = "{{ aw_windows_rules_path }}"
CustomPolicyPath = "{{ aw_windows_policy_path }}"
}
{% if (aw_windows_package_url | default('') | string | length) > 0 %}
$params.PackageUrl = "{{ aw_windows_package_url }}"
{% endif %}
{% if (aw_windows_package_zip_path | default('') | string | length) > 0 %}
$params.PackageZipPath = "{{ aw_windows_package_zip_path }}"
{% endif %}
{% if aw_windows_skip_hardening | bool %}
$params.SkipHardening = $true
{% endif %}
& "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params
- name: Принудительно запустить ActivityWatch recovery и 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: Получить Windows hostname для AW smoke-check bucket
when:
- aw_windows_api_smoke_check_enabled | bool
- aw_windows_afk_enabled | bool
ansible.windows.win_command: powershell.exe -NoProfile -Command "$env:COMPUTERNAME"
register: aw_windows_hostname_result
changed_when: false
- name: Вычислить AW AFK smoke-check bucket
when:
- aw_windows_api_smoke_check_enabled | bool
- aw_windows_afk_enabled | bool
ansible.builtin.set_fact:
aw_windows_api_smoke_check_bucket_effective: >-
{{
aw_windows_api_smoke_check_bucket
if (aw_windows_api_smoke_check_bucket | default('') | string | length) > 0
else 'aw-watcher-afk_' ~ (aw_windows_hostname_result.stdout | trim)
}}
- name: Дождаться свежих AFK событий на AW server
when:
- aw_windows_api_smoke_check_enabled | bool
- aw_windows_afk_enabled | bool
delegate_to: localhost
ansible.builtin.uri:
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/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: Выполнить валидацию и сохранить отчёт на целевом Windows host
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
$report | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
if ({{ '$true' if (aw_windows_fail_on_validation_error | bool) else '$false' }} -and -not [bool]$report.overallOk) {
throw "Проверка развёртывания ActivityWatch завершилась ошибкой. Отчёт: {{ aw_windows_validation_remote_path }}"
}
- name: Создать локальный каталог для validation reports
ansible.builtin.file:
path: "{{ aw_windows_validation_local_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
- name: Забрать validation report
ansible.builtin.fetch:
src: "{{ aw_windows_validation_remote_path }}"
dest: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
flat: true
- name: Показать путь к отчёту
ansible.builtin.debug:
msg:
- "Windows/RDP развёртывание завершено на {{ inventory_hostname }}."
- "Отчёт проверки: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
@@ -1,133 +0,0 @@
---
- name: Deploy AWatch-rus Windows phase2 collectors
hosts: aw_windows
gather_facts: false
vars:
aw_windows_repo_root: "/home/igor/tmp/AWatch-rus"
aw_windows_deploy_root: "C:\\Deploy\\AWatch-rus"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_domain: "SHARKON2025"
aw_windows_users:
- user1
- user2
- user3
- user4
- user5
aw_windows_extra_users: []
aw_windows_users_effective: "{{ (aw_windows_users + aw_windows_extra_users) | unique }}"
aw_windows_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_state_root: "C:\\ProgramData\\ActivityWatch"
aw_windows_afk_enabled: true
aw_windows_window_enabled: true
aw_windows_local_agent_logs_enabled: false
aw_windows_incident_capture_enabled: true
aw_windows_incident_screenshot_enabled: true
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
aw_windows_logon_marker_enabled: true
aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
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"
tasks:
- name: Validate required variables
ansible.builtin.assert:
that:
- aw_windows_server_host is defined
- aw_windows_server_port is defined
- aw_windows_domain is defined
- aw_windows_users_effective | length > 0
- aw_windows_install_root is defined
- aw_windows_state_root is defined
fail_msg: "Missing required Windows deployment variables."
- name: Ensure deploy directories exist
ansible.windows.win_file:
path: "{{ item }}"
state: directory
loop:
- "{{ aw_windows_deploy_root }}"
- "{{ aw_windows_deploy_root }}\\windows"
- name: Upload Windows deployment toolkit
ansible.windows.win_copy:
src: "{{ aw_windows_repo_root }}/windows/{{ item }}"
dest: "{{ aw_windows_deploy_root }}\\windows\\{{ item }}"
loop:
- ActivityWatch.Windows.Common.psd1
- ActivityWatch.Windows.Common.psm1
- browser-domains-native-collector.ps1
- dlp-endpoint-signals-collector.ps1
- deploy-domain-users.ps1
- deploy-ensemble.ps1
- hardening-recovery.ps1
- validate-deployment.ps1
- web-category-rules.example.json
- dlp-policy.example.json
- name: Upload user list for domain deploy
ansible.windows.win_copy:
dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
content: |
{% for user in aw_windows_users -%}
{{ user }}
{% endfor -%}
{% for user in aw_windows_extra_users -%}
{{ user }}
{% endfor -%}
- name: Run phase2 ensemble deployment
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$params = @{
ServerHost = "{{ aw_windows_server_host }}"
ServerPort = {{ aw_windows_server_port }}
Domain = "{{ aw_windows_domain }}"
UserListPath = "{{ aw_windows_deploy_root }}\windows\users.txt"
InstallRoot = "{{ aw_windows_install_root }}"
StateRoot = "{{ aw_windows_state_root }}"
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
IncidentArtifactsRoot = "{{ aw_windows_incident_artifacts_root }}"
LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
CustomRulesPath = "{{ aw_windows_rules_path }}"
CustomPolicyPath = "{{ aw_windows_policy_path }}"
}
{% if aw_windows_skip_hardening | bool %}
$params.SkipHardening = $true
{% endif %}
& "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params
- name: Run validation and store report on target
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
$report | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
- name: Ensure local validation directory exists
ansible.builtin.file:
path: "{{ aw_windows_validation_local_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
- name: Fetch validation report
ansible.builtin.fetch:
src: "{{ aw_windows_validation_remote_path }}"
dest: "{{ aw_windows_validation_local_dir }}/"
flat: false
- name: Show report location
ansible.builtin.debug:
msg:
- "Windows phase2 deploy completed on {{ inventory_hostname }}."
- "Validation report: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}/C$/Windows/Temp/aw_validate_phase2_ansible.json"
@@ -8,17 +8,17 @@ aw_server_log_dir: "/var/log/activitywatch"
aw_server_user: "activitywatch"
aw_server_group: "activitywatch"
aw_repo_root: "/home/igor/tmp/AWatch-rus"
aw_repo_root: "{{ playbook_dir | dirname }}"
# Optional: apply a baseline worktime-focused categorization and views via AW settings API.
# WARNING: this overwrites existing server-side settings/classes/views.
# Опционально: применить базовые категории и views для рабочего времени через AW settings API.
# Внимание: это перезаписывает существующие 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.
# Опциональные значения периода рабочего времени в Web UI.
# startOfDay задаёт границу дня и стартовое время окна отчёта.
# durationDefault задаёт диапазон по умолчанию в секундах.
#
# Recommended: set worktime window explicitly and let the playbook derive duration.
# Рекомендуется явно задать рабочий интервал и дать playbook вычислить duration.
aw_worktime_from: "08:00"
aw_worktime_to: "17:00"
aw_worktime_start_of_day: "{{ aw_worktime_from }}"
@@ -1,7 +1,11 @@
aw_windows_repo_root: "/home/igor/tmp/AWatch-rus"
aw_windows_deploy_root: "C:\\Deploy\\AWatch-rus"
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
aw_windows_package_zip_path: ""
aw_windows_domain: "SHARKON2025"
aw_windows_users:
- user1
@@ -14,9 +18,9 @@ aw_windows_extra_users: []
# aw_windows_extra_users:
# - Администратор
# Рекомендуемый изолированный профиль для фазового раската.
aw_windows_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_state_root: "C:\\ProgramData\\ActivityWatch"
# Единые Windows/RDP пути: те же, что использует InnoSetup.
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
aw_windows_afk_enabled: true
aw_windows_window_enabled: true
aw_windows_local_agent_logs_enabled: false
@@ -29,5 +33,18 @@ aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
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_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ansible.json"
aw_windows_validation_local_dir: "/tmp/aw-rus-validation"
aw_windows_fail_on_validation_error: true
# Безопасная миграция текущего прода со старых путей в единый профиль AWatch-rus.
aw_windows_migration_enabled: true
aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2"
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
# По умолчанию AFK bucket вычисляется как aw-watcher-afk_<COMPUTERNAME>.
# Задайте явное значение только если watcher пишет в нестандартный bucket.
aw_windows_api_smoke_check_enabled: true
aw_windows_api_smoke_check_bucket: ""
aw_windows_api_smoke_check_limit: 10
@@ -0,0 +1,16 @@
---
# Полный установщик AWatch-rus.
# Выполняет развёртывание одной командой:
# 1) создание Proxmox CT + bootstrap AW (если в inventory есть [proxmox])
# 2) развёртывание AW server на хостах [aw_server]
# 3) развёртывание Windows/RDP collector'ов на [aw_windows]
# 4) развёртывание pfSense poller'а на [aw_pfsense_pollers]
#
# Примечания:
# - Заполняйте только нужные группы inventory для своего окружения.
# - Play без совпадающих host groups Ansible пропускает автоматически.
- import_playbook: provision_proxmox_ct_and_deploy_aw.yml
- import_playbook: deploy_aw_server.yml
- import_playbook: deploy_aw_windows.yml
- import_playbook: deploy_aw_pfsense_poller.yml
@@ -5,4 +5,8 @@ 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
# Примечание: в русифицированных Windows часто нужен "Администратор", а не "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
[aw_pfsense_pollers]
# pfsense-poller1 ansible_host=192.168.100.30 ansible_user=root ansible_port=22
@@ -1,5 +1,5 @@
---
- name: Provision single Proxmox CT and deploy AWatch-rus
- name: Создать один Proxmox CT и развернуть AWatch-rus
hosts: proxmox
gather_facts: false
@@ -17,7 +17,7 @@
- settings/views-default.json
tasks:
- name: Execute single-CT provisioning workflow
- name: Выполнить workflow создания одного CT
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
vars:
ct_id: "{{ proxmox_ct_id }}"
@@ -1,5 +1,5 @@
---
- name: Provision Proxmox CT matrix and deploy AWatch-rus with RU patch
- name: Создать матрицу Proxmox CT и развернуть AWatch-rus с RU patch
hosts: proxmox
gather_facts: false
@@ -17,14 +17,14 @@
- settings/views-default.json
tasks:
- name: Validate CT matrix is provided
- name: Проверить, что матрица CT задана
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"
fail_msg: "Задайте proxmox_ct_matrix в group_vars/proxmox-matrix.yml"
- name: Execute provisioning workflow for each CT
- name: Выполнить workflow создания для каждого CT
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
vars:
ct_id: "{{ item.id }}"
@@ -1,5 +1,5 @@
---
- name: Validate required per-CT variables
- name: Проверить обязательные переменные CT
ansible.builtin.assert:
that:
- ct_id is defined
@@ -27,14 +27,14 @@
- 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."
fail_msg: "Не заданы обязательные переменные для создания CT и развёртывания."
- name: Build CT network string
- name: Сформировать сетевую строку CT
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
- name: Проверить, существует ли CT
ansible.builtin.command:
argv:
- pct
@@ -44,7 +44,7 @@
failed_when: false
changed_when: false
- name: Create CT when absent
- name: Создать CT, если он отсутствует
ansible.builtin.command:
argv:
- pct
@@ -78,8 +78,9 @@
- --ostype
- debian
when: ct_status_check.rc != 0
no_log: true
- name: Check current CT runtime state
- name: Проверить текущее состояние CT
ansible.builtin.command:
argv:
- pct
@@ -88,7 +89,7 @@
register: ct_runtime_status
changed_when: false
- name: Start CT when stopped
- name: Запустить CT, если он остановлен
ansible.builtin.command:
argv:
- pct
@@ -96,20 +97,23 @@
- "{{ ct_id }}"
when: "'stopped' in ct_runtime_status.stdout"
- name: Ensure bootstrap directory on Proxmox host
- name: Создать bootstrap каталог на Proxmox host
ansible.builtin.file:
path: "{{ proxmox_bootstrap_dir }}"
path: "{{ item }}"
state: directory
mode: "0700"
loop:
- "{{ proxmox_bootstrap_dir }}"
- "{{ proxmox_bootstrap_dir }}/settings"
- name: Copy AW bootstrap files to Proxmox host temp
- name: Скопировать AW bootstrap файлы во временный каталог Proxmox host
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
- name: Установить базовые зависимости ОС внутри CT
ansible.builtin.command:
argv:
- pct
@@ -122,12 +126,16 @@
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
apt-get install -y curl ca-certificates bash unzip xz-utils jq rsync openssh-server python3
mkdir -p /root/bootstrap/settings /etc/activitywatch
systemctl enable ssh || true
systemctl restart ssh || true
register: ct_bootstrap_result
retries: 10
delay: 6
until: ct_bootstrap_result.rc == 0
- name: Push bootstrap files into CT
- name: Передать bootstrap файлы внутрь CT
ansible.builtin.command:
argv:
- pct
@@ -137,7 +145,7 @@
- "/root/bootstrap/{{ item }}"
loop: "{{ aw_bootstrap_files }}"
- name: Write AW server env file on Proxmox host temp
- name: Записать AW server env во временный каталог Proxmox host
ansible.builtin.copy:
dest: "{{ proxmox_bootstrap_dir }}/aw-server.env"
mode: "0600"
@@ -151,8 +159,9 @@
AW_SERVER_LOG_DIR={{ aw_server_log_dir }}
AW_SERVER_USER={{ aw_server_user }}
AW_SERVER_GROUP={{ aw_server_group }}
no_log: true
- name: Push AW server env into CT
- name: Передать AW server env внутрь CT
ansible.builtin.command:
argv:
- pct
@@ -160,8 +169,9 @@
- "{{ ct_id }}"
- "{{ proxmox_bootstrap_dir }}/aw-server.env"
- /etc/activitywatch/aw-server.env
no_log: true
- name: Set mode for env inside CT
- name: Настроить права env файла внутри CT
ansible.builtin.command:
argv:
- pct
@@ -172,7 +182,7 @@
- "0600"
- /etc/activitywatch/aw-server.env
- name: Install server and apply RU patch inside CT
- name: Установить сервер и применить RU patch внутри CT
ansible.builtin.command:
argv:
- pct
@@ -188,7 +198,7 @@
bash /root/bootstrap/apply_webui_ru_patch.sh
systemctl restart activitywatch-server.service
- name: Validate AW API from inside CT
- name: Проверить AW API изнутри CT
ansible.builtin.command:
argv:
- pct
@@ -199,7 +209,7 @@
- -lc
- "curl -fsS http://127.0.0.1:{{ aw_server_port }}/api/0/info >/dev/null"
- name: Validate RU patch hooks in index
- name: Проверить hooks RU patch в index.html
ansible.builtin.command:
argv:
- pct
@@ -210,8 +220,8 @@
- -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
- name: Показать итоговый endpoint
ansible.builtin.debug:
msg:
- "CT {{ ct_id }} is provisioned and configured."
- "ActivityWatch endpoint: http://{{ ct_ip | regex_replace('/[0-9]+$', '') }}:{{ aw_server_port }}"
- "CT {{ ct_id }} создан и настроен."
- "Endpoint ActivityWatch: http://{{ ct_ip | regex_replace('/[0-9]+$', '') }}:{{ aw_server_port }}"
@@ -1,57 +1,57 @@
{
"version": 1,
"generatedAtUtc": "2026-04-27T01:14:21.9184268Z",
"server": {
"host": "10.10.10.13",
"port": 5600,
"scheme": "http"
},
"paths": {
"installRoot": "C:\\Program Files\\ActivityWatch-Phase2-admin",
"stateRoot": "C:\\ProgramData\\ActivityWatch\\phase2-admin",
"logsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\logs",
"collectorScript": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\browser-domains-native-collector.ps1",
"endpointCollectorScript": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\dlp-endpoint-signals-collector.ps1",
"rulesPath": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\web-category-rules.json",
"policyPath": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\dlp-policy.json",
"launchScript": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\launch-watchers.ps1",
"recoveryScript": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\recovery-loop.ps1"
},
"collector": {
"pollSeconds": 5,
"pulseSeconds": 30
},
"collectors": {
"afkEnabled": true,
"windowEnabled": true
},
"logging": {
"localAgentLogsEnabled": false
},
"incidentCapture": {
"enabled": true,
"screenshotEnabled": true,
"artifactsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\incident-artifacts"
},
"sessionEvents": {
"logonEnabled": true,
"bucketPrefix": "aw-session-events"
},
"recovery": {
"intervalSeconds": 180,
"taskName": "ActivityWatch Recovery"
},
"dlp": {
"incidentBucketPrefix": "aw-dlp-incidents",
"enabled": true
},
"package": {
"version": "v0.13.2"
},
"userTasks": [
{
"UserId": "SHARKON2025\\Администратор",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_РђРґРјРёРЅРёСЃС_СЂР_С_РѕСЂ]"
}
]
}
{
"version": 1,
"generatedAtUtc": "2026-04-27T01:14:21.9184268Z",
"server": {
"host": "10.10.10.13",
"port": 5600,
"scheme": "http"
},
"paths": {
"installRoot": "C:\\Program Files\\AWatch-rus\\bin",
"stateRoot": "C:\\ProgramData\\AWatch-rus",
"logsRoot": "C:\\ProgramData\\AWatch-rus\\logs",
"collectorScript": "C:\\ProgramData\\AWatch-rus\\browser-domains-native-collector.ps1",
"endpointCollectorScript": "C:\\ProgramData\\AWatch-rus\\dlp-endpoint-signals-collector.ps1",
"rulesPath": "C:\\ProgramData\\AWatch-rus\\web-category-rules.json",
"policyPath": "C:\\ProgramData\\AWatch-rus\\dlp-policy.json",
"launchScript": "C:\\ProgramData\\AWatch-rus\\launch-watchers.ps1",
"recoveryScript": "C:\\ProgramData\\AWatch-rus\\recovery-loop.ps1"
},
"collector": {
"pollSeconds": 5,
"pulseSeconds": 30
},
"collectors": {
"afkEnabled": true,
"windowEnabled": true
},
"logging": {
"localAgentLogsEnabled": false
},
"incidentCapture": {
"enabled": true,
"screenshotEnabled": true,
"artifactsRoot": "C:\\ProgramData\\AWatch-rus\\incident-artifacts"
},
"sessionEvents": {
"logonEnabled": true,
"bucketPrefix": "aw-session-events"
},
"recovery": {
"intervalSeconds": 180,
"taskName": "ActivityWatch Recovery"
},
"dlp": {
"incidentBucketPrefix": "aw-dlp-incidents",
"enabled": true
},
"package": {
"version": "v0.13.2"
},
"userTasks": [
{
"UserId": "SHARKON2025\\Администратор",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_РђРґРјРёРЅРёСЃС_СЂР_С_РѕСЂ]"
}
]
}
@@ -1,69 +1,69 @@
{
"version": 1,
"generatedAtUtc": "2026-04-27T01:09:42.4193209Z",
"server": {
"host": "10.10.10.13",
"port": 5600,
"scheme": "http"
},
"paths": {
"installRoot": "C:\\Program Files\\ActivityWatch-Phase2-u2u5",
"stateRoot": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5",
"logsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\logs",
"collectorScript": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\browser-domains-native-collector.ps1",
"endpointCollectorScript": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\dlp-endpoint-signals-collector.ps1",
"rulesPath": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\web-category-rules.json",
"policyPath": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\dlp-policy.json",
"launchScript": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\launch-watchers.ps1",
"recoveryScript": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\recovery-loop.ps1"
},
"collector": {
"pollSeconds": 5,
"pulseSeconds": 30
},
"collectors": {
"afkEnabled": true,
"windowEnabled": true
},
"logging": {
"localAgentLogsEnabled": false
},
"incidentCapture": {
"enabled": true,
"screenshotEnabled": true,
"artifactsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\incident-artifacts"
},
"sessionEvents": {
"logonEnabled": true,
"bucketPrefix": "aw-session-events"
},
"recovery": {
"intervalSeconds": 180,
"taskName": "ActivityWatch Recovery"
},
"dlp": {
"incidentBucketPrefix": "aw-dlp-incidents",
"enabled": true
},
"package": {
"version": "v0.13.2"
},
"userTasks": [
{
"UserId": "SHARKON2025\\user2",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user2]"
},
{
"UserId": "SHARKON2025\\user3",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user3]"
},
{
"UserId": "SHARKON2025\\user4",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user4]"
},
{
"UserId": "SHARKON2025\\user5",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user5]"
}
]
}
{
"version": 1,
"generatedAtUtc": "2026-04-27T01:09:42.4193209Z",
"server": {
"host": "10.10.10.13",
"port": 5600,
"scheme": "http"
},
"paths": {
"installRoot": "C:\\Program Files\\AWatch-rus\\bin",
"stateRoot": "C:\\ProgramData\\AWatch-rus",
"logsRoot": "C:\\ProgramData\\AWatch-rus\\logs",
"collectorScript": "C:\\ProgramData\\AWatch-rus\\browser-domains-native-collector.ps1",
"endpointCollectorScript": "C:\\ProgramData\\AWatch-rus\\dlp-endpoint-signals-collector.ps1",
"rulesPath": "C:\\ProgramData\\AWatch-rus\\web-category-rules.json",
"policyPath": "C:\\ProgramData\\AWatch-rus\\dlp-policy.json",
"launchScript": "C:\\ProgramData\\AWatch-rus\\launch-watchers.ps1",
"recoveryScript": "C:\\ProgramData\\AWatch-rus\\recovery-loop.ps1"
},
"collector": {
"pollSeconds": 5,
"pulseSeconds": 30
},
"collectors": {
"afkEnabled": true,
"windowEnabled": true
},
"logging": {
"localAgentLogsEnabled": false
},
"incidentCapture": {
"enabled": true,
"screenshotEnabled": true,
"artifactsRoot": "C:\\ProgramData\\AWatch-rus\\incident-artifacts"
},
"sessionEvents": {
"logonEnabled": true,
"bucketPrefix": "aw-session-events"
},
"recovery": {
"intervalSeconds": 180,
"taskName": "ActivityWatch Recovery"
},
"dlp": {
"incidentBucketPrefix": "aw-dlp-incidents",
"enabled": true
},
"package": {
"version": "v0.13.2"
},
"userTasks": [
{
"UserId": "SHARKON2025\\user2",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user2]"
},
{
"UserId": "SHARKON2025\\user3",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user3]"
},
{
"UserId": "SHARKON2025\\user4",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user4]"
},
{
"UserId": "SHARKON2025\\user5",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user5]"
}
]
}
@@ -1,57 +1,57 @@
{
"version": 1,
"generatedAtUtc": "2026-04-27T01:09:38.9519788Z",
"server": {
"host": "10.10.10.13",
"port": 5600,
"scheme": "http"
},
"paths": {
"installRoot": "C:\\Program Files\\ActivityWatch-Phase2",
"stateRoot": "C:\\ProgramData\\ActivityWatch\\phase2-user1",
"logsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\logs",
"collectorScript": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\browser-domains-native-collector.ps1",
"endpointCollectorScript": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\dlp-endpoint-signals-collector.ps1",
"rulesPath": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\web-category-rules.json",
"policyPath": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\dlp-policy.json",
"launchScript": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\launch-watchers.ps1",
"recoveryScript": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\recovery-loop.ps1"
},
"collector": {
"pollSeconds": 5,
"pulseSeconds": 30
},
"collectors": {
"afkEnabled": true,
"windowEnabled": true
},
"logging": {
"localAgentLogsEnabled": false
},
"incidentCapture": {
"enabled": true,
"screenshotEnabled": true,
"artifactsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\incident-artifacts"
},
"sessionEvents": {
"logonEnabled": true,
"bucketPrefix": "aw-session-events"
},
"recovery": {
"intervalSeconds": 180,
"taskName": "ActivityWatch Recovery"
},
"dlp": {
"incidentBucketPrefix": "aw-dlp-incidents",
"enabled": true
},
"package": {
"version": "v0.13.2"
},
"userTasks": [
{
"UserId": "SHARKON2025\\user1",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user1]"
}
]
}
{
"version": 1,
"generatedAtUtc": "2026-04-27T01:09:38.9519788Z",
"server": {
"host": "10.10.10.13",
"port": 5600,
"scheme": "http"
},
"paths": {
"installRoot": "C:\\Program Files\\AWatch-rus\\bin",
"stateRoot": "C:\\ProgramData\\AWatch-rus",
"logsRoot": "C:\\ProgramData\\AWatch-rus\\logs",
"collectorScript": "C:\\ProgramData\\AWatch-rus\\browser-domains-native-collector.ps1",
"endpointCollectorScript": "C:\\ProgramData\\AWatch-rus\\dlp-endpoint-signals-collector.ps1",
"rulesPath": "C:\\ProgramData\\AWatch-rus\\web-category-rules.json",
"policyPath": "C:\\ProgramData\\AWatch-rus\\dlp-policy.json",
"launchScript": "C:\\ProgramData\\AWatch-rus\\launch-watchers.ps1",
"recoveryScript": "C:\\ProgramData\\AWatch-rus\\recovery-loop.ps1"
},
"collector": {
"pollSeconds": 5,
"pulseSeconds": 30
},
"collectors": {
"afkEnabled": true,
"windowEnabled": true
},
"logging": {
"localAgentLogsEnabled": false
},
"incidentCapture": {
"enabled": true,
"screenshotEnabled": true,
"artifactsRoot": "C:\\ProgramData\\AWatch-rus\\incident-artifacts"
},
"sessionEvents": {
"logonEnabled": true,
"bucketPrefix": "aw-session-events"
},
"recovery": {
"intervalSeconds": 180,
"taskName": "ActivityWatch Recovery"
},
"dlp": {
"incidentBucketPrefix": "aw-dlp-incidents",
"enabled": true
},
"package": {
"version": "v0.13.2"
},
"userTasks": [
{
"UserId": "SHARKON2025\\user1",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user1]"
}
]
}
@@ -5,7 +5,7 @@ function Assert-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated PowerShell session.'
throw 'Запустите этот скрипт из PowerShell с правами администратора.'
}
}
@@ -64,7 +64,7 @@ function Get-ActivityWatchPackageRoot {
Select-Object -First 1
if (-not $afkBinary) {
throw "Cannot find aw-watcher-afk.exe under $ExpandedRoot."
throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot."
}
return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent)
@@ -130,7 +130,7 @@ function Get-ActivityWatchExecutableMap {
foreach ($entry in $map.GetEnumerator()) {
if (-not (Test-Path -LiteralPath $entry.Value)) {
throw "Missing required ActivityWatch binary: $($entry.Value)"
throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)"
}
}
@@ -194,7 +194,7 @@ function Normalize-ActivityWatchUsers {
Sort-Object -Unique
if (-not $normalized -or $normalized.Count -eq 0) {
throw 'No target users resolved. Provide -Users or -UserListPath.'
throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.'
}
return @($normalized)
@@ -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
@@ -413,7 +421,7 @@ function Read-ActivityWatchDeploymentConfig {
)
if (-not (Test-Path -LiteralPath $Path)) {
throw "Deployment config not found: $Path"
throw "Конфигурация развёртывания не найдена: $Path"
}
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
@@ -539,7 +547,7 @@ function Send-LogonMarkerIfNeeded {
`$stateRoot = [string]`$Config.paths.stateRoot
`$markerRoots = New-Object System.Collections.Generic.List[string]
if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) {
`$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'ActivityWatch-Phase2\markers'))
`$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers'))
}
if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) {
`$markerRoots.Add((Join-Path `$stateRoot 'markers'))
@@ -585,7 +593,7 @@ function Send-LogonMarkerIfNeeded {
userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)"
sessionId = `$SessionId
hostname = `$script:Hostname
source = 'launch-watchers-phase2'
source = 'launch-watchers-awatch-rus'
}
} | ConvertTo-Json -Depth 5 -Compress
@@ -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)
@@ -639,11 +648,11 @@ function Start-CollectorScriptIfNeeded {
`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true }
if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) {
throw "Missing aw-watcher-afk.exe: `$afkExe"
throw "Не найден aw-watcher-afk.exe: `$afkExe"
}
if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) {
throw "Missing aw-watcher-window.exe: `$windowExe"
throw "Не найден aw-watcher-window.exe: `$windowExe"
}
if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) {
@@ -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
@@ -850,7 +860,7 @@ function Set-ActivityWatchScheduledTaskAction {
$taskCommand = ('"{0}" {1}' -f $Execute, $Arguments)
& schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "schtasks.exe /Change failed for $TaskName"
throw "schtasks.exe /Change завершился с ошибкой для $TaskName"
}
}
@@ -951,17 +961,17 @@ function Set-ActivityWatchAcl {
& icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "icacls failed for $InstallRoot"
throw "icacls завершился с ошибкой для $InstallRoot"
}
& icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "icacls failed for $StateRoot"
throw "icacls завершился с ошибкой для $StateRoot"
}
& icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "icacls failed for $LogsRoot"
throw "icacls завершился с ошибкой для $LogsRoot"
}
}
@@ -1,6 +1,6 @@
[CmdletBinding()]
param(
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
[string]$ServerHost,
[int]$ServerPort,
[ValidateSet('http', 'https')]
@@ -49,18 +49,18 @@ function Get-DeploymentConfig {
}
$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' }
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' }
$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 }
$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' }
$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\ActivityWatch\web-category-rules.json' }
$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\ActivityWatch\dlp-policy.json' }
$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\AWatch-rus\web-category-rules.json' }
$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' }
$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 }
$resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pulseSeconds } else { 30 }
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\ActivityWatch\logs' }
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' }
$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) }
$resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) }
$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true }
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'ActivityWatch-Phase2\\incident-artifacts' }
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' }
$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true }
if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) {
@@ -158,12 +158,12 @@ function Get-HostFromUrl {
try {
$uri = [Uri]$Url
$host = $uri.Host.ToLowerInvariant()
if ($host.StartsWith('www.')) {
return $host.Substring(4)
$uriHost = $uri.Host.ToLowerInvariant()
if ($uriHost.StartsWith('www.')) {
return $uriHost.Substring(4)
}
return $host
return $uriHost
}
catch {
return $null
@@ -263,11 +263,11 @@ function Load-CustomCategoryRules {
if ($rules.Count -gt 0) {
$script:CategoryRules = @($rules) + @($script:CategoryRules)
Write-CollectorLog ("custom rules loaded: {0}" -f $rules.Count)
Write-CollectorLog ("пользовательские правила загружены: {0}" -f $rules.Count)
}
}
catch {
Write-CollectorLog ("custom rules load failed: {0}" -f $_.Exception.Message)
Write-CollectorLog ("не удалось загрузить пользовательские правила: {0}" -f $_.Exception.Message)
}
}
@@ -338,7 +338,7 @@ function Load-DlpPolicy {
param([string]$Path)
if (-not $Path -or -not (Test-Path -LiteralPath $Path)) {
Write-CollectorLog ("dlp policy not found, disabled: {0}" -f $Path)
Write-CollectorLog ("DLP-политика не найдена, DLP отключен: {0}" -f $Path)
return
}
@@ -372,7 +372,7 @@ function Load-DlpPolicy {
enabled = if ($rule.PSObject.Properties.Name -contains 'enabled') { [bool]$rule.enabled } else { $true }
action = if ($rule.action) { [string]$rule.action } else { [string]$script:DlpDefaults.action }
severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:DlpDefaults.severity }
message = if ($rule.message) { [string]$rule.message } else { "DLP rule matched: $($rule.id)" }
message = if ($rule.message) { [string]$rule.message } else { "Сработало DLP-правило: $($rule.id)" }
cooldownSeconds = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:DlpDefaults.cooldownSeconds }
when = [pscustomobject]@{
domains = if ($when.PSObject.Properties.Name -contains 'domains') { @($when.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() }
@@ -388,10 +388,10 @@ function Load-DlpPolicy {
}
$script:DlpRules = @($loaded)
Write-CollectorLog ("dlp policy loaded: enabled={0}, rules={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count)
Write-CollectorLog ("DLP-политика загружена: включена={0}, правил={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count)
}
catch {
Write-CollectorLog ("dlp policy parse failed: {0}" -f $_.Exception.Message)
Write-CollectorLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message)
}
}
@@ -625,7 +625,7 @@ function Capture-IncidentScreenshot {
}
}
catch {
Write-CollectorLog ("screenshot capture failed: {0}" -f $_.Exception.Message)
Write-CollectorLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message)
return @{}
}
}
@@ -775,7 +775,7 @@ function Send-CategoryHeartbeat {
Load-CustomCategoryRules -Path $resolvedRulesPath
Load-DlpPolicy -Path $resolvedPolicyPath
Write-CollectorLog ("collector started against {0}" -f $script:ApiBase)
Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase)
while ($true) {
try {
@@ -815,7 +815,7 @@ while ($true) {
}
}
catch {
Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message)
Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message)
}
Start-Sleep -Seconds $resolvedPollSeconds
@@ -11,8 +11,8 @@ param(
[string]$Version = 'v0.13.2',
[string]$PackageUrl,
[string]$PackageZipPath,
[string]$InstallRoot = 'C:\Program Files\ActivityWatch',
[string]$StateRoot = 'C:\ProgramData\ActivityWatch',
[string]$InstallRoot = 'C:\Program Files\AWatch-rus\bin',
[string]$StateRoot = 'C:\ProgramData\AWatch-rus',
[int]$PollSeconds = 5,
[int]$PulseSeconds = 30,
[int]$RecoveryIntervalSeconds = 180,
@@ -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 `
@@ -100,8 +103,8 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
Write-Host 'ActivityWatch deployed for users:'
Write-Host 'ActivityWatch развёрнут для пользователей:'
$targetUsers | ForEach-Object { Write-Host " - $_" }
Write-Host "Server: ${ServerScheme}://$ServerHost`:$ServerPort"
Write-Host "State root: $StateRoot"
Write-Host "Policy file: $($assetResult.ActivePolicy)"
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
Write-Host "Каталог данных: $StateRoot"
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
@@ -11,8 +11,8 @@ param(
[string]$Version = 'v0.13.2',
[string]$PackageUrl,
[string]$PackageZipPath,
[string]$InstallRoot = 'C:\Program Files\ActivityWatch',
[string]$StateRoot = 'C:\ProgramData\ActivityWatch',
[string]$InstallRoot = 'C:\Program Files\AWatch-rus\bin',
[string]$StateRoot = 'C:\ProgramData\AWatch-rus',
[int]$PollSeconds = 5,
[int]$PulseSeconds = 30,
[int]$RecoveryIntervalSeconds = 180,
@@ -46,7 +46,7 @@ $hardeningScript = Join-Path $PSScriptRoot 'hardening-recovery.ps1'
$validationScript = Join-Path $PSScriptRoot 'validate-deployment.ps1'
if (-not (Test-Path -LiteralPath $deployScript)) {
throw "Missing script: $deployScript"
throw "Не найден скрипт: $deployScript"
}
& $deployScript `
@@ -118,7 +118,7 @@ $report = [ordered]@{
if ($ValidateAfterDeploy) {
if (-not (Test-Path -LiteralPath $validationScript)) {
throw "Missing script: $validationScript"
throw "Не найден скрипт: $validationScript"
}
$validation = & $validationScript -ConfigPath (Join-Path $StateRoot 'deployment-config.json')
@@ -132,6 +132,6 @@ if ($reportDirectory) {
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8
Write-Host 'ActivityWatch ensemble deploy completed.'
Write-Host "Users: $($resolvedUsers -join ', ')"
Write-Host "Report: $effectiveReportPath"
Write-Host 'Комплексное развёртывание ActivityWatch завершено.'
Write-Host "Пользователи: $($resolvedUsers -join ', ')"
Write-Host "Отчёт: $effectiveReportPath"
@@ -10,8 +10,8 @@ param(
[string]$Version = 'v0.13.2',
[string]$PackageUrl,
[string]$PackageZipPath,
[string]$InstallRoot = 'C:\Program Files\ActivityWatch',
[string]$StateRoot = 'C:\ProgramData\ActivityWatch',
[string]$InstallRoot = 'C:\Program Files\AWatch-rus\bin',
[string]$StateRoot = 'C:\ProgramData\AWatch-rus',
[int]$PollSeconds = 5,
[int]$PulseSeconds = 30,
[int]$RecoveryIntervalSeconds = 180,
@@ -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 `
@@ -98,9 +101,9 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
Write-Host "ActivityWatch deployed for $TargetUser"
Write-Host "Server: ${ServerScheme}://$ServerHost`:$ServerPort"
Write-Host "Install root: $InstallRoot"
Write-Host "State root: $StateRoot"
Write-Host "Rules file: $($assetResult.ActiveRules)"
Write-Host "Policy file: $($assetResult.ActivePolicy)"
Write-Host "ActivityWatch развёрнут для пользователя: $TargetUser"
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
Write-Host "Каталог установки: $InstallRoot"
Write-Host "Каталог данных: $StateRoot"
Write-Host "Файл правил: $($assetResult.ActiveRules)"
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
@@ -1,6 +1,6 @@
[CmdletBinding()]
param(
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
[string]$ServerHost,
[int]$ServerPort,
[ValidateSet('http', 'https')]
@@ -81,7 +81,7 @@ function Send-EndpointSignalHeartbeat {
username = $env:USERNAME
sessionId = $script:SessionId
hostname = $script:Hostname
source = 'endpoint-signals-phase2'
source = 'endpoint-signals-awatch-rus'
} + $Data
} | ConvertTo-Json -Depth 6 -Compress
@@ -122,7 +122,7 @@ function Send-DlpIncidentHeartbeat {
username = $env:USERNAME
sessionId = $script:SessionId
hostname = $script:Hostname
source = 'endpoint-signals-phase2'
source = 'endpoint-signals-awatch-rus'
} + $Data + $captureData
} | ConvertTo-Json -Depth 7 -Compress
@@ -210,7 +210,7 @@ function Capture-IncidentScreenshot {
}
}
catch {
Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message)
Write-EndpointLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message)
return @{}
}
}
@@ -246,7 +246,7 @@ function Load-DlpPolicy {
}
if (-not $Path -or -not (Test-Path -LiteralPath $Path)) {
Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path)
Write-EndpointLog ("DLP-политика не найдена, используются значения по умолчанию: {0}" -f $Path)
return
}
@@ -266,7 +266,7 @@ function Load-DlpPolicy {
}
}
catch {
Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message)
Write-EndpointLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message)
}
}
@@ -319,13 +319,13 @@ function Evaluate-ClipboardRules {
$action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action }
$severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity }
$message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" }
$message = if ($rule.message) { [string]$rule.message } else { "Сработало правило буфера обмена: $ruleId" }
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{
clipboardHash = $ClipboardHash
clipboardLength = $ClipboardText.Length
}
Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2}" -f $ruleId, $action, $severity)
Write-EndpointLog ("инцидент буфера обмена правило={0} действие={1} важность={2}" -f $ruleId, $action, $severity)
}
}
@@ -347,13 +347,13 @@ function Evaluate-UsbRules {
$action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action }
$severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity }
$message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" }
$message = if ($rule.message) { [string]$rule.message } else { "Сработало правило USB-носителя: $ruleId" }
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{
driveLetter = $DriveLetter
volumeName = $VolumeName
}
Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3}" -f $ruleId, $action, $severity, $DriveLetter)
Write-EndpointLog ("инцидент USB правило={0} действие={1} важность={2} диск={3}" -f $ruleId, $action, $severity, $DriveLetter)
}
}
@@ -385,14 +385,14 @@ function Evaluate-PrintRules {
$action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action }
$severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity }
$message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" }
$message = if ($rule.message) { [string]$rule.message } else { "Сработало правило печати: $ruleId" }
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{
printerName = $PrinterName
documentName = $DocumentName
owner = $Owner
}
Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3}" -f $ruleId, $action, $severity, $PrinterName)
Write-EndpointLog ("инцидент печати правило={0} действие={1} важность={2} принтер={3}" -f $ruleId, $action, $severity, $PrinterName)
}
}
@@ -402,6 +402,52 @@ function Test-LooksLikeMojibakeQuestionMarks {
return $Value -match '\?{2,}'
}
function Test-DocumentNameNeedsFallback {
param([AllowNull()][string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) { return $true }
$trimmed = $Value.Trim()
if (Test-LooksLikeMojibakeQuestionMarks -Value $trimmed) { return $true }
if ($trimmed -match '^[0-9]+$') { return $true }
if ($trimmed -match '^(?i)(print document|document|local downlevel document)$') { return $true }
return $false
}
function Get-EventXmlValue {
param(
[Parameter(Mandatory = $true)][xml]$EventXml,
[Parameter(Mandatory = $true)][string]$Name
)
$node = $EventXml.Event.UserData.DocumentPrinted.$Name
if ($null -ne $node) {
return [string]$node
}
return ''
}
function Get-PrintJobPrinterName {
param(
[AllowNull()][string]$JobName,
[AllowNull()][string]$FallbackPrinterName
)
if ([string]::IsNullOrWhiteSpace($JobName)) {
if (-not [string]::IsNullOrWhiteSpace($FallbackPrinterName)) {
return $FallbackPrinterName.Trim()
}
return ''
}
$parts = $JobName -split ',', 2
if ($parts.Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($parts[0])) {
return $parts[0].Trim()
}
return $JobName.Trim()
}
function Normalize-OwnerForMatch {
param([AllowNull()][string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) { return '' }
@@ -469,13 +515,50 @@ function Get-PrintServiceEventSummary {
$propertyValues += [string]$prop.Value
}
$xml = $null
try {
$xml = [xml]$Event.ToXml()
}
catch {
}
$jobId = ''
$documentName = ''
$owner = ''
$portName = ''
$printerName = ''
$sizeBytes = ''
$pageCount = ''
if ($xml) {
$jobId = Get-EventXmlValue -EventXml $xml -Name 'Param1'
$documentName = Get-EventXmlValue -EventXml $xml -Name 'Param2'
$owner = Get-EventXmlValue -EventXml $xml -Name 'Param3'
$portName = Get-EventXmlValue -EventXml $xml -Name 'Param4'
$printerName = Get-EventXmlValue -EventXml $xml -Name 'Param5'
$sizeBytes = Get-EventXmlValue -EventXml $xml -Name 'Param7'
$pageCount = Get-EventXmlValue -EventXml $xml -Name 'Param8'
}
if ([string]::IsNullOrWhiteSpace($jobId) -and $props.Count -ge 1) { $jobId = [string]$props[0].Value }
if ([string]::IsNullOrWhiteSpace($documentName) -and $props.Count -ge 2) { $documentName = [string]$props[1].Value }
if ([string]::IsNullOrWhiteSpace($owner) -and $props.Count -ge 3) { $owner = [string]$props[2].Value }
if ([string]::IsNullOrWhiteSpace($portName) -and $props.Count -ge 4) { $portName = [string]$props[3].Value }
if ([string]::IsNullOrWhiteSpace($printerName) -and $props.Count -ge 5) { $printerName = [string]$props[4].Value }
if ([string]::IsNullOrWhiteSpace($sizeBytes) -and $props.Count -ge 7) { $sizeBytes = [string]$props[6].Value }
if ([string]::IsNullOrWhiteSpace($pageCount) -and $props.Count -ge 8) { $pageCount = [string]$props[7].Value }
[pscustomobject]@{
RecordId = [string]$Event.RecordId
TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' }
PropertyCount = $props.Count
DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' }
Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' }
PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' }
JobId = $jobId
DocumentName = $documentName
Owner = $owner
PortName = $portName
PrinterName = $printerName
SizeBytes = $sizeBytes
PageCount = $pageCount
PropertyValues = $propertyValues
}
}
@@ -488,7 +571,7 @@ function Get-PrintServiceDocumentFallback {
)
$preferred = [string]$EventSummary.DocumentName
if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') {
if (-not (Test-DocumentNameNeedsFallback -Value $preferred)) {
return $preferred
}
@@ -499,9 +582,10 @@ function Get-PrintServiceDocumentFallback {
$candidate = [string]$value
if ([string]::IsNullOrWhiteSpace($candidate)) { continue }
if ($candidate -eq $preferred) { continue }
if ($EventSummary.JobId -and $candidate -eq [string]$EventSummary.JobId) { continue }
if ($Owner -and $candidate -like "*$Owner*") { continue }
if ($PrinterName -and $candidate -like "*$PrinterName*") { continue }
if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue }
if (Test-DocumentNameNeedsFallback -Value $candidate) { continue }
if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') {
$pathCandidates.Add($candidate)
@@ -546,7 +630,7 @@ function Write-PrintServiceEventTrace {
}
Write-EndpointLog (
'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f
'printservice-307 этап={0} recordId={1} время={2} владелец={3} принтер={4} документ={5} итоговыйДокумент={6} свойства=[{7}] причина={8}' -f
$Phase,
$EventSummary.RecordId,
$EventSummary.TimeCreated,
@@ -561,6 +645,7 @@ function Write-PrintServiceEventTrace {
function Get-BetterDocumentNameFromPrintServiceEvents {
param(
[string]$JobId,
[string]$Owner,
[string]$PrinterName
)
@@ -578,32 +663,41 @@ function Get-BetterDocumentNameFromPrintServiceEvents {
$summary = Get-PrintServiceEventSummary -Event $event
$resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName
$jobMatches = if ($JobId) { [string]$summary.JobId -eq [string]$JobId } else { $true }
$ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true }
$printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true }
if ($pass -eq 'strict') {
if ($JobId -and -not $jobMatches) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-jobid-strict' -ResolvedDocument $resolvedDocument
continue
}
if ($Owner -and -not $ownerMatches) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-владельца-strict' -ResolvedDocument $resolvedDocument
continue
}
if ($PrinterName -and -not $printerMatches) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-принтера-strict' -ResolvedDocument $resolvedDocument
continue
}
}
else {
if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument
if ($JobId -and (-not $jobMatches) -and $Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-владельца-и-принтера-relaxed' -ResolvedDocument $resolvedDocument
continue
}
if ((-not $JobId) -and $Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-владельца-и-принтера-relaxed' -ResolvedDocument $resolvedDocument
continue
}
}
if ([string]::IsNullOrWhiteSpace($resolvedDocument)) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument ''
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('нет-кандидата-документа-' + $pass) -ResolvedDocument ''
continue
}
$matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' }
$matchReasonBase = if (Test-DocumentNameNeedsFallback -Value $summary.DocumentName) { 'использован-резервный-вариант' } else { 'напрямую' }
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument
return $resolvedDocument
}
@@ -616,15 +710,15 @@ function Get-BetterDocumentNameFromPrintServiceEvents {
}
$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' }
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' }
$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 }
$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' }
$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\ActivityWatch\dlp-policy.json' }
$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' }
$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 }
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\ActivityWatch\logs' }
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' }
$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) }
$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true }
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'ActivityWatch-Phase2\\incident-artifacts' }
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' }
$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true }
if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) {
@@ -648,7 +742,7 @@ $script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled
$script:ScreenshotTypesLoaded = $false
Load-DlpPolicy -Path $resolvedPolicyPath
Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase)
Write-EndpointLog ("endpoint-коллектор запущен для {0}" -f $script:ApiBase)
while ($true) {
try {
@@ -709,13 +803,13 @@ while ($true) {
if ($script:SeenPrintJob.ContainsKey($jobId)) { continue }
$script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime()
$printerName = [string]$job.Name
$printerName = Get-PrintJobPrinterName -JobName ([string]$job.Name) -FallbackPrinterName ([string]$job.DriverName)
$documentName = [string]$job.Document
$owner = [string]$job.Owner
$documentNameOriginal = $documentName
if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) {
$eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName
if (Test-DocumentNameNeedsFallback -Value $documentName) {
$eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -JobId $jobId -Owner $owner -PrinterName $printerName
if ($eventDocumentName) {
$documentName = $eventDocumentName
}
@@ -726,6 +820,7 @@ while ($true) {
documentName = $documentName
documentNameOriginal = $documentNameOriginal
owner = $owner
printJobId = $jobId
}
Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner
}
@@ -789,7 +884,7 @@ while ($true) {
}
}
catch {
Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message)
Write-EndpointLog ("ошибка коллектора: {0}" -f $_.Exception.Message)
}
Start-Sleep -Seconds $resolvedPollSeconds
@@ -1,6 +1,6 @@
[CmdletBinding()]
param(
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
[string]$ServerHost,
[int]$ServerPort,
[ValidateSet('http', 'https')]
@@ -42,11 +42,11 @@ if (Test-Path -LiteralPath $ConfigPath) {
}
if (-not $existingConfig -and (-not $ServerHost)) {
throw 'deployment-config.json is missing. Provide -ServerHost and user parameters, or run a deploy script first.'
throw 'deployment-config.json отсутствует. Укажите -ServerHost и параметры пользователей либо сначала выполните скрипт развёртывания.'
}
$effectiveStateRoot = if ($StateRoot) { $StateRoot } elseif ($existingConfig) { [string]$existingConfig.paths.stateRoot } else { 'C:\ProgramData\ActivityWatch' }
$effectiveInstallRoot = if ($InstallRoot) { $InstallRoot } elseif ($existingConfig) { [string]$existingConfig.paths.installRoot } else { 'C:\Program Files\ActivityWatch' }
$effectiveStateRoot = if ($StateRoot) { $StateRoot } elseif ($existingConfig) { [string]$existingConfig.paths.stateRoot } else { 'C:\ProgramData\AWatch-rus' }
$effectiveInstallRoot = if ($InstallRoot) { $InstallRoot } elseif ($existingConfig) { [string]$existingConfig.paths.installRoot } else { 'C:\Program Files\AWatch-rus\bin' }
$effectiveLogsRoot = if ($existingConfig) { [string]$existingConfig.paths.logsRoot } else { Join-Path $effectiveStateRoot 'logs' }
$effectiveConfigPath = if ($ConfigPath) { $ConfigPath } else { Join-Path $effectiveStateRoot 'deployment-config.json' }
$effectiveLaunchScript = Join-Path $effectiveStateRoot 'launch-watchers.ps1'
@@ -78,7 +78,7 @@ elseif ($existingConfig) {
@($existingConfig.userTasks | ForEach-Object { [string]$_.userId })
}
else {
throw 'Target users are missing.'
throw 'Не указаны целевые пользователи.'
}
New-ActivityWatchDirectory -Path $effectiveStateRoot
@@ -96,6 +96,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $effectiveInstallRoot | Out-Null
$assetResult = Copy-ActivityWatchCollectorAssets `
-CollectorScriptSource (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') `
-EndpointCollectorScriptSource (Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1') `
-SessionCollectorScriptSource (Join-Path $PSScriptRoot 'worktime-session-collector.ps1') `
-ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') `
-ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') `
-StateRoot $effectiveStateRoot `
@@ -115,6 +116,7 @@ $config = New-ActivityWatchDeploymentConfig `
-LogsRoot $effectiveLogsRoot `
-CollectorScript $effectiveCollector `
-EndpointCollectorScript $effectiveEndpointCollector `
-SessionCollectorScript $effectiveSessionCollector `
-RulesPath $effectiveRules `
-PolicyPath $effectivePolicy `
-PollSeconds $effectivePollSeconds `
@@ -139,6 +141,6 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
Write-Host 'ActivityWatch hardening/recovery completed.'
Write-Host "Config: $effectiveConfigPath"
Write-Host "Users repaired: $($effectiveUsers -join ', ')"
Write-Host 'Укрепление и восстановление ActivityWatch завершены.'
Write-Host "Конфигурация: $effectiveConfigPath"
Write-Host "Пользователи восстановлены: $($effectiveUsers -join ', ')"
@@ -0,0 +1,214 @@
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2',
[string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2',
[string]$NewInstallRoot = 'C:\Program Files\AWatch-rus\bin',
[string]$NewStateRoot = 'C:\ProgramData\AWatch-rus',
[string]$ToolkitRoot = 'C:\Program Files\AWatch-rus\windows',
[switch]$SkipValidation
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1'
Import-Module $modulePath -Force
Assert-Administrator
function Copy-DirectoryContents {
param(
[Parameter(Mandatory = $true)]
[string]$Source,
[Parameter(Mandatory = $true)]
[string]$Destination
)
if (-not (Test-Path -LiteralPath $Source)) {
return
}
New-ActivityWatchDirectory -Path $Destination
Copy-Item -Path (Join-Path $Source '*') -Destination $Destination -Recurse -Force
}
function Copy-IfExists {
param(
[Parameter(Mandatory = $true)]
[string]$Source,
[Parameter(Mandatory = $true)]
[string]$Destination
)
if (Test-Path -LiteralPath $Source) {
Copy-Item -LiteralPath $Source -Destination $Destination -Force
}
}
function Convert-PathValue {
param(
[AllowNull()]
[string]$Value
)
if ([string]::IsNullOrWhiteSpace($Value)) {
return $Value
}
return $Value.Replace($OldInstallRoot, $NewInstallRoot).Replace($OldStateRoot, $NewStateRoot)
}
function Stop-AWatchTaskSet {
foreach ($task in @(Get-ScheduledTask | Where-Object { $_.TaskName -eq 'ActivityWatch Recovery' -or $_.TaskName -like 'ActivityWatch Launch *' })) {
Stop-ScheduledTask -TaskName $task.TaskName -ErrorAction SilentlyContinue
}
}
function Get-ExistingAWatchConfig {
$newConfigPath = Join-Path $NewStateRoot 'deployment-config.json'
$oldConfigPath = Join-Path $OldStateRoot 'deployment-config.json'
if (Test-Path -LiteralPath $oldConfigPath) {
return [pscustomobject]@{
Path = $oldConfigPath
Config = Read-ActivityWatchDeploymentConfig -Path $oldConfigPath
}
}
if (Test-Path -LiteralPath $newConfigPath) {
return [pscustomobject]@{
Path = $newConfigPath
Config = Read-ActivityWatchDeploymentConfig -Path $newConfigPath
}
}
throw "Не найден deployment-config.json ни в $OldStateRoot, ни в $NewStateRoot."
}
function Update-AWatchConfigPaths {
param(
[Parameter(Mandatory = $true)]
[pscustomobject]$Config
)
$logsRoot = Join-Path $NewStateRoot 'logs'
$Config.paths.installRoot = $NewInstallRoot
$Config.paths.stateRoot = $NewStateRoot
$Config.paths.logsRoot = $logsRoot
$Config.paths.collectorScript = Join-Path $NewStateRoot 'browser-domains-native-collector.ps1'
$Config.paths.endpointCollectorScript = Join-Path $NewStateRoot 'dlp-endpoint-signals-collector.ps1'
if ($Config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') {
$Config.paths.sessionCollectorScript = Join-Path $NewStateRoot 'worktime-session-collector.ps1'
}
$Config.paths.rulesPath = Join-Path $NewStateRoot 'web-category-rules.json'
if ($Config.paths.PSObject.Properties.Name -contains 'policyPath') {
$Config.paths.policyPath = Join-Path $NewStateRoot 'dlp-policy.json'
}
$Config.paths.launchScript = Join-Path $NewStateRoot 'launch-watchers.ps1'
$Config.paths.recoveryScript = Join-Path $NewStateRoot 'recovery-loop.ps1'
if ($Config.PSObject.Properties.Name -contains 'incidentCapture' -and $Config.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') {
$Config.incidentCapture.artifactsRoot = Convert-PathValue -Value ([string]$Config.incidentCapture.artifactsRoot)
}
return $Config
}
$existing = Get-ExistingAWatchConfig
$backupRoot = Join-Path $NewStateRoot ('migration-backups\' + (Get-Date -Format 'yyyyMMdd-HHmmss'))
$newConfigPath = Join-Path $NewStateRoot 'deployment-config.json'
$newLogsRoot = Join-Path $NewStateRoot 'logs'
$summary = [ordered]@{
sourceConfig = $existing.Path
oldInstallRoot = $OldInstallRoot
oldStateRoot = $OldStateRoot
newInstallRoot = $NewInstallRoot
newStateRoot = $NewStateRoot
backupRoot = $backupRoot
actions = @(
'stop ActivityWatch scheduled tasks',
'backup old/new install and state directories',
'copy old install/state contents to AWatch-rus paths',
'rewrite deployment-config.json paths',
'regenerate launcher/recovery scripts',
're-register scheduled tasks',
'run validate-deployment.ps1'
)
}
if ($WhatIfPreference) {
return [pscustomobject]$summary
}
if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Миграция ActivityWatch Windows/RDP путей в AWatch-rus')) {
New-ActivityWatchDirectory -Path $NewStateRoot
New-ActivityWatchDirectory -Path $backupRoot
Stop-AWatchTaskSet
foreach ($item in @(
@{ Source = $OldInstallRoot; Name = 'old-install' },
@{ Source = $OldStateRoot; Name = 'old-state' },
@{ Source = $NewInstallRoot; Name = 'new-install' },
@{ Source = $NewStateRoot; Name = 'new-state' }
)) {
if (Test-Path -LiteralPath $item.Source) {
Copy-Item -LiteralPath $item.Source -Destination (Join-Path $backupRoot $item.Name) -Recurse -Force
}
}
Copy-DirectoryContents -Source $OldInstallRoot -Destination $NewInstallRoot
Copy-DirectoryContents -Source $OldStateRoot -Destination $NewStateRoot
New-ActivityWatchDirectory -Path $newLogsRoot
foreach ($file in @(
'browser-domains-native-collector.ps1',
'dlp-endpoint-signals-collector.ps1',
'worktime-session-collector.ps1',
'web-category-rules.example.json',
'dlp-policy.example.json'
)) {
Copy-IfExists -Source (Join-Path $ToolkitRoot $file) -Destination (Join-Path $NewStateRoot $file)
}
Copy-IfExists -Source (Join-Path $OldStateRoot 'web-category-rules.json') -Destination (Join-Path $NewStateRoot 'web-category-rules.json')
Copy-IfExists -Source (Join-Path $OldStateRoot 'dlp-policy.json') -Destination (Join-Path $NewStateRoot 'dlp-policy.json')
if (-not (Test-Path -LiteralPath (Join-Path $NewStateRoot 'web-category-rules.json'))) {
Copy-IfExists -Source (Join-Path $NewStateRoot 'web-category-rules.example.json') -Destination (Join-Path $NewStateRoot 'web-category-rules.json')
}
if (-not (Test-Path -LiteralPath (Join-Path $NewStateRoot 'dlp-policy.json'))) {
Copy-IfExists -Source (Join-Path $NewStateRoot 'dlp-policy.example.json') -Destination (Join-Path $NewStateRoot 'dlp-policy.json')
}
$config = Update-AWatchConfigPaths -Config $existing.Config
Write-ActivityWatchDeploymentConfig -Config $config -Path $newConfigPath
Write-ActivityWatchLaunchScript -Path $config.paths.launchScript -ConfigPath $newConfigPath
Write-ActivityWatchRecoveryScript -Path $config.paths.recoveryScript -ConfigPath $newConfigPath
$taskDefinitions = @($config.userTasks)
Set-ActivityWatchAcl -InstallRoot $NewInstallRoot -StateRoot $NewStateRoot -LogsRoot $newLogsRoot
Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $config.paths.launchScript -ConfigPath $newConfigPath
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $config.paths.recoveryScript -ConfigPath $newConfigPath
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
Start-Sleep -Seconds 5
if (-not $SkipValidation) {
$validateScript = Join-Path $ToolkitRoot 'validate-deployment.ps1'
if (-not (Test-Path -LiteralPath $validateScript)) {
$validateScript = Join-Path $PSScriptRoot 'validate-deployment.ps1'
}
$report = & $validateScript -ConfigPath $newConfigPath
if (-not [bool]$report.overallOk) {
throw "Миграция выполнена, но validation завершился ошибкой. Backup: $backupRoot"
}
}
[pscustomobject]@{
migrated = $true
backupRoot = $backupRoot
configPath = $newConfigPath
installRoot = $NewInstallRoot
stateRoot = $NewStateRoot
}
}
@@ -1,6 +1,6 @@
[CmdletBinding()]
param(
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json'
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json'
)
Set-StrictMode -Version Latest
@@ -14,29 +14,48 @@ $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
$recoveryScript = [string]$config.paths.recoveryScript
$afkExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true }
$windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true }
$requiredFiles = @(
(Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe'),
(Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'),
$collectorScript,
$endpointCollectorScript,
$sessionCollectorScript,
$rulesPath,
$policyPath,
$launchScript,
$recoveryScript,
$ConfigPath
)
if ($afkExpected) {
$requiredFiles += (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe')
}
if ($windowExpected) {
$requiredFiles += (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe')
}
$missingFiles = @(
$requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) }
)
$processNames = @('aw-watcher-afk', 'aw-watcher-window')
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
$processNames = @()
if ($afkExpected) { $processNames += 'aw-watcher-afk' }
if ($windowExpected) { $processNames += 'aw-watcher-window' }
$runningProcesses = @()
if ($processNames.Count -gt 0) {
$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) {
@@ -57,7 +76,7 @@ $tasks = foreach ($taskName in $taskNames) {
else {
[pscustomobject]@{
taskName = $taskName
state = 'Missing'
state = 'Отсутствует'
present = $false
}
}
@@ -80,8 +99,16 @@ $result = [ordered]@{
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present }))
}
processes = [ordered]@{
expected = $processNames
list = @($runningProcesses)
ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2)
sessionCollectors = @($sessionCollectorProcesses)
ok = [bool](
(
($processNames.Count -eq 0) -or
(($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge $processNames.Count)
) -and
($sessionCollectorProcesses.Count -ge 1)
)
}
}
@@ -0,0 +1,154 @@
param(
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\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 "Конфигурация не найдена: $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
}
$sessionName = ''
$sessionIdIndex = 2
if ($parts[1] -match '^\d+$') {
$sessionIdIndex = 1
}
else {
$sessionName = $parts[1]
}
$sessionId = 0
if ($parts[$sessionIdIndex] -match '^\d+$') {
$sessionId = [int]$parts[$sessionIdIndex]
}
$records += [pscustomobject]@{
username = $parts[0]
sessionName = $sessionName
sessionId = $sessionId
state = $parts[$sessionIdIndex + 1]
}
}
}
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
}
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env bash
set -euo pipefail
mkdir -p windows/installkit/innosetup
mkdir -p docs/windows
cat > windows/installkit/innosetup/innosetup-rdp-package-filelist.md <<'EOF'
# Inno Setup: файл-лист для Windows RDP deployment
Дата актуализации: 2026-05-02 (UTC).
## Что это за документ
Этот файл — **чеклист упаковки** для Inno Setup.
- Он описывает, **что класть** в инсталлятор.
- Он описывает, **что не класть** (генерируется уже на целевом хосте).
- Он **не меняет** текущие deploy-скрипты и логику проекта.
## Важное уточнение по единой Windows-директории
Чтобы исключить путаницу:
1. InnoSetup и Ansible используют один набор путей.
2. Toolkit лежит в `{app}\windows` = `C:\Program Files\AWatch-rus\windows`.
3. Бинарники ActivityWatch лежат в `C:\Program Files\AWatch-rus\bin`.
4. Runtime-конфиг, collectors, логи и отчёты лежат в `C:\ProgramData\AWatch-rus`.
## 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`
- `windows/migrate-awatch-rus-paths.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
Нужен один из двух режимов:
- **Online**: скрипты скачивают `activitywatch-<version>-windows-x86_64.zip` из GitHub Releases.
- **Offline**: ZIP добавляется в пакет (например `payload\activitywatch-v0.13.2-windows-x86_64.zip`) и передаётся через `-PackageZipPath`.
## 3) Что НЕ включать в installer как статические файлы
Эти файлы/папки появляются на целевом Windows-хосте во время/после деплоя:
- `C:\ProgramData\AWatch-rus\deployment-config.json`
- `C:\ProgramData\AWatch-rus\web-category-rules.json`
- `C:\ProgramData\AWatch-rus\dlp-policy.json`
- `C:\ProgramData\AWatch-rus\logs\*`
- `%LOCALAPPDATA%\AWatch-rus\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\migrate-awatch-rus-paths.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. Выбран режим payload: online или offline.
3. Для offline-режима ZIP действительно лежит в `payload\`.
4. В .iss есть запуск нужного deploy-сценария (`deploy-ensemble.ps1` или `deploy-domain-users.ps1`).
5. После установки запускается `validate-deployment.ps1` с сохранением JSON-отчёта.
EOF
cat > docs/windows/innosetup-rdp-package-filelist.md <<'EOF'
windows/installkit/innosetup/innosetup-rdp-package-filelist.md
EOF
cat > windows/installkit/innosetup/AWatch-rus-InnoSetup.iss <<'EOF'
#define MyAppName "AWatch-rus InstallKit"
#define MyAppVersion "1.0.0"
#define MyAppPublisher "AWatch-rus"
#define MyAppExeName "powershell.exe"
[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
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 skipifsourcedoesntexist
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"""; Flags: runhidden
Filename: "powershell.exe"; Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\windows\validate-deployment.ps1"""; Flags: runhidden
EOF
mkdir -p windows/installkit/innosetup/payload
touch windows/installkit/innosetup/payload/.gitkeep
echo "patched"
echo "windows/installkit/innosetup/innosetup-rdp-package-filelist.md"
echo "docs/windows/innosetup-rdp-package-filelist.md"
echo "windows/installkit/innosetup/AWatch-rus-InnoSetup.iss"
echo "windows/installkit/innosetup/payload/.gitkeep"
+76
View File
@@ -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"
+45 -2
View File
@@ -67,7 +67,8 @@ cat > "$CONFIG_PATH" <<EOF
"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}"
"task_bucket": "aw-pve-task-events_${HOST_SHORT}",
"category_bucket": "aw-detmir-web-category_${HOST_SHORT}"
}
EOF
@@ -133,6 +134,7 @@ class Collector:
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"])
@@ -235,6 +237,9 @@ class Collector:
},
}
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):
@@ -260,6 +265,44 @@ class Collector:
}
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()
@@ -303,4 +346,4 @@ 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}"
echo "Buckets: aw-pve-webadmin-events_${HOST_SHORT}, aw-pve-task-events_${HOST_SHORT}, aw-detmir-web-category_${HOST_SHORT}"
+1 -1
View File
@@ -9,7 +9,7 @@ find aw-server proxmox -type f -name "*.sh" -print0 | xargs -0 -r -n1 bash -n
echo "[2/3] Shellcheck (if available)"
if command -v shellcheck >/dev/null 2>&1; then
find aw-server proxmox -type f -name "*.sh" -print0 | xargs -0 -r shellcheck
find aw-server proxmox -type f -name "*.sh" -print0 | xargs -0 -r shellcheck -e SC1007,SC1090,SC2016
else
echo "shellcheck not found, skipping."
fi
+23 -13
View File
@@ -5,7 +5,7 @@ function Assert-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated PowerShell session.'
throw 'Запустите этот скрипт из PowerShell с правами администратора.'
}
}
@@ -64,7 +64,7 @@ function Get-ActivityWatchPackageRoot {
Select-Object -First 1
if (-not $afkBinary) {
throw "Cannot find aw-watcher-afk.exe under $ExpandedRoot."
throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot."
}
return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent)
@@ -130,7 +130,7 @@ function Get-ActivityWatchExecutableMap {
foreach ($entry in $map.GetEnumerator()) {
if (-not (Test-Path -LiteralPath $entry.Value)) {
throw "Missing required ActivityWatch binary: $($entry.Value)"
throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)"
}
}
@@ -194,7 +194,7 @@ function Normalize-ActivityWatchUsers {
Sort-Object -Unique
if (-not $normalized -or $normalized.Count -eq 0) {
throw 'No target users resolved. Provide -Users or -UserListPath.'
throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.'
}
return @($normalized)
@@ -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
@@ -413,7 +421,7 @@ function Read-ActivityWatchDeploymentConfig {
)
if (-not (Test-Path -LiteralPath $Path)) {
throw "Deployment config not found: $Path"
throw "Конфигурация развёртывания не найдена: $Path"
}
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
@@ -539,7 +547,7 @@ function Send-LogonMarkerIfNeeded {
`$stateRoot = [string]`$Config.paths.stateRoot
`$markerRoots = New-Object System.Collections.Generic.List[string]
if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) {
`$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'ActivityWatch-Phase2\markers'))
`$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers'))
}
if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) {
`$markerRoots.Add((Join-Path `$stateRoot 'markers'))
@@ -585,7 +593,7 @@ function Send-LogonMarkerIfNeeded {
userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)"
sessionId = `$SessionId
hostname = `$script:Hostname
source = 'launch-watchers-phase2'
source = 'launch-watchers-awatch-rus'
}
} | ConvertTo-Json -Depth 5 -Compress
@@ -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)
@@ -639,11 +648,11 @@ function Start-CollectorScriptIfNeeded {
`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true }
if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) {
throw "Missing aw-watcher-afk.exe: `$afkExe"
throw "Не найден aw-watcher-afk.exe: `$afkExe"
}
if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) {
throw "Missing aw-watcher-window.exe: `$windowExe"
throw "Не найден aw-watcher-window.exe: `$windowExe"
}
if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) {
@@ -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
@@ -850,7 +860,7 @@ function Set-ActivityWatchScheduledTaskAction {
$taskCommand = ('"{0}" {1}' -f $Execute, $Arguments)
& schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "schtasks.exe /Change failed for $TaskName"
throw "schtasks.exe /Change завершился с ошибкой для $TaskName"
}
}
@@ -951,17 +961,17 @@ function Set-ActivityWatchAcl {
& icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "icacls failed for $InstallRoot"
throw "icacls завершился с ошибкой для $InstallRoot"
}
& icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "icacls failed for $StateRoot"
throw "icacls завершился с ошибкой для $StateRoot"
}
& icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "icacls failed for $LogsRoot"
throw "icacls завершился с ошибкой для $LogsRoot"
}
}
+19 -19
View File
@@ -1,6 +1,6 @@
[CmdletBinding()]
param(
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
[string]$ServerHost,
[int]$ServerPort,
[ValidateSet('http', 'https')]
@@ -49,18 +49,18 @@ function Get-DeploymentConfig {
}
$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' }
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' }
$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 }
$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' }
$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\ActivityWatch\web-category-rules.json' }
$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\ActivityWatch\dlp-policy.json' }
$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\AWatch-rus\web-category-rules.json' }
$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' }
$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 }
$resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pulseSeconds } else { 30 }
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\ActivityWatch\logs' }
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' }
$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) }
$resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) }
$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true }
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'ActivityWatch-Phase2\\incident-artifacts' }
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' }
$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true }
if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) {
@@ -158,12 +158,12 @@ function Get-HostFromUrl {
try {
$uri = [Uri]$Url
$host = $uri.Host.ToLowerInvariant()
if ($host.StartsWith('www.')) {
return $host.Substring(4)
$uriHost = $uri.Host.ToLowerInvariant()
if ($uriHost.StartsWith('www.')) {
return $uriHost.Substring(4)
}
return $host
return $uriHost
}
catch {
return $null
@@ -263,11 +263,11 @@ function Load-CustomCategoryRules {
if ($rules.Count -gt 0) {
$script:CategoryRules = @($rules) + @($script:CategoryRules)
Write-CollectorLog ("custom rules loaded: {0}" -f $rules.Count)
Write-CollectorLog ("пользовательские правила загружены: {0}" -f $rules.Count)
}
}
catch {
Write-CollectorLog ("custom rules load failed: {0}" -f $_.Exception.Message)
Write-CollectorLog ("не удалось загрузить пользовательские правила: {0}" -f $_.Exception.Message)
}
}
@@ -338,7 +338,7 @@ function Load-DlpPolicy {
param([string]$Path)
if (-not $Path -or -not (Test-Path -LiteralPath $Path)) {
Write-CollectorLog ("dlp policy not found, disabled: {0}" -f $Path)
Write-CollectorLog ("DLP-политика не найдена, DLP отключен: {0}" -f $Path)
return
}
@@ -372,7 +372,7 @@ function Load-DlpPolicy {
enabled = if ($rule.PSObject.Properties.Name -contains 'enabled') { [bool]$rule.enabled } else { $true }
action = if ($rule.action) { [string]$rule.action } else { [string]$script:DlpDefaults.action }
severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:DlpDefaults.severity }
message = if ($rule.message) { [string]$rule.message } else { "DLP rule matched: $($rule.id)" }
message = if ($rule.message) { [string]$rule.message } else { "Сработало DLP-правило: $($rule.id)" }
cooldownSeconds = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:DlpDefaults.cooldownSeconds }
when = [pscustomobject]@{
domains = if ($when.PSObject.Properties.Name -contains 'domains') { @($when.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() }
@@ -388,10 +388,10 @@ function Load-DlpPolicy {
}
$script:DlpRules = @($loaded)
Write-CollectorLog ("dlp policy loaded: enabled={0}, rules={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count)
Write-CollectorLog ("DLP-политика загружена: включена={0}, правил={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count)
}
catch {
Write-CollectorLog ("dlp policy parse failed: {0}" -f $_.Exception.Message)
Write-CollectorLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message)
}
}
@@ -625,7 +625,7 @@ function Capture-IncidentScreenshot {
}
}
catch {
Write-CollectorLog ("screenshot capture failed: {0}" -f $_.Exception.Message)
Write-CollectorLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message)
return @{}
}
}
@@ -775,7 +775,7 @@ function Send-CategoryHeartbeat {
Load-CustomCategoryRules -Path $resolvedRulesPath
Load-DlpPolicy -Path $resolvedPolicyPath
Write-CollectorLog ("collector started against {0}" -f $script:ApiBase)
Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase)
while ($true) {
try {
@@ -815,7 +815,7 @@ while ($true) {
}
}
catch {
Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message)
Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message)
}
Start-Sleep -Seconds $resolvedPollSeconds
+9 -6
View File
@@ -11,8 +11,8 @@ param(
[string]$Version = 'v0.13.2',
[string]$PackageUrl,
[string]$PackageZipPath,
[string]$InstallRoot = 'C:\Program Files\ActivityWatch',
[string]$StateRoot = 'C:\ProgramData\ActivityWatch',
[string]$InstallRoot = 'C:\Program Files\AWatch-rus\bin',
[string]$StateRoot = 'C:\ProgramData\AWatch-rus',
[int]$PollSeconds = 5,
[int]$PulseSeconds = 30,
[int]$RecoveryIntervalSeconds = 180,
@@ -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 `
@@ -100,8 +103,8 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
Write-Host 'ActivityWatch deployed for users:'
Write-Host 'ActivityWatch развёрнут для пользователей:'
$targetUsers | ForEach-Object { Write-Host " - $_" }
Write-Host "Server: ${ServerScheme}://$ServerHost`:$ServerPort"
Write-Host "State root: $StateRoot"
Write-Host "Policy file: $($assetResult.ActivePolicy)"
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
Write-Host "Каталог данных: $StateRoot"
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
+7 -7
View File
@@ -11,8 +11,8 @@ param(
[string]$Version = 'v0.13.2',
[string]$PackageUrl,
[string]$PackageZipPath,
[string]$InstallRoot = 'C:\Program Files\ActivityWatch',
[string]$StateRoot = 'C:\ProgramData\ActivityWatch',
[string]$InstallRoot = 'C:\Program Files\AWatch-rus\bin',
[string]$StateRoot = 'C:\ProgramData\AWatch-rus',
[int]$PollSeconds = 5,
[int]$PulseSeconds = 30,
[int]$RecoveryIntervalSeconds = 180,
@@ -46,7 +46,7 @@ $hardeningScript = Join-Path $PSScriptRoot 'hardening-recovery.ps1'
$validationScript = Join-Path $PSScriptRoot 'validate-deployment.ps1'
if (-not (Test-Path -LiteralPath $deployScript)) {
throw "Missing script: $deployScript"
throw "Не найден скрипт: $deployScript"
}
& $deployScript `
@@ -118,7 +118,7 @@ $report = [ordered]@{
if ($ValidateAfterDeploy) {
if (-not (Test-Path -LiteralPath $validationScript)) {
throw "Missing script: $validationScript"
throw "Не найден скрипт: $validationScript"
}
$validation = & $validationScript -ConfigPath (Join-Path $StateRoot 'deployment-config.json')
@@ -132,6 +132,6 @@ if ($reportDirectory) {
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8
Write-Host 'ActivityWatch ensemble deploy completed.'
Write-Host "Users: $($resolvedUsers -join ', ')"
Write-Host "Report: $effectiveReportPath"
Write-Host 'Комплексное развёртывание ActivityWatch завершено.'
Write-Host "Пользователи: $($resolvedUsers -join ', ')"
Write-Host "Отчёт: $effectiveReportPath"
+11 -8
View File
@@ -10,8 +10,8 @@ param(
[string]$Version = 'v0.13.2',
[string]$PackageUrl,
[string]$PackageZipPath,
[string]$InstallRoot = 'C:\Program Files\ActivityWatch',
[string]$StateRoot = 'C:\ProgramData\ActivityWatch',
[string]$InstallRoot = 'C:\Program Files\AWatch-rus\bin',
[string]$StateRoot = 'C:\ProgramData\AWatch-rus',
[int]$PollSeconds = 5,
[int]$PulseSeconds = 30,
[int]$RecoveryIntervalSeconds = 180,
@@ -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 `
@@ -98,9 +101,9 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
Write-Host "ActivityWatch deployed for $TargetUser"
Write-Host "Server: ${ServerScheme}://$ServerHost`:$ServerPort"
Write-Host "Install root: $InstallRoot"
Write-Host "State root: $StateRoot"
Write-Host "Rules file: $($assetResult.ActiveRules)"
Write-Host "Policy file: $($assetResult.ActivePolicy)"
Write-Host "ActivityWatch развёрнут для пользователя: $TargetUser"
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
Write-Host "Каталог установки: $InstallRoot"
Write-Host "Каталог данных: $StateRoot"
Write-Host "Файл правил: $($assetResult.ActiveRules)"
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
+128 -33
View File
@@ -1,6 +1,6 @@
[CmdletBinding()]
param(
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
[string]$ServerHost,
[int]$ServerPort,
[ValidateSet('http', 'https')]
@@ -81,7 +81,7 @@ function Send-EndpointSignalHeartbeat {
username = $env:USERNAME
sessionId = $script:SessionId
hostname = $script:Hostname
source = 'endpoint-signals-phase2'
source = 'endpoint-signals-awatch-rus'
} + $Data
} | ConvertTo-Json -Depth 6 -Compress
@@ -122,7 +122,7 @@ function Send-DlpIncidentHeartbeat {
username = $env:USERNAME
sessionId = $script:SessionId
hostname = $script:Hostname
source = 'endpoint-signals-phase2'
source = 'endpoint-signals-awatch-rus'
} + $Data + $captureData
} | ConvertTo-Json -Depth 7 -Compress
@@ -210,7 +210,7 @@ function Capture-IncidentScreenshot {
}
}
catch {
Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message)
Write-EndpointLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message)
return @{}
}
}
@@ -246,7 +246,7 @@ function Load-DlpPolicy {
}
if (-not $Path -or -not (Test-Path -LiteralPath $Path)) {
Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path)
Write-EndpointLog ("DLP-политика не найдена, используются значения по умолчанию: {0}" -f $Path)
return
}
@@ -266,7 +266,7 @@ function Load-DlpPolicy {
}
}
catch {
Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message)
Write-EndpointLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message)
}
}
@@ -319,13 +319,13 @@ function Evaluate-ClipboardRules {
$action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action }
$severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity }
$message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" }
$message = if ($rule.message) { [string]$rule.message } else { "Сработало правило буфера обмена: $ruleId" }
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{
clipboardHash = $ClipboardHash
clipboardLength = $ClipboardText.Length
}
Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2}" -f $ruleId, $action, $severity)
Write-EndpointLog ("инцидент буфера обмена правило={0} действие={1} важность={2}" -f $ruleId, $action, $severity)
}
}
@@ -347,13 +347,13 @@ function Evaluate-UsbRules {
$action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action }
$severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity }
$message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" }
$message = if ($rule.message) { [string]$rule.message } else { "Сработало правило USB-носителя: $ruleId" }
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{
driveLetter = $DriveLetter
volumeName = $VolumeName
}
Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3}" -f $ruleId, $action, $severity, $DriveLetter)
Write-EndpointLog ("инцидент USB правило={0} действие={1} важность={2} диск={3}" -f $ruleId, $action, $severity, $DriveLetter)
}
}
@@ -385,14 +385,14 @@ function Evaluate-PrintRules {
$action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action }
$severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity }
$message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" }
$message = if ($rule.message) { [string]$rule.message } else { "Сработало правило печати: $ruleId" }
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{
printerName = $PrinterName
documentName = $DocumentName
owner = $Owner
}
Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3}" -f $ruleId, $action, $severity, $PrinterName)
Write-EndpointLog ("инцидент печати правило={0} действие={1} важность={2} принтер={3}" -f $ruleId, $action, $severity, $PrinterName)
}
}
@@ -402,6 +402,52 @@ function Test-LooksLikeMojibakeQuestionMarks {
return $Value -match '\?{2,}'
}
function Test-DocumentNameNeedsFallback {
param([AllowNull()][string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) { return $true }
$trimmed = $Value.Trim()
if (Test-LooksLikeMojibakeQuestionMarks -Value $trimmed) { return $true }
if ($trimmed -match '^[0-9]+$') { return $true }
if ($trimmed -match '^(?i)(print document|document|local downlevel document)$') { return $true }
return $false
}
function Get-EventXmlValue {
param(
[Parameter(Mandatory = $true)][xml]$EventXml,
[Parameter(Mandatory = $true)][string]$Name
)
$node = $EventXml.Event.UserData.DocumentPrinted.$Name
if ($null -ne $node) {
return [string]$node
}
return ''
}
function Get-PrintJobPrinterName {
param(
[AllowNull()][string]$JobName,
[AllowNull()][string]$FallbackPrinterName
)
if ([string]::IsNullOrWhiteSpace($JobName)) {
if (-not [string]::IsNullOrWhiteSpace($FallbackPrinterName)) {
return $FallbackPrinterName.Trim()
}
return ''
}
$parts = $JobName -split ',', 2
if ($parts.Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($parts[0])) {
return $parts[0].Trim()
}
return $JobName.Trim()
}
function Normalize-OwnerForMatch {
param([AllowNull()][string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) { return '' }
@@ -469,13 +515,50 @@ function Get-PrintServiceEventSummary {
$propertyValues += [string]$prop.Value
}
$xml = $null
try {
$xml = [xml]$Event.ToXml()
}
catch {
}
$jobId = ''
$documentName = ''
$owner = ''
$portName = ''
$printerName = ''
$sizeBytes = ''
$pageCount = ''
if ($xml) {
$jobId = Get-EventXmlValue -EventXml $xml -Name 'Param1'
$documentName = Get-EventXmlValue -EventXml $xml -Name 'Param2'
$owner = Get-EventXmlValue -EventXml $xml -Name 'Param3'
$portName = Get-EventXmlValue -EventXml $xml -Name 'Param4'
$printerName = Get-EventXmlValue -EventXml $xml -Name 'Param5'
$sizeBytes = Get-EventXmlValue -EventXml $xml -Name 'Param7'
$pageCount = Get-EventXmlValue -EventXml $xml -Name 'Param8'
}
if ([string]::IsNullOrWhiteSpace($jobId) -and $props.Count -ge 1) { $jobId = [string]$props[0].Value }
if ([string]::IsNullOrWhiteSpace($documentName) -and $props.Count -ge 2) { $documentName = [string]$props[1].Value }
if ([string]::IsNullOrWhiteSpace($owner) -and $props.Count -ge 3) { $owner = [string]$props[2].Value }
if ([string]::IsNullOrWhiteSpace($portName) -and $props.Count -ge 4) { $portName = [string]$props[3].Value }
if ([string]::IsNullOrWhiteSpace($printerName) -and $props.Count -ge 5) { $printerName = [string]$props[4].Value }
if ([string]::IsNullOrWhiteSpace($sizeBytes) -and $props.Count -ge 7) { $sizeBytes = [string]$props[6].Value }
if ([string]::IsNullOrWhiteSpace($pageCount) -and $props.Count -ge 8) { $pageCount = [string]$props[7].Value }
[pscustomobject]@{
RecordId = [string]$Event.RecordId
TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' }
PropertyCount = $props.Count
DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' }
Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' }
PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' }
JobId = $jobId
DocumentName = $documentName
Owner = $owner
PortName = $portName
PrinterName = $printerName
SizeBytes = $sizeBytes
PageCount = $pageCount
PropertyValues = $propertyValues
}
}
@@ -488,7 +571,7 @@ function Get-PrintServiceDocumentFallback {
)
$preferred = [string]$EventSummary.DocumentName
if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') {
if (-not (Test-DocumentNameNeedsFallback -Value $preferred)) {
return $preferred
}
@@ -499,9 +582,10 @@ function Get-PrintServiceDocumentFallback {
$candidate = [string]$value
if ([string]::IsNullOrWhiteSpace($candidate)) { continue }
if ($candidate -eq $preferred) { continue }
if ($EventSummary.JobId -and $candidate -eq [string]$EventSummary.JobId) { continue }
if ($Owner -and $candidate -like "*$Owner*") { continue }
if ($PrinterName -and $candidate -like "*$PrinterName*") { continue }
if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue }
if (Test-DocumentNameNeedsFallback -Value $candidate) { continue }
if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') {
$pathCandidates.Add($candidate)
@@ -546,7 +630,7 @@ function Write-PrintServiceEventTrace {
}
Write-EndpointLog (
'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f
'printservice-307 этап={0} recordId={1} время={2} владелец={3} принтер={4} документ={5} итоговыйДокумент={6} свойства=[{7}] причина={8}' -f
$Phase,
$EventSummary.RecordId,
$EventSummary.TimeCreated,
@@ -561,6 +645,7 @@ function Write-PrintServiceEventTrace {
function Get-BetterDocumentNameFromPrintServiceEvents {
param(
[string]$JobId,
[string]$Owner,
[string]$PrinterName
)
@@ -578,32 +663,41 @@ function Get-BetterDocumentNameFromPrintServiceEvents {
$summary = Get-PrintServiceEventSummary -Event $event
$resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName
$jobMatches = if ($JobId) { [string]$summary.JobId -eq [string]$JobId } else { $true }
$ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true }
$printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true }
if ($pass -eq 'strict') {
if ($JobId -and -not $jobMatches) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-jobid-strict' -ResolvedDocument $resolvedDocument
continue
}
if ($Owner -and -not $ownerMatches) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-владельца-strict' -ResolvedDocument $resolvedDocument
continue
}
if ($PrinterName -and -not $printerMatches) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-принтера-strict' -ResolvedDocument $resolvedDocument
continue
}
}
else {
if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument
if ($JobId -and (-not $jobMatches) -and $Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-владельца-и-принтера-relaxed' -ResolvedDocument $resolvedDocument
continue
}
if ((-not $JobId) -and $Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'несовпадение-владельца-и-принтера-relaxed' -ResolvedDocument $resolvedDocument
continue
}
}
if ([string]::IsNullOrWhiteSpace($resolvedDocument)) {
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument ''
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('нет-кандидата-документа-' + $pass) -ResolvedDocument ''
continue
}
$matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' }
$matchReasonBase = if (Test-DocumentNameNeedsFallback -Value $summary.DocumentName) { 'использован-резервный-вариант' } else { 'напрямую' }
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument
return $resolvedDocument
}
@@ -616,15 +710,15 @@ function Get-BetterDocumentNameFromPrintServiceEvents {
}
$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' }
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' }
$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 }
$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' }
$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\ActivityWatch\dlp-policy.json' }
$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' }
$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 }
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\ActivityWatch\logs' }
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' }
$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) }
$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true }
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'ActivityWatch-Phase2\\incident-artifacts' }
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' }
$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true }
if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) {
@@ -648,7 +742,7 @@ $script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled
$script:ScreenshotTypesLoaded = $false
Load-DlpPolicy -Path $resolvedPolicyPath
Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase)
Write-EndpointLog ("endpoint-коллектор запущен для {0}" -f $script:ApiBase)
while ($true) {
try {
@@ -709,13 +803,13 @@ while ($true) {
if ($script:SeenPrintJob.ContainsKey($jobId)) { continue }
$script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime()
$printerName = [string]$job.Name
$printerName = Get-PrintJobPrinterName -JobName ([string]$job.Name) -FallbackPrinterName ([string]$job.DriverName)
$documentName = [string]$job.Document
$owner = [string]$job.Owner
$documentNameOriginal = $documentName
if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) {
$eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName
if (Test-DocumentNameNeedsFallback -Value $documentName) {
$eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -JobId $jobId -Owner $owner -PrinterName $printerName
if ($eventDocumentName) {
$documentName = $eventDocumentName
}
@@ -726,6 +820,7 @@ while ($true) {
documentName = $documentName
documentNameOriginal = $documentNameOriginal
owner = $owner
printJobId = $jobId
}
Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner
}
@@ -789,7 +884,7 @@ while ($true) {
}
}
catch {
Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message)
Write-EndpointLog ("ошибка коллектора: {0}" -f $_.Exception.Message)
}
Start-Sleep -Seconds $resolvedPollSeconds
+10 -8
View File
@@ -1,6 +1,6 @@
[CmdletBinding()]
param(
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
[string]$ServerHost,
[int]$ServerPort,
[ValidateSet('http', 'https')]
@@ -42,11 +42,11 @@ if (Test-Path -LiteralPath $ConfigPath) {
}
if (-not $existingConfig -and (-not $ServerHost)) {
throw 'deployment-config.json is missing. Provide -ServerHost and user parameters, or run a deploy script first.'
throw 'deployment-config.json отсутствует. Укажите -ServerHost и параметры пользователей либо сначала выполните скрипт развёртывания.'
}
$effectiveStateRoot = if ($StateRoot) { $StateRoot } elseif ($existingConfig) { [string]$existingConfig.paths.stateRoot } else { 'C:\ProgramData\ActivityWatch' }
$effectiveInstallRoot = if ($InstallRoot) { $InstallRoot } elseif ($existingConfig) { [string]$existingConfig.paths.installRoot } else { 'C:\Program Files\ActivityWatch' }
$effectiveStateRoot = if ($StateRoot) { $StateRoot } elseif ($existingConfig) { [string]$existingConfig.paths.stateRoot } else { 'C:\ProgramData\AWatch-rus' }
$effectiveInstallRoot = if ($InstallRoot) { $InstallRoot } elseif ($existingConfig) { [string]$existingConfig.paths.installRoot } else { 'C:\Program Files\AWatch-rus\bin' }
$effectiveLogsRoot = if ($existingConfig) { [string]$existingConfig.paths.logsRoot } else { Join-Path $effectiveStateRoot 'logs' }
$effectiveConfigPath = if ($ConfigPath) { $ConfigPath } else { Join-Path $effectiveStateRoot 'deployment-config.json' }
$effectiveLaunchScript = Join-Path $effectiveStateRoot 'launch-watchers.ps1'
@@ -78,7 +78,7 @@ elseif ($existingConfig) {
@($existingConfig.userTasks | ForEach-Object { [string]$_.userId })
}
else {
throw 'Target users are missing.'
throw 'Не указаны целевые пользователи.'
}
New-ActivityWatchDirectory -Path $effectiveStateRoot
@@ -96,6 +96,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $effectiveInstallRoot | Out-Null
$assetResult = Copy-ActivityWatchCollectorAssets `
-CollectorScriptSource (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') `
-EndpointCollectorScriptSource (Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1') `
-SessionCollectorScriptSource (Join-Path $PSScriptRoot 'worktime-session-collector.ps1') `
-ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') `
-ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') `
-StateRoot $effectiveStateRoot `
@@ -115,6 +116,7 @@ $config = New-ActivityWatchDeploymentConfig `
-LogsRoot $effectiveLogsRoot `
-CollectorScript $effectiveCollector `
-EndpointCollectorScript $effectiveEndpointCollector `
-SessionCollectorScript $effectiveSessionCollector `
-RulesPath $effectiveRules `
-PolicyPath $effectivePolicy `
-PollSeconds $effectivePollSeconds `
@@ -139,6 +141,6 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
Write-Host 'ActivityWatch hardening/recovery completed.'
Write-Host "Config: $effectiveConfigPath"
Write-Host "Users repaired: $($effectiveUsers -join ', ')"
Write-Host 'Укрепление и восстановление ActivityWatch завершены.'
Write-Host "Конфигурация: $effectiveConfigPath"
Write-Host "Пользователи восстановлены: $($effectiveUsers -join ', ')"
+6
View File
@@ -0,0 +1,6 @@
# Build outputs
/*.exe
# Offline payload ZIPs should not be committed
/*.zip
/payload/*.zip
@@ -0,0 +1,212 @@
#define MyAppName "AWatch-rus InstallKit"
#define MyAppVersion "1.0.0"
#define MyAppPublisher "AWatch-rus"
#define AwDefaultServerHost "10.10.10.13"
#define AwDefaultServerPort "5600"
#define AwDefaultUsers "user1,user2,user3,user4,user5"
#define AwDefaultInstallRoot "C:\\Program Files\\AWatch-rus\\bin"
#define AwDefaultStateRoot "C:\\ProgramData\\AWatch-rus"
#define AwDefaultZipName "activitywatch-v0.13.2-windows-x86_64.zip"
[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
Compression=lzma
SolidCompression=yes
ArchitecturesInstallIn64BitMode=x64compatible
PrivilegesRequired=admin
[Languages]
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
[Tasks]
Name: "deploy"; Description: "Запустить деплой после установки"; Flags: checkedonce
Name: "validate"; Description: "Запустить validate-deployment (через -ValidateAfterDeploy)"; Flags: checkedonce
[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: "..\..\migrate-awatch-rus-paths.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
; Offline payload (optional): place ZIP into windows/installkit/innosetup/payload/ before compiling.
Source: "payload\{#AwDefaultZipName}"; DestDir: "{app}\payload"; Flags: ignoreversion skipifsourcedoesntexist
Source: "innosetup-rdp-package-filelist.md"; DestDir: "{app}\windows\installkit\innosetup"; Flags: ignoreversion
[Run]
Filename: "powershell.exe"; Parameters: "{code:GetDeployEnsembleParams}"; Flags: runhidden; Tasks: deploy
[Code]
var
ServerHostPage: TInputQueryWizardPage;
UsersPage: TInputQueryWizardPage;
OptionsPage: TInputOptionWizardPage;
function NormalizeUserCsv(const UserCsv: string): string;
var
i: Integer;
s: string;
token: string;
begin
Result := '';
s := UserCsv;
while True do
begin
i := Pos(',', s);
if i = 0 then
begin
token := Trim(s);
s := '';
end
else
begin
token := Trim(Copy(s, 1, i - 1));
Delete(s, 1, i);
end;
if token <> '' then
begin
if Result <> '' then
Result := Result + ',';
Result := Result + token;
end;
if s = '' then
Break;
end;
end;
function BuildUsersPowerShellArg(const UserCsv: string): string;
var
i: Integer;
s: string;
token: string;
quoted: string;
begin
Result := '';
s := UserCsv;
while True do
begin
i := Pos(',', s);
if i = 0 then
begin
token := Trim(s);
s := '';
end
else
begin
token := Trim(Copy(s, 1, i - 1));
Delete(s, 1, i);
end;
if token <> '' then
begin
quoted := '"' + token + '"';
if Result <> '' then
Result := Result + ',';
Result := Result + quoted;
end;
if s = '' then
Break;
end;
if Result <> '' then
Result := '-Users ' + Result;
end;
function PayloadZipPath: string;
begin
Result := ExpandConstant('{app}\payload\{#AwDefaultZipName}');
end;
function HasPayloadZip: Boolean;
begin
Result := FileExists(ExpandConstant('{src}\payload\{#AwDefaultZipName}'));
end;
procedure InitializeWizard;
begin
ServerHostPage := CreateInputQueryPage(
wpSelectDir,
'Параметры AW сервера',
'Укажите сервер ActivityWatch (куда агенты будут отправлять данные).',
'Если нужно, измените host/port. По умолчанию — наша конфигурация.'
);
ServerHostPage.Add('ServerHost', False);
ServerHostPage.Add('ServerPort', False);
ServerHostPage.Values[0] := '{#AwDefaultServerHost}';
ServerHostPage.Values[1] := '{#AwDefaultServerPort}';
UsersPage := CreateInputQueryPage(
ServerHostPage.ID,
'Пользователи (RDP)',
'Перечень пользователей, для которых разворачиваем агенты.',
'Введите список через запятую. Пример: user1,user2,user3'
);
UsersPage.Add('Users (CSV)', False);
UsersPage.Values[0] := '{#AwDefaultUsers}';
OptionsPage := CreateInputOptionPage(
UsersPage.ID,
'Опции деплоя',
'Выберите опции для установки/валидации.',
'',
False,
False
);
OptionsPage.Add('Использовать offline payload (встроенный ZIP)');
OptionsPage.Add('Запустить validate-deployment после деплоя');
OptionsPage.Values[0] := HasPayloadZip;
OptionsPage.Values[1] := True;
end;
function GetDeployEnsembleParams(Param: string): string;
var
serverHost: string;
serverPort: string;
usersCsv: string;
usersArg: string;
zipArg: string;
validateArg: string;
begin
serverHost := Trim(ServerHostPage.Values[0]);
serverPort := Trim(ServerHostPage.Values[1]);
usersCsv := NormalizeUserCsv(UsersPage.Values[0]);
usersArg := BuildUsersPowerShellArg(usersCsv);
if usersArg = '' then
RaiseException('Users list is empty.');
zipArg := '';
if OptionsPage.Values[0] then
zipArg := ' -PackageZipPath "' + PayloadZipPath + '"';
validateArg := '';
if OptionsPage.Values[1] and WizardIsTaskSelected('validate') then
validateArg := ' -ValidateAfterDeploy';
Result :=
'-NoProfile -ExecutionPolicy Bypass -File "' + ExpandConstant('{app}\windows\deploy-ensemble.ps1') + '"' +
' -ServerHost "' + serverHost + '"' +
' -ServerPort ' + serverPort +
' ' + usersArg +
zipArg +
' -InstallRoot "{#AwDefaultInstallRoot}"' +
' -StateRoot "{#AwDefaultStateRoot}"' +
validateArg;
end;
+35
View File
@@ -0,0 +1,35 @@
# Build (Inno Setup)
## Prerequisites
- Windows machine with Inno Setup installed (`ISCC.exe` available).
## Offline vs Online payload
- **Offline (recommended for closed networks)**: put `activitywatch-v0.13.2-windows-x86_64.zip` into `payload\`.
- **Online**: leave `payload\` empty; the deploy script will download the ZIP from GitHub Releases.
## Compile
From this folder:
```bat
iscc AWatch-rus-InnoSetup.iss
```
The resulting installer `AWatch-rus-InstallKit.exe` is written to the same directory (by `OutputDir=.`).
## Compile from Linux (Wine)
```sh
./build_with_wine.sh
```
## Install-time parameters
The installer wizard asks for:
- `ServerHost` / `ServerPort` (defaults to our AW server `10.10.10.13:5600`)
- `Users` (CSV)
- Whether to use offline payload (auto-enabled when the ZIP exists at compile time)
- Whether to validate after deploy (`-ValidateAfterDeploy`, report written to `C:\ProgramData\AWatch-rus\ensemble-report-*.json`)
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
KIT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
WINEPREFIX_DEFAULT="${HOME}/.wine-aw-inno"
WINEPREFIX="${WINEPREFIX:-$WINEPREFIX_DEFAULT}"
IS_EXE="${IS_EXE:-/tmp/innosetup.exe}"
ISCC_WIN='C:\InnoSetup\ISCC.exe'
cd "$KIT_DIR"
mkdir -p payload
ZIP_NAME="activitywatch-v0.13.2-windows-x86_64.zip"
# If someone dropped the ZIP in the kit root, stage it into payload/.
if [[ -f "$ZIP_NAME" && ! -f "payload/$ZIP_NAME" ]]; then
cp -f "$ZIP_NAME" "payload/$ZIP_NAME"
fi
export WINEPREFIX
export WINEDEBUG="${WINEDEBUG:--all}"
if [[ ! -f "${WINEPREFIX}/drive_c/InnoSetup/ISCC.exe" ]]; then
mkdir -p "$WINEPREFIX"
if [[ ! -f "$IS_EXE" ]]; then
curl -fsSL -o "$IS_EXE" https://jrsoftware.org/download.php/is.exe
fi
wineboot -u
wine "$IS_EXE" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP- /DIR="C:\InnoSetup"
fi
rm -f AWatch-rus-InstallKit.exe
wine "$ISCC_WIN" "AWatch-rus-InnoSetup.iss"
ls -la AWatch-rus-InstallKit.exe
sha256sum AWatch-rus-InstallKit.exe
@@ -0,0 +1,96 @@
# Inno Setup: файл-лист для Windows RDP deployment
Дата актуализации: 2026-05-02 (UTC).
## Что это за документ
Этот файл — **чеклист упаковки** для Inno Setup.
- Он описывает, **что класть** в инсталлятор.
- Он описывает, **что не класть** (генерируется уже на целевом хосте).
- Он **не меняет** текущие deploy-скрипты и логику проекта.
## Важное уточнение по единой Windows-директории
Чтобы исключить путаницу:
1. InnoSetup и Ansible используют один набор путей.
2. Toolkit лежит в `{app}\windows` = `C:\Program Files\AWatch-rus\windows`.
3. Бинарники ActivityWatch лежат в `C:\Program Files\AWatch-rus\bin`.
4. Runtime-конфиг, collectors, логи и отчёты лежат в `C:\ProgramData\AWatch-rus`.
## 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`
- `windows/migrate-awatch-rus-paths.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
Поддерживаются оба режима:
- **Online**: ZIP скачивается из GitHub Releases.
- **Offline**: ZIP кладётся в installer (`payload\activitywatch-v0.13.2-windows-x86_64.zip`) и передаётся в deploy через `-PackageZipPath`.
Для нашей закрытой среды обычно используется **offline-режим**.
## 3) Что НЕ включать в installer как статические файлы
Эти файлы/папки появляются на целевом Windows-хосте во время/после деплоя:
- `C:\ProgramData\AWatch-rus\deployment-config.json`
- `C:\ProgramData\AWatch-rus\web-category-rules.json`
- `C:\ProgramData\AWatch-rus\dlp-policy.json`
- `C:\ProgramData\AWatch-rus\logs\*`
- `%LOCALAPPDATA%\AWatch-rus\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\migrate-awatch-rus-paths.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. В .iss не осталось вызова `deploy-ensemble.ps1` без параметров: нужны `-ServerHost` и `-Users`.
3. Для Windows/RDP используются единые пути:
- `InstallRoot = C:\Program Files\AWatch-rus\bin`
- `StateRoot = C:\ProgramData\AWatch-rus`
4. Для offline-режима ZIP лежит в `windows/installkit/innosetup/payload/` (имя: `activitywatch-v0.13.2-windows-x86_64.zip`).
5. Для проверки используется `-ValidateAfterDeploy` (отчёт `ensemble-report-*.json` пишется в `C:\ProgramData\AWatch-rus\`).
@@ -0,0 +1,6 @@
Place ActivityWatch ZIP here for offline installer builds.
Expected filename:
- `activitywatch-v0.13.2-windows-x86_64.zip`
If the ZIP is absent, the installer can still be built (online mode), but the deploy step will download the payload from GitHub Releases.
+214
View File
@@ -0,0 +1,214 @@
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2',
[string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2',
[string]$NewInstallRoot = 'C:\Program Files\AWatch-rus\bin',
[string]$NewStateRoot = 'C:\ProgramData\AWatch-rus',
[string]$ToolkitRoot = 'C:\Program Files\AWatch-rus\windows',
[switch]$SkipValidation
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1'
Import-Module $modulePath -Force
Assert-Administrator
function Copy-DirectoryContents {
param(
[Parameter(Mandatory = $true)]
[string]$Source,
[Parameter(Mandatory = $true)]
[string]$Destination
)
if (-not (Test-Path -LiteralPath $Source)) {
return
}
New-ActivityWatchDirectory -Path $Destination
Copy-Item -Path (Join-Path $Source '*') -Destination $Destination -Recurse -Force
}
function Copy-IfExists {
param(
[Parameter(Mandatory = $true)]
[string]$Source,
[Parameter(Mandatory = $true)]
[string]$Destination
)
if (Test-Path -LiteralPath $Source) {
Copy-Item -LiteralPath $Source -Destination $Destination -Force
}
}
function Convert-PathValue {
param(
[AllowNull()]
[string]$Value
)
if ([string]::IsNullOrWhiteSpace($Value)) {
return $Value
}
return $Value.Replace($OldInstallRoot, $NewInstallRoot).Replace($OldStateRoot, $NewStateRoot)
}
function Stop-AWatchTaskSet {
foreach ($task in @(Get-ScheduledTask | Where-Object { $_.TaskName -eq 'ActivityWatch Recovery' -or $_.TaskName -like 'ActivityWatch Launch *' })) {
Stop-ScheduledTask -TaskName $task.TaskName -ErrorAction SilentlyContinue
}
}
function Get-ExistingAWatchConfig {
$newConfigPath = Join-Path $NewStateRoot 'deployment-config.json'
$oldConfigPath = Join-Path $OldStateRoot 'deployment-config.json'
if (Test-Path -LiteralPath $oldConfigPath) {
return [pscustomobject]@{
Path = $oldConfigPath
Config = Read-ActivityWatchDeploymentConfig -Path $oldConfigPath
}
}
if (Test-Path -LiteralPath $newConfigPath) {
return [pscustomobject]@{
Path = $newConfigPath
Config = Read-ActivityWatchDeploymentConfig -Path $newConfigPath
}
}
throw "Не найден deployment-config.json ни в $OldStateRoot, ни в $NewStateRoot."
}
function Update-AWatchConfigPaths {
param(
[Parameter(Mandatory = $true)]
[pscustomobject]$Config
)
$logsRoot = Join-Path $NewStateRoot 'logs'
$Config.paths.installRoot = $NewInstallRoot
$Config.paths.stateRoot = $NewStateRoot
$Config.paths.logsRoot = $logsRoot
$Config.paths.collectorScript = Join-Path $NewStateRoot 'browser-domains-native-collector.ps1'
$Config.paths.endpointCollectorScript = Join-Path $NewStateRoot 'dlp-endpoint-signals-collector.ps1'
if ($Config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') {
$Config.paths.sessionCollectorScript = Join-Path $NewStateRoot 'worktime-session-collector.ps1'
}
$Config.paths.rulesPath = Join-Path $NewStateRoot 'web-category-rules.json'
if ($Config.paths.PSObject.Properties.Name -contains 'policyPath') {
$Config.paths.policyPath = Join-Path $NewStateRoot 'dlp-policy.json'
}
$Config.paths.launchScript = Join-Path $NewStateRoot 'launch-watchers.ps1'
$Config.paths.recoveryScript = Join-Path $NewStateRoot 'recovery-loop.ps1'
if ($Config.PSObject.Properties.Name -contains 'incidentCapture' -and $Config.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') {
$Config.incidentCapture.artifactsRoot = Convert-PathValue -Value ([string]$Config.incidentCapture.artifactsRoot)
}
return $Config
}
$existing = Get-ExistingAWatchConfig
$backupRoot = Join-Path $NewStateRoot ('migration-backups\' + (Get-Date -Format 'yyyyMMdd-HHmmss'))
$newConfigPath = Join-Path $NewStateRoot 'deployment-config.json'
$newLogsRoot = Join-Path $NewStateRoot 'logs'
$summary = [ordered]@{
sourceConfig = $existing.Path
oldInstallRoot = $OldInstallRoot
oldStateRoot = $OldStateRoot
newInstallRoot = $NewInstallRoot
newStateRoot = $NewStateRoot
backupRoot = $backupRoot
actions = @(
'stop ActivityWatch scheduled tasks',
'backup old/new install and state directories',
'copy old install/state contents to AWatch-rus paths',
'rewrite deployment-config.json paths',
'regenerate launcher/recovery scripts',
're-register scheduled tasks',
'run validate-deployment.ps1'
)
}
if ($WhatIfPreference) {
return [pscustomobject]$summary
}
if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Миграция ActivityWatch Windows/RDP путей в AWatch-rus')) {
New-ActivityWatchDirectory -Path $NewStateRoot
New-ActivityWatchDirectory -Path $backupRoot
Stop-AWatchTaskSet
foreach ($item in @(
@{ Source = $OldInstallRoot; Name = 'old-install' },
@{ Source = $OldStateRoot; Name = 'old-state' },
@{ Source = $NewInstallRoot; Name = 'new-install' },
@{ Source = $NewStateRoot; Name = 'new-state' }
)) {
if (Test-Path -LiteralPath $item.Source) {
Copy-Item -LiteralPath $item.Source -Destination (Join-Path $backupRoot $item.Name) -Recurse -Force
}
}
Copy-DirectoryContents -Source $OldInstallRoot -Destination $NewInstallRoot
Copy-DirectoryContents -Source $OldStateRoot -Destination $NewStateRoot
New-ActivityWatchDirectory -Path $newLogsRoot
foreach ($file in @(
'browser-domains-native-collector.ps1',
'dlp-endpoint-signals-collector.ps1',
'worktime-session-collector.ps1',
'web-category-rules.example.json',
'dlp-policy.example.json'
)) {
Copy-IfExists -Source (Join-Path $ToolkitRoot $file) -Destination (Join-Path $NewStateRoot $file)
}
Copy-IfExists -Source (Join-Path $OldStateRoot 'web-category-rules.json') -Destination (Join-Path $NewStateRoot 'web-category-rules.json')
Copy-IfExists -Source (Join-Path $OldStateRoot 'dlp-policy.json') -Destination (Join-Path $NewStateRoot 'dlp-policy.json')
if (-not (Test-Path -LiteralPath (Join-Path $NewStateRoot 'web-category-rules.json'))) {
Copy-IfExists -Source (Join-Path $NewStateRoot 'web-category-rules.example.json') -Destination (Join-Path $NewStateRoot 'web-category-rules.json')
}
if (-not (Test-Path -LiteralPath (Join-Path $NewStateRoot 'dlp-policy.json'))) {
Copy-IfExists -Source (Join-Path $NewStateRoot 'dlp-policy.example.json') -Destination (Join-Path $NewStateRoot 'dlp-policy.json')
}
$config = Update-AWatchConfigPaths -Config $existing.Config
Write-ActivityWatchDeploymentConfig -Config $config -Path $newConfigPath
Write-ActivityWatchLaunchScript -Path $config.paths.launchScript -ConfigPath $newConfigPath
Write-ActivityWatchRecoveryScript -Path $config.paths.recoveryScript -ConfigPath $newConfigPath
$taskDefinitions = @($config.userTasks)
Set-ActivityWatchAcl -InstallRoot $NewInstallRoot -StateRoot $NewStateRoot -LogsRoot $newLogsRoot
Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $config.paths.launchScript -ConfigPath $newConfigPath
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $config.paths.recoveryScript -ConfigPath $newConfigPath
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
Start-Sleep -Seconds 5
if (-not $SkipValidation) {
$validateScript = Join-Path $ToolkitRoot 'validate-deployment.ps1'
if (-not (Test-Path -LiteralPath $validateScript)) {
$validateScript = Join-Path $PSScriptRoot 'validate-deployment.ps1'
}
$report = & $validateScript -ConfigPath $newConfigPath
if (-not [bool]$report.overallOk) {
throw "Миграция выполнена, но validation завершился ошибкой. Backup: $backupRoot"
}
}
[pscustomobject]@{
migrated = $true
backupRoot = $backupRoot
configPath = $newConfigPath
installRoot = $NewInstallRoot
stateRoot = $NewStateRoot
}
}
+34 -7
View File
@@ -1,6 +1,6 @@
[CmdletBinding()]
param(
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json'
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json'
)
Set-StrictMode -Version Latest
@@ -14,29 +14,48 @@ $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
$recoveryScript = [string]$config.paths.recoveryScript
$afkExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true }
$windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true }
$requiredFiles = @(
(Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe'),
(Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'),
$collectorScript,
$endpointCollectorScript,
$sessionCollectorScript,
$rulesPath,
$policyPath,
$launchScript,
$recoveryScript,
$ConfigPath
)
if ($afkExpected) {
$requiredFiles += (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe')
}
if ($windowExpected) {
$requiredFiles += (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe')
}
$missingFiles = @(
$requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) }
)
$processNames = @('aw-watcher-afk', 'aw-watcher-window')
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
$processNames = @()
if ($afkExpected) { $processNames += 'aw-watcher-afk' }
if ($windowExpected) { $processNames += 'aw-watcher-window' }
$runningProcesses = @()
if ($processNames.Count -gt 0) {
$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) {
@@ -57,7 +76,7 @@ $tasks = foreach ($taskName in $taskNames) {
else {
[pscustomobject]@{
taskName = $taskName
state = 'Missing'
state = 'Отсутствует'
present = $false
}
}
@@ -80,8 +99,16 @@ $result = [ordered]@{
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present }))
}
processes = [ordered]@{
expected = $processNames
list = @($runningProcesses)
ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2)
sessionCollectors = @($sessionCollectorProcesses)
ok = [bool](
(
($processNames.Count -eq 0) -or
(($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge $processNames.Count)
) -and
($sessionCollectorProcesses.Count -ge 1)
)
}
}
+154
View File
@@ -0,0 +1,154 @@
param(
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\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 "Конфигурация не найдена: $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
}
$sessionName = ''
$sessionIdIndex = 2
if ($parts[1] -match '^\d+$') {
$sessionIdIndex = 1
}
else {
$sessionName = $parts[1]
}
$sessionId = 0
if ($parts[$sessionIdIndex] -match '^\d+$') {
$sessionId = [int]$parts[$sessionIdIndex]
}
$records += [pscustomobject]@{
username = $parts[0]
sessionName = $sessionName
sessionId = $sessionId
state = $parts[$sessionIdIndex + 1]
}
}
}
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
}