diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc1770e..a3511cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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." diff --git a/.gitignore b/.gitignore index 7eecd45..62fb7b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,13 @@ # Local secrets secrets/deploy.secrets.env +secrets/runtime.env # Runtime / reports *.log *.tmp *.bak windows/*.report.json +.rollout-logs/ # IDE .idea/ diff --git a/README.md b/README.md index b2d419a..231b8ca 100755 --- a/README.md +++ b/README.md @@ -13,12 +13,14 @@ - `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. +- `docs/dlp-aggregator.md` — прототип централизованной агрегации DLP/file-operation событий. - `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_*`) и session-level presence для удалённых Windows/RDP пользователей (`aw-worktime-sessions_*`). +- `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/aggregate_dlp_events.py` — сбор `aw-file-operations_*` и `aw-dlp-incidents_*` в SQLite/PostgreSQL. - `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. @@ -41,9 +43,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'а: diff --git a/ansible/README.md b/ansible/README.md index 6dab5f3..3c9d42a 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -1,46 +1,51 @@ # 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 ``` +## Секреты (пароли) безопасно + +Рекомендуемый способ не хранить пароли в репозитории — перед запуском экспортировать их в переменные окружения: + +- Linux `aw_server` (SSH пароль root): `AW_SSH_PASSWORD` +- Windows `aw_windows` (WinRM пароль): `AW_WINRM_PASSWORD` + +В `group_vars/aw_server.yml` и `group_vars/windows.yml` они читаются через `lookup('env', ...)`. + ## Полный установочный playbook (всё за один запуск) Если нужно прогнать полный цикл одной командой: ```bash -cd /home/igor/tmp/AWatch-rus/ansible +cd ansible ansible-playbook -i inventory.ini install_full_stack.yml ``` @@ -48,7 +53,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 +61,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_` и ожидает свежие `not-afk` события; - запускает `validate-deployment.ps1`; - забирает JSON-отчёт в локальную директорию (`/tmp/aw-rus-validation` по умолчанию). @@ -110,17 +116,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_`; +- `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 +155,16 @@ 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`. + +## Prod rollout одной командой + +Для ручного запуска с dry-run и логированием используйте: + +```bash +bash scripts/prod_rollout.sh +``` + +Скрипт попросит `AW_SSH_PASSWORD` и `AW_WINRM_PASSWORD` интерактивно (ввод скрыт) и сложит логи в `.rollout-logs/`. diff --git a/ansible/deploy_aw_pfsense_poller.yml b/ansible/deploy_aw_pfsense_poller.yml index 3eea27c..34aa29d 100644 --- a/ansible/deploy_aw_pfsense_poller.yml +++ b/ansible/deploy_aw_pfsense_poller.yml @@ -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 diff --git a/ansible/deploy_aw_server.yml b/ansible/deploy_aw_server.yml index d5f7fda..42b88fc 100644 --- a/ansible/deploy_aw_server.yml +++ b/ansible/deploy_aw_server.yml @@ -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,30 @@ 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_db_path | dirname }}" + - "{{ aw_server_data_dir }}/.config" + - "{{ aw_server_data_dir }}/.config/activitywatch" + - "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust" + - "{{ aw_server_data_dir }}/backups" + - "{{ aw_server_log_dir }}" + - /etc/activitywatch + - "{{ aw_bootstrap_dir }}" + + - name: Настроить каталоги ActivityWatch с владельцем сервиса ansible.builtin.file: path: "{{ item }}" state: directory @@ -41,148 +71,526 @@ 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_db_path | dirname }}" + - "{{ aw_server_data_dir }}/.config" + - "{{ aw_server_data_dir }}/.config/activitywatch" + - "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust" + - "{{ aw_server_data_dir }}/backups" - "{{ aw_server_log_dir }}" - - /etc/activitywatch - - "{{ aw_bootstrap_dir }}" - - name: Download ActivityWatch release archive - ansible.builtin.get_url: - url: "{{ aw_server_download_url }}" - dest: "{{ aw_archive_path }}" - mode: "0644" + - name: (Check mode) Пропустить установку релиза ActivityWatch + ansible.builtin.debug: + msg: "ansible_check_mode=true: download/unarchive/install of ActivityWatch release is skipped." + when: ansible_check_mode - - name: Unpack ActivityWatch release - ansible.builtin.unarchive: - src: "{{ aw_archive_path }}" - dest: "{{ aw_release_dir }}" - remote_src: true - extra_opts: ["-o"] + - name: Установить релиз ActivityWatch (download/unarchive/install) + when: not ansible_check_mode + block: + - name: Скачать архив релиза ActivityWatch + ansible.builtin.get_url: + url: "{{ aw_server_download_url }}" + dest: "{{ aw_archive_path }}" + mode: "0644" - - name: Discover extracted AW directory - ansible.builtin.find: - paths: "{{ aw_release_dir }}" - file_type: directory - patterns: "activitywatch*" - register: aw_release_find + - name: Распаковать релиз ActivityWatch + ansible.builtin.unarchive: + src: "{{ aw_archive_path }}" + dest: "{{ aw_release_dir }}" + remote_src: true + extra_opts: ["-o"] - - 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: Найти распакованный каталог ActivityWatch + ansible.builtin.find: + paths: "{{ aw_release_dir }}" + recurse: true + file_type: directory + patterns: "activitywatch*" + register: aw_release_find - - name: Verify extracted directory exists - ansible.builtin.assert: - that: - - aw_release_extracted is defined - - aw_release_extracted | length > 0 - fail_msg: "Cannot locate extracted ActivityWatch release directory." + - name: Найти бинарный файл AW server + ansible.builtin.find: + paths: "{{ aw_release_dir }}" + recurse: true + file_type: file + patterns: + - aw-server-rust + - aw-server + register: aw_server_binary_find - - name: Sync release content to /opt/activitywatch - ansible.builtin.command: - cmd: "rsync -a --delete {{ aw_release_extracted }}/ /opt/activitywatch/" + - name: Найти index.html WebUI + ansible.builtin.find: + paths: "{{ aw_release_dir }}" + recurse: true + file_type: file + patterns: + - index.html + register: aw_webui_index_find - - name: Copy bootstrap files from repository + - name: Сохранить пути распакованного релиза (binary + webui index) + 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) + | select('match', '.*/aw-server-rust$') | list | first + ) + | default( + ( + (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) + ), + true + ) + ) | default('') + }} + aw_webui_index_path: >- + {{ + ( + ( + (aw_webui_index_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list) + | select('search', '/static/index\\.html$') | list | first + ) + | default( + ( + (aw_webui_index_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) + ), + true + ) + ) | default('') + }} + + - name: Сохранить каталог WebUI (dirname index.html) + ansible.builtin.set_fact: + aw_webui_source_path: "{{ aw_webui_index_path | dirname }}" + + - name: Проверить, что компоненты релиза найдены + ansible.builtin.assert: + that: + - aw_release_extracted is defined + - aw_release_extracted | length > 0 + - 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: Создать каталог установленного релиза + 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 {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/" + + - 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: - src: "{{ item.src }}" - dest: "{{ item.dest }}" - mode: "{{ item.mode }}" - loop: - - { src: "{{ aw_repo_root }}/aw-server/activitywatch-server.service", dest: "/etc/systemd/system/activitywatch-server.service", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" } + 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: - - Reload systemd - - Restart activitywatch + - Перезагрузить systemd + - Перезапустить 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: (Check mode) Пропустить WebUI patch и запуск сервиса + ansible.builtin.debug: + msg: "ansible_check_mode=true: WebUI patch + service start + API checks are skipped." + when: ansible_check_mode - - name: Insert RU patch scripts into index.html - ansible.builtin.replace: - path: "{{ aw_server_webui_dir }}/index.html" - regexp: '' - replace: '' + - name: Применить WebUI RU patch и запустить сервис + when: not ansible_check_mode + block: + - 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/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" } - - name: Insert RU patch loader before body end - ansible.builtin.replace: - path: "{{ aw_server_webui_dir }}/index.html" - regexp: '' - replace: '' + - name: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh + ansible.builtin.file: + path: /root/bootstrap + state: directory + mode: "0755" - - name: Write /etc/activitywatch/aw-server.env - ansible.builtin.copy: - dest: /etc/activitywatch/aw-server.env - mode: "0640" - content: | - AW_SERVER_HOST={{ aw_server_bind_host }} - AW_SERVER_PORT={{ aw_server_port }} - AW_DATA_DIR={{ aw_server_data_dir }} - AW_LOG_DIR={{ aw_server_log_dir }} - AW_WEBUI_DIR={{ aw_server_webui_dir }} - AW_SERVER_USER={{ aw_server_user }} - AW_SERVER_GROUP={{ aw_server_group }} + - name: Скопировать RU patch файлы для apply_webui_ru_patch.sh (хотфиксы compiled JS чанков) + ansible.builtin.copy: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + mode: "{{ item.mode }}" + loop: + - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/root/bootstrap/aw-ru-patch.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/root/bootstrap/aw-sw-cleanup.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" } - - name: Enable and start service - ansible.builtin.systemd: - name: activitywatch-server.service - enabled: true - state: restarted - daemon_reload: true + - name: Скопировать apply_webui_ru_patch.sh скрипт + ansible.builtin.copy: + src: "{{ aw_repo_root }}/aw-server/apply_webui_ru_patch.sh" + dest: /opt/activitywatch/aw-server/apply_webui_ru_patch.sh + mode: "0755" - - name: Wait for API + - name: Записать /etc/activitywatch/aw-server.env перед хотфиксами + ansible.builtin.copy: + dest: /etc/activitywatch/aw-server.env + mode: "0640" + owner: root + group: root + content: | + AW_SERVER_BIND_HOST={{ aw_server_bind_host }} + AW_SERVER_PORT={{ aw_server_port }} + AW_SERVER_DATA_DIR={{ aw_server_data_dir }} + AW_SERVER_DB_PATH={{ aw_server_db_path }} + 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 }} + XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share + XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config + + - name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper) + ansible.builtin.command: + cmd: "/opt/activitywatch/aw-server/apply_webui_ru_patch.sh" + register: apply_ru_patch_result + failed_when: false + + - name: Вывести результат применения хотфиксов + ansible.builtin.debug: + msg: "apply_webui_ru_patch.sh: {{ apply_ru_patch_result.stdout }}" + + - name: Проверить наличие index.html после копирования + ansible.builtin.stat: + path: "{{ aw_server_webui_dir }}/index.html" + register: aw_webui_ru_index + + - 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: ']+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*>' + replace: '' + + - name: Добавить cleanup script RU patch в index.html + ansible.builtin.replace: + path: "{{ aw_server_webui_dir }}/index.html" + regexp: '' + replace: '' + + - name: Добавить загрузчик RU patch перед закрытием body + ansible.builtin.replace: + path: "{{ aw_server_webui_dir }}/index.html" + regexp: '' + replace: '' + + - name: Скопировать merge script AW DB на сервер + ansible.builtin.copy: + src: "{{ aw_repo_root }}/scripts/merge_aw_server_dbs.py" + dest: /usr/local/bin/merge_aw_server_dbs.py + owner: root + group: root + mode: "0755" + + - name: Проверить наличие legacy root DB + ansible.builtin.stat: + path: /root/.local/share/activitywatch/aw-server-rust/sqlite.db + register: aw_legacy_root_db + + - name: Проверить наличие target DB + ansible.builtin.stat: + path: "{{ aw_server_db_path }}" + register: aw_target_db + + - name: Остановить сервис перед merge server DB + ansible.builtin.systemd: + name: activitywatch-server.service + state: stopped + when: aw_legacy_root_db.stat.exists | default(false) + + - name: Создать backup каталоги server DB + ansible.builtin.file: + path: "{{ aw_server_data_dir }}/backups/db" + state: directory + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "0755" + when: aw_legacy_root_db.stat.exists | default(false) + + - name: Backup target DB перед merge + ansible.builtin.copy: + remote_src: true + src: "{{ aw_server_db_path }}" + dest: "{{ aw_server_data_dir }}/backups/db/target-before-merge-{{ ansible_date_time.iso8601_basic_short }}.sqlite.db" + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "0644" + when: + - aw_legacy_root_db.stat.exists | default(false) + - aw_target_db.stat.exists | default(false) + + - name: Backup legacy root DB перед merge + ansible.builtin.copy: + remote_src: true + src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db + dest: "{{ aw_server_data_dir }}/backups/db/legacy-root-{{ ansible_date_time.iso8601_basic_short }}.sqlite.db" + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "0644" + when: aw_legacy_root_db.stat.exists | default(false) + + - name: Merge legacy root DB в target DB + ansible.builtin.command: + argv: + - python3 + - /usr/local/bin/merge_aw_server_dbs.py + - --base + - /root/.local/share/activitywatch/aw-server-rust/sqlite.db + - --overlay + - "{{ aw_server_db_path }}" + - --output + - "{{ aw_server_db_path }}.merged" + when: + - aw_legacy_root_db.stat.exists | default(false) + - aw_target_db.stat.exists | default(false) + + - name: Install merged DB as active target DB + ansible.builtin.copy: + remote_src: true + src: "{{ aw_server_db_path }}.merged" + dest: "{{ aw_server_db_path }}" + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "0644" + when: + - aw_legacy_root_db.stat.exists | default(false) + - aw_target_db.stat.exists | default(false) + + - name: Скопировать legacy root DB в target DB если target ещё не существует + ansible.builtin.copy: + remote_src: true + src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db + dest: "{{ aw_server_db_path }}" + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "0644" + when: + - aw_legacy_root_db.stat.exists | default(false) + - not (aw_target_db.stat.exists | default(false)) + + - name: Записать aw-server-rust config.toml с разрешёнными CORS origin + ansible.builtin.copy: + dest: "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust/config.toml" + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "0644" + content: | + cors = [ + {% for origin in aw_server_cors_origins | default([]) %} + "{{ origin }}"{% if not loop.last %},{% endif %} + {% endfor %} + ] + + - name: Включить и запустить сервис + ansible.builtin.systemd: + name: activitywatch-server.service + enabled: true + state: restarted + daemon_reload: true + + - name: Дождаться ответа API + ansible.builtin.uri: + url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info" + method: GET + status_code: 200 + register: aw_api + retries: 10 + delay: 3 + until: aw_api.status == 200 + + - name: Считать текущие server-side settings ansible.builtin.uri: - url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info" + url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/" method: GET status_code: 200 - register: aw_api - retries: 10 - delay: 3 - until: aw_api.status == 200 + register: aw_settings_current + when: aw_apply_worktime_settings | default(false) | bool - - name: Apply baseline worktime settings (classes) + - name: Считать текущие server-side views + ansible.builtin.uri: + url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/views" + method: GET + status_code: 200 + register: aw_views_current + when: aw_apply_worktime_settings | default(false) | bool + + - name: Считать текущие server-side classes + ansible.builtin.uri: + url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/classes" + method: GET + status_code: 200 + register: aw_classes_current + when: aw_apply_worktime_settings | default(false) | bool + + - name: Создать backup текущих server-side settings/views/classes + ansible.builtin.copy: + dest: "{{ aw_server_data_dir }}/backups/{{ item.name }}-{{ ansible_date_time.iso8601_basic_short }}.json" + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "0644" + content: "{{ item.payload | to_nice_json }}" + loop: + - name: settings + payload: "{{ aw_settings_current.json | default({}) }}" + - name: views + payload: "{{ aw_views_current.json | default(none) }}" + - name: classes + payload: "{{ aw_classes_current.json | default(none) }}" + when: aw_apply_worktime_settings | default(false) | bool + + - name: Настроить DLP Aggregator (Phase 2) + block: + - name: Создать каталог для скриптов + ansible.builtin.file: + path: "/opt/activitywatch/scripts" + state: directory + owner: root + group: root + mode: "0755" + + - name: Скопировать агрегатор событий DLP + ansible.builtin.copy: + src: "{{ aw_repo_root }}/scripts/aggregate_dlp_events.py" + dest: "/opt/activitywatch/scripts/aggregate_dlp_events.py" + owner: root + group: root + mode: "0755" + + - name: Установить systemd unit для агрегатора + ansible.builtin.copy: + dest: /etc/systemd/system/activitywatch-dlp-aggregator.service + content: | + [Unit] + Description=ActivityWatch DLP Event Aggregator + After=activitywatch-server.service + + [Service] + Type=oneshot + User={{ aw_server_user }} + WorkingDirectory={{ aw_server_data_dir }} + ExecStart=/usr/bin/python3 /opt/activitywatch/scripts/aggregate_dlp_events.py \ + --aw-url http://127.0.0.1:{{ aw_server_port }}/api/0 \ + --sqlite-path {{ aw_server_data_dir }}/dlp_warehouse.sqlite \ + --state-path {{ aw_server_data_dir }}/dlp-aggregator-state.json + + [Install] + WantedBy=multi-user.target + + - name: Установить systemd timer для агрегатора + ansible.builtin.copy: + dest: /etc/systemd/system/activitywatch-dlp-aggregator.timer + content: | + [Unit] + Description=Run ActivityWatch DLP Aggregator every 5 minutes + + [Timer] + OnBootSec=1min + OnUnitActiveSec=5min + AccuracySec=1s + + [Install] + WantedBy=timers.target + + - name: Включить и запустить таймер агрегатора + ansible.builtin.systemd: + name: activitywatch-dlp-aggregator.timer + enabled: true + state: started + daemon_reload: true + + - 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 }}" - aw_worktime_to_h: "{{ (aw_worktime_to | default('17:00')).split(':')[0] | int }}" - aw_worktime_to_m: "{{ (aw_worktime_to | default('17:00')).split(':')[1] | int }}" aw_worktime_duration_default_derived: >- {{ ( ( - ((aw_worktime_to_h | int) * 60 + (aw_worktime_to_m | int)) - - ((aw_worktime_from_h | int) * 60 + (aw_worktime_from_m | int)) + (((aw_worktime_to | default('17:00')).split(':')[0] | int) * 60 + ((aw_worktime_to | default('17:00')).split(':')[1] | int)) - + (((aw_worktime_from | default('08:00')).split(':')[0] | int) * 60 + ((aw_worktime_from | default('08:00')).split(':')[1] | int)) ) * 60 ) }} 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,38 +600,64 @@ }} 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 - body: "{{ aw_worktime_start_of_day }}" - body_format: json - status_code: 200 + body: "\"{{ aw_worktime_start_of_day }}\"" + headers: + Content-Type: application/json + status_code: [200, 201] 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 - body: "{{ aw_worktime_duration_default_effective }}" - body_format: json - status_code: 200 + body: "{{ aw_worktime_duration_default_effective | string }}" + headers: + Content-Type: application/json + status_code: [200, 201] when: aw_apply_worktime_settings | default(false) | bool + - name: Применить always_active_pattern для fallback без AFK + ansible.builtin.uri: + url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/always_active_pattern" + method: POST + body: "\"{{ aw_server_always_active_pattern }}\"" + headers: + Content-Type: application/json + status_code: [200, 201] + when: + - aw_apply_worktime_settings | default(false) | bool + - (aw_server_always_active_pattern | default('') | string | length) > 0 + + - name: Применить landingpage профиля + ansible.builtin.uri: + url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/landingpage" + method: POST + body: "\"{{ aw_server_landingpage }}\"" + headers: + Content-Type: application/json + status_code: [200, 201] + when: + - aw_apply_worktime_settings | default(false) | bool + - (aw_server_landingpage | default('') | string | length) > 0 + 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 diff --git a/ansible/deploy_aw_windows.yml b/ansible/deploy_aw_windows.yml new file mode 100644 index 0000000..92a352b --- /dev/null +++ b/ansible/deploy_aw_windows.yml @@ -0,0 +1,253 @@ +--- +- 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_file_ops_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 + - file-operations-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: Нормализовать кодировку PowerShell файлов (UTF-8 BOM для Windows PowerShell) + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $toolkitDir = "{{ aw_windows_deploy_root }}\windows" + $encIn = New-Object System.Text.UTF8Encoding($false) + $encOut = New-Object System.Text.UTF8Encoding($true) + Get-ChildItem -LiteralPath $toolkitDir -File -Include *.ps1,*.psm1,*.psd1 | ForEach-Object { + $text = [System.IO.File]::ReadAllText($_.FullName, $encIn) + [System.IO.File]::WriteAllText($_.FullName, $text, $encOut) + } + + - 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' }} + FileOpsEnabled = {{ '$true' if (aw_windows_file_ops_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 Launch tasks вне текущего deployment-config + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $config = Get-Content -Raw -LiteralPath "{{ aw_windows_state_root }}\deployment-config.json" | ConvertFrom-Json + $desired = @($config.userTasks | ForEach-Object { [string]$_.LaunchTaskName }) + foreach ($task in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch *' })) { + if ($desired -notcontains [string]$task.TaskName) { + Unregister-ScheduledTask -TaskName $task.TaskName -Confirm:$false -ErrorAction SilentlyContinue + & cmd.exe /c "schtasks /Delete /TN `"$($task.TaskName)`" /F >nul 2>&1" | Out-Null + } + } + + - 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 + - aw_windows_hostname_result.stdout is defined + ansible.builtin.set_fact: + aw_windows_api_smoke_check_bucket_effective: "aw-watcher-afk_{{ aw_windows_hostname_result.stdout | trim }}" + + - name: Выполнить AW API smoke-check (проверка наличия свежих событий в AFK бакете) + when: + - aw_windows_api_smoke_check_enabled | bool + - aw_windows_afk_enabled | bool + 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 + status_code: 200 + register: aw_windows_api_smoke_result + until: aw_windows_api_smoke_result.json | length > 0 + retries: 5 + delay: 5 + ignore_errors: true + + - name: Валидировать развёртывание на эндпоинте + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $result = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" ` + -ConfigPath "{{ aw_windows_state_root }}\deployment-config.json" + $result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8 + return $result + + - name: Создать локальную директорию для отчётов валидации + ansible.builtin.file: + path: "{{ aw_windows_validation_local_dir }}" + state: directory + mode: "0755" + delegate_to: localhost + + - name: Стянуть отчёт валидации с эндпоинта + 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.shell: | + python3 - <<'PY' + import json, sys + with open('{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json', 'r') as f: + data = json.load(f) + if not data.get('overallOk', False): + print(f"Validation failed for {{ inventory_hostname }}: {data.get('summary', 'Unknown error')}") + sys.exit(1) + PY + delegate_to: localhost + when: aw_windows_fail_on_validation_error | bool diff --git a/ansible/deploy_aw_windows_phase2.yml b/ansible/deploy_aw_windows_phase2.yml deleted file mode 100644 index c772f44..0000000 --- a/ansible/deploy_aw_windows_phase2.yml +++ /dev/null @@ -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" diff --git a/ansible/group_vars/all.example.yml b/ansible/group_vars/all.example.yml index 8a41775..2091344 100644 --- a/ansible/group_vars/all.example.yml +++ b/ansible/group_vars/all.example.yml @@ -4,21 +4,32 @@ aw_server_bind_host: "0.0.0.0" aw_server_port: 5600 aw_server_webui_dir: "/opt/activitywatch/webui-ru" aw_server_data_dir: "/var/lib/activitywatch" +aw_server_db_path: "/var/lib/activitywatch/.local/share/activitywatch/aw-server-rust/sqlite.db" 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. -aw_apply_worktime_settings: false +# Применить базовые категории и views для рабочего времени через AW settings API. +# При прод-обновлениях это нужно оставлять включённым, иначе UI остаётся без views/classes. +aw_apply_worktime_settings: true -# 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. +# Дополнительные origin для aw-server-rust CORS. +# Обязательно включите тот origin, с которого реально открывается Web UI. +aw_server_cors_origins: + - "http://127.0.0.1:5600" + - "http://localhost:5600" + - "http://10.10.10.13:5600" + - "http://aw-server:5600" + +# Опциональные значения периода рабочего времени в 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 }}" +aw_server_always_active_pattern: "aw-watcher-window" +aw_server_landingpage: "/activity/SHARKON2025/view/" diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml new file mode 100644 index 0000000..2452c60 --- /dev/null +++ b/ansible/group_vars/all.yml @@ -0,0 +1,27 @@ +aw_server_version: "v0.13.2" +aw_server_download_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-linux-x86_64.zip" +aw_server_bind_host: "0.0.0.0" +aw_server_port: 5600 +aw_server_webui_dir: "/opt/activitywatch/webui-ru" +aw_server_data_dir: "/var/lib/activitywatch" +aw_server_db_path: "/var/lib/activitywatch/.local/share/activitywatch/aw-server-rust/sqlite.db" +aw_server_log_dir: "/var/log/activitywatch" +aw_server_user: "activitywatch" +aw_server_group: "activitywatch" + +aw_repo_root: "{{ playbook_dir | dirname }}" + +# Optional: apply worktime settings via server-side settings API. +aw_apply_worktime_settings: true + +aw_server_cors_origins: + - "http://127.0.0.1:5600" + - "http://localhost:5600" + - "http://10.10.10.13:5600" + - "http://aw-server:5600" + +aw_worktime_from: "08:00" +aw_worktime_to: "17:00" +aw_worktime_start_of_day: "{{ aw_worktime_from }}" +aw_server_always_active_pattern: "aw-watcher-window" +aw_server_landingpage: "/activity/SHARKON2025/view/" diff --git a/ansible/group_vars/aw_server.yml b/ansible/group_vars/aw_server.yml new file mode 100644 index 0000000..73b9934 --- /dev/null +++ b/ansible/group_vars/aw_server.yml @@ -0,0 +1,9 @@ +# Secret handling: +# - put the real SSH password into env var before running Ansible: +# export AW_SSH_PASSWORD='...' +ansible_password: "{{ lookup('env', 'AW_SSH_PASSWORD') }}" + +ansible_become: true +ansible_become_method: sudo +# If sudo password differs, set AW_SUDO_PASSWORD. Otherwise it will reuse AW_SSH_PASSWORD. +ansible_become_password: "{{ lookup('env', 'AW_SUDO_PASSWORD') | default(lookup('env', 'AW_SSH_PASSWORD'), true) }}" diff --git a/ansible/group_vars/aw_windows.yml b/ansible/group_vars/aw_windows.yml new file mode 100644 index 0000000..4ab2f5f --- /dev/null +++ b/ansible/group_vars/aw_windows.yml @@ -0,0 +1,53 @@ +# Secret handling: +# - put the real password into env var before running Ansible: +# export AW_WINRM_PASSWORD='...' +ansible_password: "{{ lookup('env', 'AW_WINRM_PASSWORD') }}" + +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_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_file_ops_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_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" + +aw_windows_api_smoke_check_enabled: true +aw_windows_api_smoke_check_bucket: "" +aw_windows_api_smoke_check_limit: 10 + diff --git a/ansible/group_vars/windows.example.yml b/ansible/group_vars/windows.example.yml index 3b6a958..707b658 100644 --- a/ansible/group_vars/windows.example.yml +++ b/ansible/group_vars/windows.example.yml @@ -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,11 +18,12 @@ 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_file_ops_enabled: true aw_windows_local_agent_logs_enabled: false aw_windows_incident_capture_enabled: true aw_windows_incident_screenshot_enabled: true @@ -29,5 +34,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_. +# Задайте явное значение только если watcher пишет в нестандартный bucket. +aw_windows_api_smoke_check_enabled: true +aw_windows_api_smoke_check_bucket: "" +aw_windows_api_smoke_check_limit: 10 diff --git a/ansible/install_full_stack.yml b/ansible/install_full_stack.yml index 18b4052..cd3a7f6 100644 --- a/ansible/install_full_stack.yml +++ b/ansible/install_full_stack.yml @@ -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 diff --git a/ansible/inventory.example.ini b/ansible/inventory.example.ini index da44e90..e1c77ff 100644 --- a/ansible/inventory.example.ini +++ b/ansible/inventory.example.ini @@ -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 diff --git a/ansible/inventory.ini b/ansible/inventory.ini new file mode 100644 index 0000000..3aacfd6 --- /dev/null +++ b/ansible/inventory.ini @@ -0,0 +1,14 @@ +[proxmox] +# Optional. Leave empty if you don't use Proxmox provisioning from this repo. +# pve-main ansible_host=10.10.10.2 ansible_user=igor ansible_port=22 + +[aw_server] +aw-server ansible_host=10.10.10.13 ansible_user=igor ansible_port=22 + +[aw_windows] +# Note: on RU-localized Windows the built-in admin account name is often "Администратор". +rdp-prod ansible_host=192.168.100.21 ansible_user=Администратор ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore + +[aw_pfsense_pollers] +# Optional. +# pfsense-poller1 ansible_host=192.168.100.30 ansible_user=root ansible_port=22 diff --git a/ansible/post_validate_aw_windows.yml b/ansible/post_validate_aw_windows.yml new file mode 100644 index 0000000..2f28634 --- /dev/null +++ b/ansible/post_validate_aw_windows.yml @@ -0,0 +1,90 @@ +--- +- name: Post-deploy validation for Windows/RDP AWatch-rus + hosts: aw_windows + gather_facts: false + + vars: + 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 + + tasks: + - 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 + 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 + 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 + 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 validation failed. Report: {{ 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: + - "Validation OK on {{ inventory_hostname }}." + - "Report: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json" + diff --git a/ansible/provision_proxmox_ct_and_deploy_aw.yml b/ansible/provision_proxmox_ct_and_deploy_aw.yml index 2a932a7..dd9dadf 100644 --- a/ansible/provision_proxmox_ct_and_deploy_aw.yml +++ b/ansible/provision_proxmox_ct_and_deploy_aw.yml @@ -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 }}" diff --git a/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml b/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml index b228f66..44564da 100644 --- a/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml +++ b/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml @@ -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 }}" diff --git a/ansible/tasks/provision_ct_and_deploy_aw.yml b/ansible/tasks/provision_ct_and_deploy_aw.yml index 4cceb17..975e6ef 100644 --- a/ansible/tasks/provision_ct_and_deploy_aw.yml +++ b/ansible/tasks/provision_ct_and_deploy_aw.yml @@ -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 }}" diff --git a/aw-server/activitywatch-server.service b/aw-server/activitywatch-server.service index e8f26e3..89d31e3 100755 --- a/aw-server/activitywatch-server.service +++ b/aw-server/activitywatch-server.service @@ -9,7 +9,7 @@ EnvironmentFile=/etc/activitywatch/aw-server.env User=__AW_SERVER_USER__ Group=__AW_SERVER_GROUP__ WorkingDirectory=__AW_SERVER_DATA_DIR__ -ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT"' +ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT" --dbpath "$AW_SERVER_DB_PATH" --webpath "$AW_SERVER_WEBUI_DIR"' Restart=on-failure RestartSec=5s StateDirectory=activitywatch @@ -17,7 +17,7 @@ LogsDirectory=activitywatch NoNewPrivileges=true PrivateTmp=true ProtectSystem=full -ProtectHome=true +ProtectHome=read-only LimitNOFILE=65535 [Install] diff --git a/aw-server/apply_webui_ru_patch.sh b/aw-server/apply_webui_ru_patch.sh index 1b7c240..37a898a 100755 --- a/aw-server/apply_webui_ru_patch.sh +++ b/aw-server/apply_webui_ru_patch.sh @@ -24,7 +24,7 @@ TRENDS_REPLACEMENT='this.activityStore.ensure_loaded(r)' TIMESPIRAL_NEEDLE='start:new Date("2022-08-08")' TIMESPIRAL_REPLACEMENT='start:new Date(Date.now()-12*36e5)' CATEGORY_HELPER_NEEDLE='hostname:t.hostnameChoices[0]' -CATEGORY_HELPER_REPLACEMENT='hostname:t.hostnameChoices.filter((function(t){return"unknown"!==t}))[0]||t.hostnameChoices[0]' +CATEGORY_HELPER_REPLACEMENT='hostname:t.hostnameChoices.filter((function(t){return"unknown"!==t&&"undefined"!==t}))[0]||t.hostnameChoices[0]' [[ -f "$PATCH_JS_SRC" ]] || { echo "missing $PATCH_JS_SRC" >&2; exit 1; } [[ -f "$SW_CLEANUP_SRC" ]] || { echo "missing $SW_CLEANUP_SRC" >&2; exit 1; } diff --git a/docs/FULL_DEPLOYMENT_MANUAL_RU.md b/docs/FULL_DEPLOYMENT_MANUAL_RU.md index 62224ec..d850ac9 100755 --- a/docs/FULL_DEPLOYMENT_MANUAL_RU.md +++ b/docs/FULL_DEPLOYMENT_MANUAL_RU.md @@ -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\` Задачи планировщика: diff --git a/docs/dlp-aggregator.md b/docs/dlp-aggregator.md new file mode 100644 index 0000000..c54edbb --- /dev/null +++ b/docs/dlp-aggregator.md @@ -0,0 +1,103 @@ +# Central DLP aggregator prototype + +`scripts/aggregate_dlp_events.py` collects Phase 2 DLP telemetry from ActivityWatch buckets and stores normalized rows in one database for Grafana/SIEM-style reporting. + +## Streams + +The prototype reads: + +- `aw-file-operations_*` (`aw.file.operation`) — file create/delete/rename telemetry, including `archiveHint`. +- `aw-dlp-incidents_*` (`aw.dlp.incident`) — browser/endpoint DLP incidents and screenshot metadata when available. + +## SQLite smoke test + +SQLite is the default so the collector can be tested without deploying PostgreSQL: + +```bash +python3 scripts/aggregate_dlp_events.py \ + --aw-url http://10.10.10.13:5600/api/0 \ + --sqlite-path data/dlp-events.sqlite3 \ + --lookback-hours 24 +``` + +Useful checks: + +```bash +sqlite3 data/dlp-events.sqlite3 \ + "select stream_type, hostname, count(*) from dlp_events group by 1,2 order by 3 desc;" + +sqlite3 data/dlp-events.sqlite3 \ + "select event_ts, hostname, username, file_path from dlp_file_operations where archive_hint = 1 order by event_ts desc limit 20;" +``` + +## PostgreSQL mode + +For centralized reporting, pass a DSN through an environment variable instead of committing secrets: + +```bash +export DLP_AGGREGATOR_POSTGRES_DSN='postgresql://aw_dlp:${PASSWORD}@postgres.internal:5432/aw_dlp' +python3 -m pip install 'psycopg[binary]' +python3 scripts/aggregate_dlp_events.py \ + --aw-url http://10.10.10.13:5600/api/0 +``` + +Minimum database bootstrap: + +```sql +create database aw_dlp; +create user aw_dlp_ingest with password ''; +grant connect on database aw_dlp to aw_dlp_ingest; +grant usage, create on schema public to aw_dlp_ingest; +``` + +The script creates: + +- table `dlp_events` +- view `dlp_file_operations` +- view `dlp_incidents` + +## Incremental state + +By default, the aggregator stores the last successful end timestamp in: + +```text +data/dlp-aggregator-state.json +``` + +Future runs resume from that timestamp with a small overlap window to avoid missing late events. Duplicate inserts are ignored by `(bucket_id, event_id)`. + +## Scheduling example + +Cron every minute: + +```cron +* * * * * cd /opt/AWatch-rus && /usr/bin/python3 scripts/aggregate_dlp_events.py --aw-url http://10.10.10.13:5600/api/0 >> /var/log/aw-dlp-aggregator.log 2>&1 +``` + +## Example Grafana queries + +Archive creation by user: + +```sql +select + date_trunc('minute', event_ts) as time, + hostname, + username, + count(*) as archives +from dlp_file_operations +where archive_hint = true +group by 1, 2, 3 +order by 1 desc; +``` + +DLP incidents by severity: + +```sql +select + date_trunc('hour', event_ts) as time, + severity, + count(*) as incidents +from dlp_incidents +group by 1, 2 +order by 1 desc; +``` diff --git a/docs/dlp-gap-analysis.md b/docs/dlp-gap-analysis.md index c799ac7..354af51 100644 --- a/docs/dlp-gap-analysis.md +++ b/docs/dlp-gap-analysis.md @@ -29,8 +29,26 @@ - USB/print/clipboard collectors (endpoint signals) — внедрено. - Incident pipeline расширен на endpoint события — внедрено. -- File-operation telemetry (create/copy/archive/upload hints) — в backlog. -- Central incident aggregation/export — в backlog. +- File-operation telemetry (create/delete/rename/archive hints) — прототип внедрён (`windows/file-operations-collector.ps1`). +- Central incident aggregation/export — прототип внедрён (`scripts/aggregate_dlp_events.py`, `docs/dlp-aggregator.md`). + +### Phase 2.5 — Enforcement (внедрено) + +- USB write-block (`Set-Disk -IsReadOnly`) при `action: "block"` — внедрено. +- Print job cancel (`Remove-CimInstance Win32_PrintJob`) при `action: "block"` — внедрено. +- Clipboard clear (`Set-Clipboard -Value $null`) при `action: "block"` — внедрено. +- Windows balloon notification пользователю при блокировке — внедрено. +- Телеметрия enforcement (`enforced: true/false` в incident heartbeat) — внедрено. +- Документация: `docs/dlp-enforcement.md`. + +### Phase 2.5 — Email Outbound Collector (внедрено) + +- Мониторинг исходящей почты через Outlook COM (Sent Items polling) — внедрено. +- SMTP network connection detection (порты 25/587/465/2525) — внедрено. +- DLP-правила `endpoint.email[]` (regex по теме, получателям, вложениям, externalOnly) — внедрено. +- Enforcement: перемещение в Drafts при `action: "block"` (Outlook mode) — внедрено. +- Приватность: тема/получатели как SHA256, тело не читается — внедрено. +- Документация: `docs/email-outbound-collector.md`. ### Phase 2.5 — Enforcement (внедрено) diff --git a/docs/windows/deployment.md b/docs/windows/deployment.md index aa6686f..23b3a2d 100755 --- a/docs/windows/deployment.md +++ b/docs/windows/deployment.md @@ -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 = \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`. diff --git a/docs/windows/ensemble.md b/docs/windows/ensemble.md index d0f5ccb..4333fa8 100644 --- a/docs/windows/ensemble.md +++ b/docs/windows/ensemble.md @@ -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 ``` diff --git a/docs/windows/troubleshooting.md b/docs/windows/troubleshooting.md index 6e0c631..5cc9482 100755 --- a/docs/windows/troubleshooting.md +++ b/docs/windows/troubleshooting.md @@ -23,7 +23,7 @@ Start-ScheduledTask -TaskName 'ActivityWatch Launch [CONTOSO_user01]' - Скрипт работает через UI Automation и foreground window. - Некоторые браузеры/страницы могут скрывать адресную строку или блокировать UIA. -- Проверьте лог `C:\ProgramData\ActivityWatch\logs\browser-domains-.log`. +- Проверьте лог `C:\ProgramData\AWatch-rus\logs\browser-domains-.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 diff --git a/docs/windows/validation.md b/docs/windows/validation.md index 068948e..72ae9e5 100755 --- a/docs/windows/validation.md +++ b/docs/windows/validation.md @@ -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_`) событие видно в `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' ``` Ожидаемо: diff --git a/install-kit-awindows-20260427-211240/MANIFEST.txt b/install-kit-awindows-20260427-211240/MANIFEST.txt index da85d93..4f5c2f0 100644 --- a/install-kit-awindows-20260427-211240/MANIFEST.txt +++ b/install-kit-awindows-20260427-211240/MANIFEST.txt @@ -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 diff --git a/install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt b/install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt index 204ed7a..c59015e 100644 --- a/install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt +++ b/install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt @@ -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. diff --git a/install-kit-awindows-20260427-211240/ansible/README.md b/install-kit-awindows-20260427-211240/ansible/README.md index b8a82d7..b22fc6c 100644 --- a/install-kit-awindows-20260427-211240/ansible/README.md +++ b/install-kit-awindows-20260427-211240/ansible/README.md @@ -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_` и ожидает свежие `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_`; +- `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`. diff --git a/install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml b/install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml index 3eea27c..34aa29d 100644 --- a/install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml +++ b/install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml @@ -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 diff --git a/install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml b/install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml index d5f7fda..7f372c1 100644 --- a/install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml +++ b/install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml @@ -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,223 @@ 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/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" } + + - name: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh + ansible.builtin.file: + path: /root/bootstrap + state: directory + mode: "0755" + + - name: Скопировать RU patch файлы для apply_webui_ru_patch.sh (хотфиксы compiled JS чанков) ansible.builtin.copy: src: "{{ item.src }}" dest: "{{ item.dest }}" mode: "{{ item.mode }}" loop: - - { src: "{{ aw_repo_root }}/aw-server/activitywatch-server.service", dest: "/etc/systemd/system/activitywatch-server.service", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" } - notify: - - Reload systemd - - Restart activitywatch + - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/root/bootstrap/aw-ru-patch.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/root/bootstrap/aw-sw-cleanup.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" } - - name: Copy WebUI index template from installed distribution + - name: Скопировать apply_webui_ru_patch.sh скрипт ansible.builtin.copy: - remote_src: true - src: "/opt/activitywatch/aw-webui/index.html" - dest: "{{ aw_server_webui_dir }}/index.html" - mode: "0644" + src: "{{ aw_repo_root }}/aw-server/apply_webui_ru_patch.sh" + dest: /opt/activitywatch/aw-server/apply_webui_ru_patch.sh + mode: "0755" - - name: Insert RU patch scripts into index.html + - name: Записать /etc/activitywatch/aw-server.env перед хотфиксами + ansible.builtin.copy: + dest: /etc/activitywatch/aw-server.env + mode: "0640" + owner: root + group: root + content: | + AW_SERVER_BIND_HOST={{ aw_server_bind_host }} + AW_SERVER_PORT={{ aw_server_port }} + AW_SERVER_DATA_DIR={{ aw_server_data_dir }} + AW_SERVER_DB_PATH={{ aw_server_db_path }} + 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 }} + XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share + XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config + + - name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper) + ansible.builtin.command: + cmd: "/opt/activitywatch/aw-server/apply_webui_ru_patch.sh" + register: apply_ru_patch_result + failed_when: false + + - name: Вывести результат применения хотфиксов + ansible.builtin.debug: + msg: "apply_webui_ru_patch.sh: {{ apply_ru_patch_result.stdout }}" + + - name: Проверить наличие index.html после копирования + ansible.builtin.stat: + path: "{{ aw_server_webui_dir }}/index.html" + register: aw_webui_ru_index + + - 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: ']+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*>' + replace: '' + + - name: Добавить cleanup script RU patch в index.html ansible.builtin.replace: path: "{{ aw_server_webui_dir }}/index.html" regexp: '' replace: '' - - name: Insert RU patch loader before body end + - name: Добавить загрузчик RU patch перед закрытием body ansible.builtin.replace: path: "{{ aw_server_webui_dir }}/index.html" regexp: '' replace: '' - - name: Write /etc/activitywatch/aw-server.env - ansible.builtin.copy: - dest: /etc/activitywatch/aw-server.env - mode: "0640" - content: | - AW_SERVER_HOST={{ aw_server_bind_host }} - AW_SERVER_PORT={{ aw_server_port }} - AW_DATA_DIR={{ aw_server_data_dir }} - AW_LOG_DIR={{ aw_server_log_dir }} - AW_WEBUI_DIR={{ aw_server_webui_dir }} - AW_SERVER_USER={{ aw_server_user }} - AW_SERVER_GROUP={{ aw_server_group }} - - - name: Enable and start service + - 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 +292,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 +327,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 +337,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 +354,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 +364,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 diff --git a/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml b/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml new file mode 100644 index 0000000..5d18277 --- /dev/null +++ b/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml @@ -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" diff --git a/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows_phase2.yml b/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows_phase2.yml deleted file mode 100644 index 911e3dc..0000000 --- a/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows_phase2.yml +++ /dev/null @@ -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" diff --git a/install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml b/install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml index 8a41775..27d5703 100644 --- a/install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml +++ b/install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml @@ -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 }}" diff --git a/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml b/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml index 3b6a958..30c20c3 100644 --- a/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml +++ b/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml @@ -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_. +# Задайте явное значение только если watcher пишет в нестандартный bucket. +aw_windows_api_smoke_check_enabled: true +aw_windows_api_smoke_check_bucket: "" +aw_windows_api_smoke_check_limit: 10 diff --git a/install-kit-awindows-20260427-211240/ansible/install_full_stack.yml b/install-kit-awindows-20260427-211240/ansible/install_full_stack.yml new file mode 100644 index 0000000..cd3a7f6 --- /dev/null +++ b/install-kit-awindows-20260427-211240/ansible/install_full_stack.yml @@ -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 diff --git a/install-kit-awindows-20260427-211240/ansible/inventory.example.ini b/install-kit-awindows-20260427-211240/ansible/inventory.example.ini index f73f446..e1c77ff 100644 --- a/install-kit-awindows-20260427-211240/ansible/inventory.example.ini +++ b/install-kit-awindows-20260427-211240/ansible/inventory.example.ini @@ -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 diff --git a/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml b/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml index 2a932a7..dd9dadf 100644 --- a/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml +++ b/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml @@ -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 }}" diff --git a/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml b/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml index b228f66..44564da 100644 --- a/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml +++ b/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml @@ -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 }}" diff --git a/install-kit-awindows-20260427-211240/ansible/tasks/provision_ct_and_deploy_aw.yml b/install-kit-awindows-20260427-211240/ansible/tasks/provision_ct_and_deploy_aw.yml index 4cceb17..975e6ef 100644 --- a/install-kit-awindows-20260427-211240/ansible/tasks/provision_ct_and_deploy_aw.yml +++ b/install-kit-awindows-20260427-211240/ansible/tasks/provision_ct_and_deploy_aw.yml @@ -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 }}" diff --git a/install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh b/install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh index 1b7c240..37a898a 100755 --- a/install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh +++ b/install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh @@ -24,7 +24,7 @@ TRENDS_REPLACEMENT='this.activityStore.ensure_loaded(r)' TIMESPIRAL_NEEDLE='start:new Date("2022-08-08")' TIMESPIRAL_REPLACEMENT='start:new Date(Date.now()-12*36e5)' CATEGORY_HELPER_NEEDLE='hostname:t.hostnameChoices[0]' -CATEGORY_HELPER_REPLACEMENT='hostname:t.hostnameChoices.filter((function(t){return"unknown"!==t}))[0]||t.hostnameChoices[0]' +CATEGORY_HELPER_REPLACEMENT='hostname:t.hostnameChoices.filter((function(t){return"unknown"!==t&&"undefined"!==t}))[0]||t.hostnameChoices[0]' [[ -f "$PATCH_JS_SRC" ]] || { echo "missing $PATCH_JS_SRC" >&2; exit 1; } [[ -f "$SW_CLEANUP_SRC" ]] || { echo "missing $SW_CLEANUP_SRC" >&2; exit 1; } diff --git a/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-admin.deployment-config.json b/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-admin.deployment-config.json similarity index 64% rename from install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-admin.deployment-config.json rename to install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-admin.deployment-config.json index 2b6d20c..2899c58 100644 --- a/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-admin.deployment-config.json +++ b/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-admin.deployment-config.json @@ -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_РђРґРјРёРЅРёСЃС_СЂР_С_РѕСЂ]" + } + ] +} diff --git a/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-u2u5.deployment-config.json b/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-u2u5.deployment-config.json similarity index 69% rename from install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-u2u5.deployment-config.json rename to install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-u2u5.deployment-config.json index a0f344f..cdac8f6 100644 --- a/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-u2u5.deployment-config.json +++ b/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-u2u5.deployment-config.json @@ -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]" + } + ] +} diff --git a/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-user1.deployment-config.json b/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-user1.deployment-config.json similarity index 63% rename from install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-user1.deployment-config.json rename to install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-user1.deployment-config.json index 0eecca9..8a42851 100644 --- a/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-user1.deployment-config.json +++ b/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-user1.deployment-config.json @@ -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]" + } + ] +} diff --git a/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 b/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 index 4bca474..9c9002c 100755 --- a/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 +++ b/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 @@ -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" } } diff --git a/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 b/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 index 220961e..d0c809f 100755 --- a/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 +++ b/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 @@ -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 diff --git a/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 index 936ce3c..8518ba5 100755 --- a/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 +++ b/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 @@ -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)" diff --git a/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 index 129d226..24c873a 100644 --- a/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 +++ b/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 @@ -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" diff --git a/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 index d3eafef..9b035e4 100755 --- a/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 +++ b/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 @@ -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)" diff --git a/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 b/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 index c908b2f..64e8d73 100755 --- a/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 +++ b/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 @@ -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 ', ')" diff --git a/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 b/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 new file mode 100644 index 0000000..eb7cbe4 --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 @@ -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 + } +} diff --git a/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 b/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 index 71e93a0..52b35d2 100644 --- a/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 +++ b/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 @@ -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) + ) } } diff --git a/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 b/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 new file mode 100644 index 0000000..f1b04ef --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 @@ -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 +} diff --git a/patch.sh b/patch.sh index 40b88b7..e8f2c2d 100644 --- a/patch.sh +++ b/patch.sh @@ -17,13 +17,14 @@ cat > windows/installkit/innosetup/innosetup-rdp-package-filelist.md <<'EOF' - Он описывает, **что не класть** (генерируется уже на целевом хосте). - Он **не меняет** текущие deploy-скрипты и логику проекта. -## Важное уточнение по `phase2` +## Важное уточнение по единой Windows-директории Чтобы исключить путаницу: -1. В каталоге `windows/` нет файлов с именами `phase2-*`. -2. `phase2` в проекте — это обозначение этапа/набора телеметрии (DLP + endpoint signals). -3. Файлы вида `phase2-*.deployment-config.json` — это **примерные конфиги install-kit**, они лежат в `install-kit-*/server-configs-*`. +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 пакета @@ -37,6 +38,7 @@ cat > windows/installkit/innosetup/innosetup-rdp-package-filelist.md <<'EOF' - `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) @@ -58,11 +60,11 @@ cat > windows/installkit/innosetup/innosetup-rdp-package-filelist.md <<'EOF' Эти файлы/папки появляются на целевом Windows-хосте во время/после деплоя: -- `C:\ProgramData\ActivityWatch\deployment-config.json` -- `C:\ProgramData\ActivityWatch\web-category-rules.json` -- `C:\ProgramData\ActivityWatch\dlp-policy.json` -- `C:\ProgramData\ActivityWatch\logs\*` -- `%LOCALAPPDATA%\ActivityWatch-Phase2\incident-artifacts\*` +- `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 @@ -80,6 +82,7 @@ cat > windows/installkit/innosetup/innosetup-rdp-package-filelist.md <<'EOF' - `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` @@ -152,4 +155,3 @@ 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" - diff --git a/scripts/aggregate_dlp_events.py b/scripts/aggregate_dlp_events.py new file mode 100755 index 0000000..7be7760 --- /dev/null +++ b/scripts/aggregate_dlp_events.py @@ -0,0 +1,517 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import sqlite3 +import sys +import urllib.error +import urllib.parse +import urllib.request +from collections.abc import Iterable +from dataclasses import dataclass +from datetime import UTC, datetime, timedelta +from pathlib import Path +from typing import Protocol, TypeAlias + + +JsonScalar: TypeAlias = str | int | float | bool | None +JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"] + + +DEFAULT_BUCKET_PREFIXES = ("aw-file-operations_", "aw-dlp-incidents_") +DEFAULT_SQLITE_PATH = "data/dlp-events.sqlite3" +EVENT_COLUMNS = ( + "bucket_id", + "event_id", + "stream_type", + "hostname", + "username", + "event_ts", + "duration", + "operation", + "file_path", + "old_file_path", + "extension", + "archive_hint", + "rule_id", + "action", + "severity", + "signal_type", + "message", + "source", + "screenshot_path", + "raw_json", + "ingested_at", +) + + +@dataclass(frozen=True) +class Bucket: + id: str + type: str + client: str + hostname: str + + +@dataclass(frozen=True) +class AwEvent: + bucket_id: str + hostname: str + stream_type: str + event_id: str + timestamp: str + duration: float + data: dict[str, JsonValue] + + +class PsycopgConnection(Protocol): + def cursor(self): + ... + + def commit(self) -> None: + ... + + +def utc_now() -> datetime: + return datetime.now(tz=UTC) + + +def parse_timestamp(value: str) -> datetime: + normalized = value.replace("Z", "+00:00") + parsed = datetime.fromisoformat(normalized) + if parsed.tzinfo is None: + return parsed.replace(tzinfo=UTC) + return parsed.astimezone(UTC) + + +def format_aw_timestamp(value: datetime) -> str: + return value.astimezone(UTC).isoformat().replace("+00:00", "Z") + + +def load_state(path: Path) -> dict[str, str]: + if not path.exists(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + +def save_state(path: Path, state: dict[str, str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(state, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def normalize_base_url(base_url: str) -> str: + return base_url.rstrip("/") + + +def aw_get_json(base_url: str, path: str, timeout: int) -> JsonValue: + url = normalize_base_url(base_url) + path + request = urllib.request.Request(url, headers={"Accept": "application/json"}) + with urllib.request.urlopen(request, timeout=timeout) as response: + return json.loads(response.read().decode("utf-8")) + + +def list_buckets(base_url: str, timeout: int) -> list[Bucket]: + payload = aw_get_json(base_url, "/buckets", timeout) + if not isinstance(payload, dict): + raise ValueError("ActivityWatch /buckets response must be a JSON object") + buckets: list[Bucket] = [] + for bucket_id, bucket_data in payload.items(): + if not isinstance(bucket_data, dict): + continue + buckets.append( + Bucket( + id=str(bucket_id), + type=str(bucket_data.get("type", "")), + client=str(bucket_data.get("client", "")), + hostname=str(bucket_data.get("hostname", "")), + ) + ) + return buckets + + +def bucket_stream_type(bucket: Bucket) -> str | None: + if bucket.id.startswith("aw-file-operations_") or bucket.type == "aw.file.operation": + return "file_operation" + if bucket.id.startswith("aw-dlp-incidents_") or bucket.type == "aw.dlp.incident": + return "dlp_incident" + return None + + +def select_buckets(buckets: Iterable[Bucket], prefixes: tuple[str, ...]) -> list[tuple[Bucket, str]]: + selected: list[tuple[Bucket, str]] = [] + for bucket in buckets: + stream_type = bucket_stream_type(bucket) + if stream_type and any(bucket.id.startswith(prefix) for prefix in prefixes): + selected.append((bucket, stream_type)) + return selected + + +def build_events_path(bucket_id: str, start: datetime, end: datetime, limit: int) -> str: + query = urllib.parse.urlencode( + { + "start": format_aw_timestamp(start), + "end": format_aw_timestamp(end), + "limit": str(limit), + } + ) + return f"/buckets/{urllib.parse.quote(bucket_id, safe='')}/events?{query}" + + +def event_key(bucket_id: str, timestamp: str, duration: float, data: dict[str, JsonValue]) -> str: + payload = json.dumps(data, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return f"{bucket_id}|{timestamp}|{duration}|{payload}" + + +def fetch_bucket_events( + base_url: str, + bucket: Bucket, + stream_type: str, + start: datetime, + end: datetime, + limit: int, + timeout: int, +) -> list[AwEvent]: + payload = aw_get_json(base_url, build_events_path(bucket.id, start, end, limit), timeout) + if not isinstance(payload, list): + raise ValueError(f"ActivityWatch events response for {bucket.id} must be a JSON array") + events: list[AwEvent] = [] + for item in payload: + if not isinstance(item, dict): + continue + timestamp = str(item["timestamp"]) + duration = float(item.get("duration", 0) or 0) + data = item.get("data") or {} + if not isinstance(data, dict): + data = {"raw": data} + item_id = str(item.get("id") or event_key(bucket.id, timestamp, duration, data)) + events.append( + AwEvent( + bucket_id=bucket.id, + hostname=bucket.hostname or str(data.get("hostname") or ""), + stream_type=stream_type, + event_id=item_id, + timestamp=timestamp, + duration=duration, + data=data, + ) + ) + return events + + +def connect_sqlite(path: Path) -> sqlite3.Connection: + path.parent.mkdir(parents=True, exist_ok=True) + connection = sqlite3.connect(str(path)) + connection.execute("PRAGMA journal_mode=WAL") + connection.execute("PRAGMA synchronous=NORMAL") + connection.execute("PRAGMA foreign_keys=ON") + return connection + + +def ensure_schema(connection: sqlite3.Connection) -> None: + connection.executescript( + """ + create table if not exists dlp_events ( + id integer primary key autoincrement, + bucket_id text not null, + event_id text not null, + stream_type text not null, + hostname text not null, + username text, + event_ts text not null, + duration real not null default 0, + operation text, + file_path text, + old_file_path text, + extension text, + archive_hint integer not null default 0, + rule_id text, + action text, + severity text, + signal_type text, + message text, + source text, + screenshot_path text, + raw_json text not null, + ingested_at text not null, + unique (bucket_id, event_id) + ); + create index if not exists idx_dlp_events_event_ts on dlp_events(event_ts); + create index if not exists idx_dlp_events_host_ts on dlp_events(hostname, event_ts); + create index if not exists idx_dlp_events_stream_ts on dlp_events(stream_type, event_ts); + create index if not exists idx_dlp_events_archive on dlp_events(archive_hint, event_ts); + create index if not exists idx_dlp_events_rule on dlp_events(rule_id, event_ts); + + create view if not exists dlp_file_operations as + select * + from dlp_events + where stream_type = 'file_operation'; + + create view if not exists dlp_incidents as + select * + from dlp_events + where stream_type = 'dlp_incident'; + """ + ) + connection.commit() + + +def ensure_postgres_schema(connection: PsycopgConnection) -> None: + with connection.cursor() as cursor: + cursor.execute( + """ + create table if not exists dlp_events ( + id bigserial primary key, + bucket_id text not null, + event_id text not null, + stream_type text not null, + hostname text not null, + username text, + event_ts timestamptz not null, + duration double precision not null default 0, + operation text, + file_path text, + old_file_path text, + extension text, + archive_hint boolean not null default false, + rule_id text, + action text, + severity text, + signal_type text, + message text, + source text, + screenshot_path text, + raw_json jsonb not null, + ingested_at timestamptz not null, + unique (bucket_id, event_id) + ); + create index if not exists idx_dlp_events_event_ts on dlp_events(event_ts); + create index if not exists idx_dlp_events_host_ts on dlp_events(hostname, event_ts); + create index if not exists idx_dlp_events_stream_ts on dlp_events(stream_type, event_ts); + create index if not exists idx_dlp_events_archive on dlp_events(archive_hint, event_ts); + create index if not exists idx_dlp_events_rule on dlp_events(rule_id, event_ts); + + create or replace view dlp_file_operations as + select * + from dlp_events + where stream_type = 'file_operation'; + + create or replace view dlp_incidents as + select * + from dlp_events + where stream_type = 'dlp_incident'; + """ + ) + connection.commit() + + +def first_string(data: dict[str, JsonValue], keys: tuple[str, ...]) -> str | None: + for key in keys: + value = data.get(key) + if value is not None and str(value) != "": + return str(value) + return None + + +def bool_as_int(value: JsonValue) -> int: + if isinstance(value, bool): + return int(value) + if isinstance(value, str): + return int(value.lower() in {"1", "true", "yes", "y"}) + return int(bool(value)) + + +def event_row(event: AwEvent, ingested_at: str) -> tuple[JsonValue, ...]: + data = event.data + event_id = event.event_id or event_key(event.bucket_id, event.timestamp, event.duration, data) + return ( + event.bucket_id, + event_id, + event.stream_type, + event.hostname, + first_string(data, ("username", "user")), + event.timestamp, + event.duration, + first_string(data, ("operation",)), + first_string(data, ("path", "filePath")), + first_string(data, ("oldPath", "oldFilePath")), + first_string(data, ("extension",)), + bool_as_int(data.get("archiveHint")), + first_string(data, ("ruleId", "rule")), + first_string(data, ("action",)), + first_string(data, ("severity",)), + first_string(data, ("signalType",)), + first_string(data, ("message",)), + first_string(data, ("source",)), + first_string(data, ("screenshotPath", "capturePath", "artifactPath")), + json.dumps(data, ensure_ascii=False, sort_keys=True), + ingested_at, + ) + + +def insert_events(connection: sqlite3.Connection, events: Iterable[AwEvent]) -> int: + inserted = 0 + now = format_aw_timestamp(utc_now()) + for event in events: + cursor = connection.execute( + """ + insert or ignore into dlp_events ( + bucket_id, + event_id, + stream_type, + hostname, + username, + event_ts, + duration, + operation, + file_path, + old_file_path, + extension, + archive_hint, + rule_id, + action, + severity, + signal_type, + message, + source, + screenshot_path, + raw_json, + ingested_at + ) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + event_row(event, now), + ) + inserted += int(cursor.rowcount > 0) + connection.commit() + return inserted + + +def insert_postgres_events(dsn: str, events: Iterable[AwEvent]) -> int: + try: + import psycopg + except ImportError as exc: + raise SystemExit("PostgreSQL mode requires psycopg: python3 -m pip install 'psycopg[binary]'") from exc + + inserted = 0 + now = format_aw_timestamp(utc_now()) + columns = ", ".join(EVENT_COLUMNS) + placeholders = ", ".join(["%s"] * len(EVENT_COLUMNS)) + sql = f""" + insert into dlp_events ({columns}) + values ({placeholders}) + on conflict (bucket_id, event_id) do nothing + """ + with psycopg.connect(dsn) as connection: + ensure_postgres_schema(connection) + with connection.cursor() as cursor: + for event in events: + row = list(event_row(event, now)) + row[EVENT_COLUMNS.index("archive_hint")] = bool(row[EVENT_COLUMNS.index("archive_hint")]) + cursor.execute(sql, row) + inserted += int(cursor.rowcount > 0) + connection.commit() + return inserted + + +def get_start_time(args: argparse.Namespace, state: dict[str, str]) -> datetime: + if args.since: + return parse_timestamp(args.since) + if state.get("last_end"): + return parse_timestamp(state["last_end"]) - timedelta(seconds=args.overlap_seconds) + return utc_now() - timedelta(hours=args.lookback_hours) + + +def parse_prefixes(value: str) -> tuple[str, ...]: + prefixes = tuple(item.strip() for item in value.split(",") if item.strip()) + if not prefixes: + raise argparse.ArgumentTypeError("at least one bucket prefix is required") + return prefixes + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Aggregate AWatch-rus DLP buckets into a local warehouse database.") + parser.add_argument("--aw-url", default=os.environ.get("AW_URL", "http://127.0.0.1:5600/api/0")) + parser.add_argument("--postgres-dsn", default=os.environ.get("DLP_AGGREGATOR_POSTGRES_DSN")) + parser.add_argument("--sqlite-path", default=os.environ.get("DLP_AGGREGATOR_SQLITE_PATH", DEFAULT_SQLITE_PATH)) + parser.add_argument("--state-path", default=os.environ.get("DLP_AGGREGATOR_STATE_PATH", "data/dlp-aggregator-state.json")) + parser.add_argument("--bucket-prefixes", type=parse_prefixes, default=DEFAULT_BUCKET_PREFIXES) + parser.add_argument("--since", help="UTC ISO timestamp. Overrides saved state, for example 2026-05-02T00:00:00Z.") + parser.add_argument("--lookback-hours", type=int, default=24) + parser.add_argument("--overlap-seconds", type=int, default=60) + parser.add_argument("--limit", type=int, default=10000) + parser.add_argument("--timeout", type=int, default=15) + parser.add_argument("--dry-run", action="store_true") + return parser + + +def main() -> int: + args = build_parser().parse_args() + state_path = Path(args.state_path) + state = load_state(state_path) + start = get_start_time(args, state) + end = utc_now() + + buckets = select_buckets(list_buckets(args.aw_url, args.timeout), args.bucket_prefixes) + all_events: list[AwEvent] = [] + for bucket, stream_type in buckets: + all_events.extend(fetch_bucket_events(args.aw_url, bucket, stream_type, start, end, args.limit, args.timeout)) + + if args.dry_run: + print( + json.dumps( + { + "aw_url": args.aw_url, + "start": format_aw_timestamp(start), + "end": format_aw_timestamp(end), + "selected_buckets": [bucket.id for bucket, _stream_type in buckets], + "fetched_events": len(all_events), + }, + ensure_ascii=False, + indent=2, + ) + ) + return 0 + + if args.postgres_dsn: + target = "postgres" + target_path = args.postgres_dsn.split("@")[-1] + inserted = insert_postgres_events(args.postgres_dsn, all_events) + else: + target = "sqlite" + sqlite_path = Path(args.sqlite_path) + target_path = str(sqlite_path) + connection = connect_sqlite(sqlite_path) + try: + ensure_schema(connection) + inserted = insert_events(connection, all_events) + finally: + connection.close() + + state["last_end"] = format_aw_timestamp(end) + save_state(state_path, state) + print( + json.dumps( + { + "aw_url": args.aw_url, + "target": target, + "target_path": target_path, + "state_path": str(state_path), + "start": format_aw_timestamp(start), + "end": format_aw_timestamp(end), + "selected_buckets": len(buckets), + "fetched_events": len(all_events), + "inserted_events": inserted, + }, + ensure_ascii=False, + indent=2, + ) + ) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except urllib.error.URLError as exc: + print(f"ActivityWatch API request failed: {exc}", file=sys.stderr) + raise SystemExit(2) diff --git a/scripts/merge_aw_server_dbs.py b/scripts/merge_aw_server_dbs.py new file mode 100644 index 0000000..6b39944 --- /dev/null +++ b/scripts/merge_aw_server_dbs.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import shutil +import sqlite3 +from pathlib import Path + + +def connect(path: Path) -> sqlite3.Connection: + connection = sqlite3.connect(str(path)) + connection.execute("PRAGMA journal_mode=WAL") + connection.execute("PRAGMA synchronous=NORMAL") + return connection + + +def bucket_key(row: sqlite3.Row) -> tuple[str, str, str, str]: + return ( + str(row["name"]), + str(row["type"]), + str(row["client"]), + str(row["hostname"]), + ) + + +def ensure_parent(path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + + +def load_existing_events(connection: sqlite3.Connection, bucketrow: int) -> set[tuple[int, int, str]]: + cursor = connection.execute( + "select starttime, endtime, data from events where bucketrow = ?", + (bucketrow,), + ) + return {(int(start), int(end), str(data)) for start, end, data in cursor.fetchall()} + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--base", required=True) + parser.add_argument("--output", required=True) + parser.add_argument("--overlay") + args = parser.parse_args() + + base = Path(args.base) + output = Path(args.output) + overlay = Path(args.overlay) if args.overlay else None + + if not base.exists(): + raise SystemExit(f"Base DB not found: {base}") + + ensure_parent(output) + tmp_output = output.with_suffix(output.suffix + ".tmp") + if tmp_output.exists(): + tmp_output.unlink() + shutil.copy2(base, tmp_output) + + dest = connect(tmp_output) + dest.row_factory = sqlite3.Row + + inserted_buckets = 0 + inserted_events = 0 + + if overlay and overlay.exists(): + source = connect(overlay) + source.row_factory = sqlite3.Row + try: + source_buckets = source.execute( + "select rowid as bucketrow, id, name, type, client, hostname, created, data_deprecated, data from buckets order by rowid" + ).fetchall() + + dest_bucket_map = { + bucket_key(row): row["bucketrow"] + for row in dest.execute( + "select rowid as bucketrow, id, name, type, client, hostname, created, data_deprecated, data from buckets order by rowid" + ).fetchall() + } + + for src_bucket in source_buckets: + key = bucket_key(src_bucket) + dest_rowid = dest_bucket_map.get(key) + if dest_rowid is None: + cursor = dest.execute( + """ + insert into buckets (name, type, client, hostname, created, data_deprecated, data) + values (?, ?, ?, ?, ?, ?, ?) + """, + ( + src_bucket["name"], + src_bucket["type"], + src_bucket["client"], + src_bucket["hostname"], + src_bucket["created"], + src_bucket["data_deprecated"], + src_bucket["data"], + ), + ) + dest_rowid = int(cursor.lastrowid) + dest_bucket_map[key] = dest_rowid + inserted_buckets += 1 + + existing_events = load_existing_events(dest, dest_rowid) + for starttime, endtime, data in source.execute( + "select starttime, endtime, data from events where bucketrow = ? order by id", + (src_bucket["bucketrow"],), + ).fetchall(): + event_key = (int(starttime), int(endtime), str(data)) + if event_key in existing_events: + continue + dest.execute( + "insert into events (bucketrow, starttime, endtime, data) values (?, ?, ?, ?)", + (dest_rowid, int(starttime), int(endtime), str(data)), + ) + existing_events.add(event_key) + inserted_events += 1 + + dest.commit() + finally: + source.close() + + dest.close() + os.replace(tmp_output, output) + print( + json.dumps( + { + "base": str(base), + "overlay": str(overlay) if overlay else None, + "output": str(output), + "inserted_buckets": inserted_buckets, + "inserted_events": inserted_events, + }, + ensure_ascii=False, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/prod_backup_restore.sh b/scripts/prod_backup_restore.sh new file mode 100644 index 0000000..a731ae6 --- /dev/null +++ b/scripts/prod_backup_restore.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +if [[ -f "${ROOT_DIR}/secrets/runtime.env" ]]; then + set -a + # shellcheck disable=SC1091 + source "${ROOT_DIR}/secrets/runtime.env" + set +a +fi + +: "${AW_SSH_PASSWORD:?AW_SSH_PASSWORD is required}" +: "${AW_WINRM_PASSWORD:?AW_WINRM_PASSWORD is required}" + +command -v sshpass >/dev/null 2>&1 || { echo "missing sshpass" >&2; exit 127; } +command -v ansible-playbook >/dev/null 2>&1 || { echo "missing ansible-playbook" >&2; exit 127; } + +SERVER_HOST="${AW_SERVER_HOST:-10.10.10.13}" +SERVER_USER="${AW_SERVER_USER:-igor}" +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +REMOTE_BACKUP_DIR="/var/lib/activitywatch/backups/prod-restore-${TIMESTAMP}" +LEGACY_DB="/root/.local/share/activitywatch/aw-server-rust/sqlite.db" +TARGET_DB="/var/lib/activitywatch/.local/share/activitywatch/aw-server-rust/sqlite.db" +REMOTE_MERGE_SCRIPT="/tmp/merge_aw_server_dbs.py" + +ssh_remote() { + sshpass -p "$AW_SSH_PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "${SERVER_USER}@${SERVER_HOST}" "$@" +} + +scp_remote() { + sshpass -p "$AW_SSH_PASSWORD" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$@" +} + +scp_remote "${ROOT_DIR}/scripts/merge_aw_server_dbs.py" "${SERVER_USER}@${SERVER_HOST}:${REMOTE_MERGE_SCRIPT}" + +ssh_remote "sudo mkdir -p '${REMOTE_BACKUP_DIR}' && sudo chown root:root '${REMOTE_BACKUP_DIR}'" +ssh_remote "sudo test -f '${LEGACY_DB}'" +ssh_remote "sudo test -f '${TARGET_DB}'" +ssh_remote "sudo cp -a '${LEGACY_DB}' '${REMOTE_BACKUP_DIR}/legacy-root-sqlite.db' && sudo cp -a '${TARGET_DB}' '${REMOTE_BACKUP_DIR}/target-before-merge-sqlite.db'" +ssh_remote "sudo systemctl stop activitywatch-server.service || true" +ssh_remote "sudo python3 '${REMOTE_MERGE_SCRIPT}' --base '${LEGACY_DB}' --overlay '${TARGET_DB}' --output '${REMOTE_BACKUP_DIR}/sqlite.merged.db'" +ssh_remote "sudo install -o activitywatch -g activitywatch -m 0644 '${REMOTE_BACKUP_DIR}/sqlite.merged.db' '${TARGET_DB}'" + +ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_server.yml +ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_windows.yml +ansible-playbook -i ansible/inventory.ini ansible/post_validate_aw_windows.yml + +python3 - <<'PY' +import json, urllib.request +base = 'http://10.10.10.13:5600' +window_payload = { + 'timeperiods': ['2026-04-29T00:00:00+03:00/2026-04-29T23:59:59+03:00'], + 'query': [ + 'window_events = query_bucket(find_bucket("aw-watcher-window_SHARKON2025"));', + 'RETURN = window_events;' + ] +} +req = urllib.request.Request(base + '/api/0/query/', data=json.dumps(window_payload).encode(), method='POST', headers={'Content-Type': 'application/json', 'Origin': 'http://10.10.10.13:5600'}) +with urllib.request.urlopen(req) as response: + data = json.loads(response.read().decode()) +window_count = len(data[0]) if isinstance(data, list) and data else 0 +if window_count <= 0: + raise SystemExit('no historical window data restored for 2026-04-29') +with urllib.request.urlopen(base + '/api/0/settings/') as response: + settings = json.loads(response.read().decode()) +if settings.get('always_active_pattern') != 'aw-watcher-window': + raise SystemExit('always_active_pattern is not configured') +print(json.dumps({'restored_window_events_2026_04_29': window_count, 'always_active_pattern': settings.get('always_active_pattern')}, ensure_ascii=False)) +PY diff --git a/scripts/prod_rollout.sh b/scripts/prod_rollout.sh new file mode 100644 index 0000000..d7f080c --- /dev/null +++ b/scripts/prod_rollout.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +timestamp() { date +"%Y%m%d-%H%M%S"; } + +LOG_DIR="${ROOT_DIR}/.rollout-logs/$(timestamp)" +mkdir -p "$LOG_DIR" + +log() { printf "%s %s\n" "$(date +"%F %T")" "$*" | tee -a "${LOG_DIR}/rollout.log" >&2; } + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { log "ERROR: missing command: $1"; exit 127; } +} + +prompt_secret() { + local var_name="$1" + local prompt="$2" + if [[ -n "${!var_name:-}" ]]; then + return 0 + fi + read -r -s -p "${prompt}: " "$var_name" + echo + export "$var_name" +} + +require_cmd git +require_cmd ansible-playbook +require_cmd ansible + +log "Repo: ${ROOT_DIR}" +log "Branch: $(git branch --show-current)" + +log "Running local quality gate..." +./scripts/quality-gate.sh | tee -a "${LOG_DIR}/quality-gate.log" + +if [[ -f "${ROOT_DIR}/secrets/runtime.env" ]]; then + log "Loading secrets/runtime.env" + set -a + # shellcheck disable=SC1091 + source "${ROOT_DIR}/secrets/runtime.env" + set +a +fi + +if [[ ! -f ansible/inventory.ini ]]; then + log "ERROR: missing ansible/inventory.ini" + log "Hint: copy ansible/inventory.example.ini -> ansible/inventory.ini and adjust hosts." + exit 2 +fi + +if [[ -t 0 ]]; then + prompt_secret AW_SSH_PASSWORD "Enter SSH password for aw_server (root@10.10.10.13)" + prompt_secret AW_WINRM_PASSWORD "Enter WinRM password for aw_windows (192.168.100.21)" +fi + +if [[ -z "${AW_SSH_PASSWORD:-}" || -z "${AW_WINRM_PASSWORD:-}" ]]; then + log "ERROR: missing AW_SSH_PASSWORD or AW_WINRM_PASSWORD." + log "Provide them via interactive prompt (TTY) or create secrets/runtime.env." + exit 3 +fi + +log "Preflight connectivity..." +ansible -i ansible/inventory.ini aw_server -m ping | tee -a "${LOG_DIR}/ping_aw_server.log" +ansible -i ansible/inventory.ini aw_windows -m win_ping | tee -a "${LOG_DIR}/ping_aw_windows.log" + +log "Dry-run aw_server..." +ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_server.yml --check --diff | tee -a "${LOG_DIR}/check_aw_server.log" + +log "Deploy aw_server..." +ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_server.yml | tee -a "${LOG_DIR}/deploy_aw_server.log" + +log "Dry-run aw_windows..." +ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_windows.yml --check --diff | tee -a "${LOG_DIR}/check_aw_windows.log" + +log "Deploy aw_windows..." +ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_windows.yml | tee -a "${LOG_DIR}/deploy_aw_windows.log" + +log "DONE. Logs: ${LOG_DIR}" diff --git a/scripts/quality-gate.sh b/scripts/quality-gate.sh index 84b68eb..2433bc6 100755 --- a/scripts/quality-gate.sh +++ b/scripts/quality-gate.sh @@ -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 diff --git a/windows/ActivityWatch.Windows.Common.psm1 b/windows/ActivityWatch.Windows.Common.psm1 index fb36dca..b42a252 100755 --- a/windows/ActivityWatch.Windows.Common.psm1 +++ b/windows/ActivityWatch.Windows.Common.psm1 @@ -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 с правами администратора.' } } @@ -20,6 +20,17 @@ function New-ActivityWatchDirectory { } } +function Enable-ActivityWatchPrintTelemetry { + $policyPath = 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' + if (-not (Test-Path -LiteralPath $policyPath)) { + New-Item -Path $policyPath -Force | Out-Null + } + New-ItemProperty -Path $policyPath -Name 'ShowJobTitleInEventLogs' -Value 1 -PropertyType DWord -Force | Out-Null + + & wevtutil.exe sl 'Microsoft-Windows-PrintService/Operational' /e:true | Out-Null +} + + function Get-ActivityWatchPackageUrl { param( [string]$Version = 'v0.13.2' @@ -49,7 +60,9 @@ function Get-ActivityWatchArchive { } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}.zip" -f $Version.TrimStart('v')) + $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' + $suffix = ([guid]::NewGuid().Guid.Substring(0, 8)) + $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}-{1}-{2}.zip" -f $Version.TrimStart('v'), $stamp, $suffix) Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath return $archivePath } @@ -64,7 +77,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) @@ -85,6 +98,16 @@ function Install-ActivityWatchPackage { New-ActivityWatchDirectory -Path $WorkingRoot New-ActivityWatchDirectory -Path $BackupRoot + # Ensure nothing is holding locks inside InstallRoot during upgrade. + foreach ($procName in @('aw-watcher-afk', 'aw-watcher-window', 'aw-server', 'aw-qt')) { + try { + Get-Process -Name $procName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + } + catch { + } + } + Start-Sleep -Seconds 2 + $extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid) if (Test-Path -LiteralPath $extractRoot) { Remove-Item -LiteralPath $extractRoot -Recurse -Force @@ -130,7 +153,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 +217,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 +266,8 @@ function Copy-ActivityWatchCollectorAssets { [Parameter(Mandatory = $true)] [string]$EndpointCollectorScriptSource, [Parameter(Mandatory = $true)] + [string]$FileCollectorScriptSource, + [Parameter(Mandatory = $true)] [string]$SessionCollectorScriptSource, [Parameter(Mandatory = $true)] [string]$ExampleRulesSource, @@ -258,6 +283,7 @@ function Copy-ActivityWatchCollectorAssets { $collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1' $endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' + $fileCollectorTarget = Join-Path $StateRoot 'file-operations-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' @@ -266,6 +292,7 @@ function Copy-ActivityWatchCollectorAssets { Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force + Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force @@ -286,6 +313,7 @@ function Copy-ActivityWatchCollectorAssets { return [pscustomobject]@{ CollectorScript = $collectorTarget EndpointCollectorScript = $endpointCollectorTarget + FileCollectorScript = $fileCollectorTarget SessionCollectorScript = $sessionCollectorTarget ExampleRules = $exampleRulesTarget ActiveRules = $rulesTarget @@ -313,6 +341,8 @@ function New-ActivityWatchDeploymentConfig { [Parameter(Mandatory = $true)] [string]$EndpointCollectorScript, [Parameter(Mandatory = $true)] + [string]$FileCollectorScript, + [Parameter(Mandatory = $true)] [string]$SessionCollectorScript, [Parameter(Mandatory = $true)] [string]$RulesPath, @@ -326,6 +356,7 @@ function New-ActivityWatchDeploymentConfig { [int]$RecoveryIntervalSeconds, [bool]$AfkEnabled = $true, [bool]$WindowEnabled = $true, + [bool]$FileOpsEnabled = $true, [bool]$LocalAgentLogsEnabled = $true, [bool]$IncidentCaptureEnabled = $true, [bool]$IncidentScreenshotEnabled = $true, @@ -356,6 +387,7 @@ function New-ActivityWatchDeploymentConfig { logsRoot = $LogsRoot collectorScript = $CollectorScript endpointCollectorScript = $EndpointCollectorScript + fileCollectorScript = $FileCollectorScript sessionCollectorScript = $SessionCollectorScript rulesPath = $RulesPath policyPath = $PolicyPath @@ -369,6 +401,7 @@ function New-ActivityWatchDeploymentConfig { collectors = [pscustomobject]@{ afkEnabled = $AfkEnabled windowEnabled = $WindowEnabled + fileOpsEnabled = $FileOpsEnabled } logging = [pscustomobject]@{ localAgentLogsEnabled = $LocalAgentLogsEnabled @@ -421,7 +454,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 @@ -443,6 +476,9 @@ param( Set-StrictMode -Version Latest `$ErrorActionPreference = 'Stop' +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 +Add-Type -AssemblyName System.Net.Http + function Get-DeploymentConfig { param([string]`$Path) return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json @@ -480,8 +516,21 @@ function Invoke-AwJsonPost { [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 + `$httpClient = New-Object System.Net.Http.HttpClient + try { + `$content = New-Object System.Net.Http.StringContent(`$Json, [System.Text.Encoding]::UTF8, 'application/json') + `$response = `$httpClient.PostAsync(`$Uri, `$content).Result + if (-not `$response.IsSuccessStatusCode) { + return `$false + } + return `$true + } + catch { + return `$false + } + finally { + `$httpClient.Dispose() + } } function Ensure-Bucket { @@ -510,7 +559,9 @@ function Ensure-Bucket { } | ConvertTo-Json -Compress try { - Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body + if (-not (Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body)) { + return + } } catch { try { @@ -547,7 +598,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')) @@ -593,7 +644,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 @@ -614,6 +665,10 @@ function Start-CollectorScriptIfNeeded { [int]`$SessionId ) + if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) { + return + } + if (-not (Test-Path -LiteralPath `$ScriptPath)) { return } @@ -634,25 +689,28 @@ function Start-CollectorScriptIfNeeded { `$config = Get-DeploymentConfig -Path `$ConfigPath `$sessionId = (Get-Process -Id `$PID).SessionId `$installRoot = [string]`$config.paths.installRoot +`$stateRoot = [string]`$config.paths.stateRoot `$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port `$script:Hostname = `$env:COMPUTERNAME `$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 { '' } +`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { Join-Path `$stateRoot 'dlp-endpoint-signals-collector.ps1' } +`$fileCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]`$config.paths.fileCollectorScript } else { Join-Path `$stateRoot 'file-operations-collector.ps1' } +`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { Join-Path `$stateRoot 'worktime-session-collector.ps1' } `$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) `$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' `$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true } `$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } +`$fileOpsEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]`$config.collectors.fileOpsEnabled } 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)) { @@ -670,6 +728,9 @@ catch { } Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId +if (`$fileOpsEnabled) { + Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId +} Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId "@ @@ -860,7 +921,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" } } @@ -891,6 +952,37 @@ function Get-ActivityWatchScheduledTaskByCommand { return $null } +function Remove-StaleActivityWatchUserTasks { + param( + [Parameter(Mandatory = $true)] + [pscustomobject[]]$TaskDefinitions, + [Parameter(Mandatory = $true)] + [string]$LaunchScriptPath + ) + + $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath + $desiredTaskNames = @($TaskDefinitions | ForEach-Object { [string]$_.LaunchTaskName }) + + foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { + $taskName = [string]$candidate.TaskName + if ($desiredTaskNames -contains $taskName) { + continue + } + + $usesCurrentLauncher = $false + foreach ($action in @($candidate.Actions)) { + if ([string]$action.Arguments -like "*$launcherPath*") { + $usesCurrentLauncher = $true + break + } + } + + if ($usesCurrentLauncher) { + Remove-ActivityWatchScheduledTask -TaskName $taskName + } + } +} + function Register-ActivityWatchUserTasks { param( [Parameter(Mandatory = $true)] @@ -904,6 +996,7 @@ function Register-ActivityWatchUserTasks { $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath + Remove-StaleActivityWatchUserTasks -TaskDefinitions $TaskDefinitions -LaunchScriptPath $LaunchScriptPath foreach ($definition in $TaskDefinitions) { $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" @@ -961,17 +1054,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" } } diff --git a/windows/browser-domains-native-collector.ps1 b/windows/browser-domains-native-collector.ps1 index 220961e..d0c809f 100755 --- a/windows/browser-domains-native-collector.ps1 +++ b/windows/browser-domains-native-collector.ps1 @@ -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 diff --git a/windows/deploy-domain-users.ps1 b/windows/deploy-domain-users.ps1 index c6ca412..5a6ee7a 100755 --- a/windows/deploy-domain-users.ps1 +++ b/windows/deploy-domain-users.ps1 @@ -11,13 +11,14 @@ 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, [bool]$AfkEnabled = $true, [bool]$WindowEnabled = $true, + [bool]$FileOpsEnabled = $true, [bool]$LocalAgentLogsEnabled = $false, [bool]$IncidentCaptureEnabled = $true, [bool]$IncidentScreenshotEnabled = $true, @@ -44,12 +45,14 @@ $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' +$fileCollectorSource = Join-Path $PSScriptRoot 'file-operations-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' New-ActivityWatchDirectory -Path $StateRoot New-ActivityWatchDirectory -Path $logsRoot +Enable-ActivityWatchPrintTelemetry $archivePath = Get-ActivityWatchArchive -PackageZipPath $PackageZipPath -PackageUrl $PackageUrl -Version $Version -WorkingRoot $workingRoot Install-ActivityWatchPackage -ArchivePath $archivePath -InstallRoot $InstallRoot -WorkingRoot $workingRoot -BackupRoot $backupRoot | Out-Null @@ -58,6 +61,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null $assetResult = Copy-ActivityWatchCollectorAssets ` -CollectorScriptSource $collectorSource ` -EndpointCollectorScriptSource $endpointCollectorSource ` + -FileCollectorScriptSource $fileCollectorSource ` -SessionCollectorScriptSource $sessionCollectorSource ` -ExampleRulesSource $exampleRulesSource ` -ExamplePolicySource $examplePolicySource ` @@ -78,6 +82,7 @@ $config = New-ActivityWatchDeploymentConfig ` -LogsRoot $logsRoot ` -CollectorScript $assetResult.CollectorScript ` -EndpointCollectorScript $assetResult.EndpointCollectorScript ` + -FileCollectorScript $assetResult.FileCollectorScript ` -SessionCollectorScript $assetResult.SessionCollectorScript ` -RulesPath $assetResult.ActiveRules ` -PolicyPath $assetResult.ActivePolicy ` @@ -86,6 +91,7 @@ $config = New-ActivityWatchDeploymentConfig ` -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` -AfkEnabled $AfkEnabled ` -WindowEnabled $WindowEnabled ` + -FileOpsEnabled $FileOpsEnabled ` -LocalAgentLogsEnabled $LocalAgentLogsEnabled ` -IncidentCaptureEnabled $IncidentCaptureEnabled ` -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` @@ -103,8 +109,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)" diff --git a/windows/deploy-ensemble.ps1 b/windows/deploy-ensemble.ps1 index 129d226..fa6fd75 100644 --- a/windows/deploy-ensemble.ps1 +++ b/windows/deploy-ensemble.ps1 @@ -11,13 +11,14 @@ 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, [bool]$AfkEnabled = $true, [bool]$WindowEnabled = $true, + [bool]$FileOpsEnabled = $true, [bool]$LocalAgentLogsEnabled = $false, [bool]$IncidentCaptureEnabled = $true, [bool]$IncidentScreenshotEnabled = $true, @@ -46,7 +47,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 ` @@ -64,6 +65,7 @@ if (-not (Test-Path -LiteralPath $deployScript)) { -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` -AfkEnabled $AfkEnabled ` -WindowEnabled $WindowEnabled ` + -FileOpsEnabled $FileOpsEnabled ` -LocalAgentLogsEnabled $LocalAgentLogsEnabled ` -IncidentCaptureEnabled $IncidentCaptureEnabled ` -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` @@ -86,6 +88,7 @@ if (-not $SkipHardening) { -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` -AfkEnabled $AfkEnabled ` -WindowEnabled $WindowEnabled ` + -FileOpsEnabled $FileOpsEnabled ` -LocalAgentLogsEnabled $LocalAgentLogsEnabled ` -IncidentCaptureEnabled $IncidentCaptureEnabled ` -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` @@ -112,13 +115,14 @@ $report = [ordered]@{ collectors = [ordered]@{ afkEnabled = $AfkEnabled windowEnabled = $WindowEnabled + fileOpsEnabled = $FileOpsEnabled } hardeningApplied = (-not $SkipHardening) } 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 +136,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" diff --git a/windows/deploy-single-user.ps1 b/windows/deploy-single-user.ps1 index dfd24b4..9b035e4 100755 --- a/windows/deploy-single-user.ps1 +++ b/windows/deploy-single-user.ps1 @@ -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, @@ -101,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)" diff --git a/windows/dlp-endpoint-signals-collector.ps1 b/windows/dlp-endpoint-signals-collector.ps1 index de84fdf..f983012 100644 --- a/windows/dlp-endpoint-signals-collector.ps1 +++ b/windows/dlp-endpoint-signals-collector.ps1 @@ -1,4 +1,5 @@ -[CmdletBinding()] +\xEF\xBB\xBF-ne \xEF\xBB\xBF +[CmdletBinding()] param( [string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json', [string]$ServerHost, diff --git a/windows/file-operations-collector.ps1 b/windows/file-operations-collector.ps1 new file mode 100644 index 0000000..3fb5246 --- /dev/null +++ b/windows/file-operations-collector.ps1 @@ -0,0 +1,182 @@ +[CmdletBinding()] +param( + [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', + [string]$ServerHost, + [int]$ServerPort, + [ValidateSet('http', 'https')] + [string]$ServerScheme, + [string]$PolicyPath, + [string]$LogPath, + [int]$PollSeconds = 10, + [string[]]$WatchPaths = @('Desktop', 'Documents', 'Downloads') +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Force TLS 1.2 and load networking types +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 +Add-Type -AssemblyName System.Net.Http + +# Bucket registry +$script:KnownBuckets = @{} +$script:Hostname = $env:COMPUTERNAME +$script:SessionId = [System.Diagnostics.Process]::GetCurrentProcess().SessionId + +# Настройка логирования +$script:LogPath = $LogPath +$script:LocalAgentLogsEnabled = [bool]$LogPath + +function Get-DeploymentConfig { + param([string]$Path) + if ($Path -and (Test-Path -LiteralPath $Path)) { + return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json + } + return $null +} + +function Write-FileCollectorLog { + param([string]$Message) + if (-not $script:LocalAgentLogsEnabled) { return } + try { + Add-Content -LiteralPath $script:LogPath -Value ('{0} [FileCollector] {1}' -f (Get-Date -Format s), $Message) + } catch {} +} + +function Invoke-AwJsonPost { + param( + [Parameter(Mandatory = $true)][string]$Uri, + [Parameter(Mandatory = $true)][string]$Json + ) + try { + $httpClient = New-Object System.Net.Http.HttpClient + $content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json") + $response = $httpClient.PostAsync($Uri, $content).Result + $httpClient.Dispose() + } catch { + Write-FileCollectorLog "POST Error: $($_.Exception.Message)" + } +} + +function Ensure-Bucket { + param( + [string]$BucketId, + [string]$ClientName, + [string]$BucketType + ) + if ($script:KnownBuckets.ContainsKey($BucketId)) { return } + $body = @{ + client = $ClientName + type = $BucketType + hostname = $script:Hostname + } | ConvertTo-Json -Compress + Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body + $script:KnownBuckets[$BucketId] = $true +} + +function Send-FileOperationEvent { + param( + [string]$Operation, + [string]$FilePath, + [string]$OldFilePath = $null, + [long]$Size = 0 + ) + + $bucketId = 'aw-file-operations_' + $script:Hostname + Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation' + + $data = @{ + operation = $Operation + path = $FilePath + extension = [System.IO.Path]::GetExtension($FilePath) + username = $env:USERNAME + hostname = $script:Hostname + } + if ($OldFilePath) { $data.oldPath = $OldFilePath } + if ($Size -gt 0) { $data.size = $Size } + + # Детекция архивации (упрощенная) + if ($Operation -eq 'Created' -and $data.extension -match '\.(zip|7z|rar|tar|gz)$') { + $data.archiveHint = $true + } + + $payload = @{ + timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + duration = 0 + data = $data + } | ConvertTo-Json -Depth 5 -Compress + + Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=15" -Json $payload +} + +$config = Get-DeploymentConfig -Path $ConfigPath +if (-not $config) { throw "Configuration file not found: $ConfigPath" } + +$scheme = if ($ServerScheme) { $ServerScheme } elseif ($config.server.scheme) { $config.server.scheme } else { 'http' } +$hostName = if ($ServerHost) { $ServerHost } elseif ($config.server.host) { $config.server.host } else { 'localhost' } +$port = if ($ServerPort) { $ServerPort } elseif ($config.server.port) { $config.server.port } else { 5600 } +$script:ApiBase = "{0}://{1}:{2}/api/0" -f $scheme, $hostName, $port + +$bucketId = 'aw-file-operations_' + $script:Hostname +Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation' + +# Resolve paths for monitoring +$resolvedPaths = @() +foreach ($p in $WatchPaths) { + $fullPath = $p + if (-not [System.IO.Path]::IsPathRooted($p)) { + try { + if ($p -eq 'Desktop') { $fullPath = [Environment]::GetFolderPath('Desktop') } + elseif ($p -eq 'Documents') { $fullPath = [Environment]::GetFolderPath('MyDocuments') } + elseif ($p -eq 'Downloads') { $fullPath = Join-Path $env:USERPROFILE 'Downloads' } + } catch {} + } + if ($fullPath -and (Test-Path -LiteralPath $fullPath)) { + $resolvedPaths += $fullPath + } +} + +if ($resolvedPaths.Count -eq 0) { + Write-FileCollectorLog "No valid watch paths found. Exiting." + exit 0 +} + +Write-FileCollectorLog "Starting watch on paths: $($resolvedPaths -join ', ')" + +$watchers = @() +foreach ($path in $resolvedPaths) { + $watcher = New-Object System.IO.FileSystemWatcher + $watcher.Path = $path + $watcher.IncludeSubdirectories = $true + $watcher.EnableRaisingEvents = $true + + $onChanged = Register-ObjectEvent $watcher "Created" -Action { + $path = $Event.SourceEventArgs.FullPath + $size = 0 + try { if (Test-Path -LiteralPath $path) { $size = (Get-Item -LiteralPath $path).Length } } catch {} + Send-FileOperationEvent -Operation 'Created' -FilePath $path -Size $size + } + $onDeleted = Register-ObjectEvent $watcher "Deleted" -Action { + Send-FileOperationEvent -Operation 'Deleted' -FilePath $Event.SourceEventArgs.FullPath + } + $onRenamed = Register-ObjectEvent $watcher "Renamed" -Action { + Send-FileOperationEvent -Operation 'Renamed' -FilePath $Event.SourceEventArgs.FullPath -OldFilePath $Event.SourceEventArgs.OldFullPath + } + + $watchers += $watcher +} + +Write-FileCollectorLog "Collector started. Waiting for events..." + +try { + while ($true) { + Start-Sleep -Seconds $PollSeconds + } +} +finally { + Write-FileCollectorLog "Stopping collector..." + foreach ($w in $watchers) { + $w.EnableRaisingEvents = $false + $w.Dispose() + } +} diff --git a/windows/hardening-recovery.ps1 b/windows/hardening-recovery.ps1 index c908b2f..0a7123f 100755 --- a/windows/hardening-recovery.ps1 +++ b/windows/hardening-recovery.ps1 @@ -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')] @@ -15,6 +15,7 @@ param( [int]$RecoveryIntervalSeconds, [bool]$AfkEnabled, [bool]$WindowEnabled, + [bool]$FileOpsEnabled, [bool]$LocalAgentLogsEnabled, [bool]$IncidentCaptureEnabled, [bool]$IncidentScreenshotEnabled, @@ -42,17 +43,19 @@ 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' $effectiveRecoveryScript = Join-Path $effectiveStateRoot 'recovery-loop.ps1' $effectiveCollector = Join-Path $effectiveStateRoot 'browser-domains-native-collector.ps1' $effectiveEndpointCollector = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$existingConfig.paths.endpointCollectorScript } else { Join-Path $effectiveStateRoot 'dlp-endpoint-signals-collector.ps1' } +$effectiveFileCollector = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]$existingConfig.paths.fileCollectorScript } else { Join-Path $effectiveStateRoot 'file-operations-collector.ps1' } +$effectiveSessionCollector = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$existingConfig.paths.sessionCollectorScript } else { Join-Path $effectiveStateRoot 'worktime-session-collector.ps1' } $effectiveRules = Join-Path $effectiveStateRoot 'web-category-rules.json' $effectivePolicy = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$existingConfig.paths.policyPath } else { Join-Path $effectiveStateRoot 'dlp-policy.json' } @@ -64,6 +67,7 @@ $effectivePulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $ $effectiveRecoveryInterval = if ($PSBoundParameters.ContainsKey('RecoveryIntervalSeconds')) { $RecoveryIntervalSeconds } elseif ($existingConfig) { [int]$existingConfig.recovery.intervalSeconds } else { 180 } $effectiveAfkEnabled = if ($PSBoundParameters.ContainsKey('AfkEnabled')) { [bool]$AfkEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'collectors' -and $existingConfig.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$existingConfig.collectors.afkEnabled } else { $true } $effectiveWindowEnabled = if ($PSBoundParameters.ContainsKey('WindowEnabled')) { [bool]$WindowEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'collectors' -and $existingConfig.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$existingConfig.collectors.windowEnabled } else { $true } +$effectiveFileOpsEnabled = if ($PSBoundParameters.ContainsKey('FileOpsEnabled')) { [bool]$FileOpsEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'collectors' -and $existingConfig.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]$existingConfig.collectors.fileOpsEnabled } else { $true } $effectiveLocalAgentLogsEnabled = if ($PSBoundParameters.ContainsKey('LocalAgentLogsEnabled')) { [bool]$LocalAgentLogsEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'logging' -and $existingConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$existingConfig.logging.localAgentLogsEnabled } else { $false } $effectiveIncidentCaptureEnabled = if ($PSBoundParameters.ContainsKey('IncidentCaptureEnabled')) { [bool]$IncidentCaptureEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'enabled') { [bool]$existingConfig.incidentCapture.enabled } else { $true } $effectiveIncidentScreenshotEnabled = if ($PSBoundParameters.ContainsKey('IncidentScreenshotEnabled')) { [bool]$IncidentScreenshotEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$existingConfig.incidentCapture.screenshotEnabled } else { $true } @@ -78,11 +82,12 @@ elseif ($existingConfig) { @($existingConfig.userTasks | ForEach-Object { [string]$_.userId }) } else { - throw 'Target users are missing.' + throw 'Не указаны целевые пользователи.' } New-ActivityWatchDirectory -Path $effectiveStateRoot New-ActivityWatchDirectory -Path $effectiveLogsRoot +Enable-ActivityWatchPrintTelemetry if ($RepairPackage) { $workingRoot = Join-Path $env:TEMP 'activitywatch-windows-deploy' @@ -96,6 +101,8 @@ 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') ` + -FileCollectorScriptSource (Join-Path $PSScriptRoot 'file-operations-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 +122,8 @@ $config = New-ActivityWatchDeploymentConfig ` -LogsRoot $effectiveLogsRoot ` -CollectorScript $effectiveCollector ` -EndpointCollectorScript $effectiveEndpointCollector ` + -FileCollectorScript $effectiveFileCollector ` + -SessionCollectorScript $effectiveSessionCollector ` -RulesPath $effectiveRules ` -PolicyPath $effectivePolicy ` -PollSeconds $effectivePollSeconds ` @@ -122,6 +131,7 @@ $config = New-ActivityWatchDeploymentConfig ` -RecoveryIntervalSeconds $effectiveRecoveryInterval ` -AfkEnabled $effectiveAfkEnabled ` -WindowEnabled $effectiveWindowEnabled ` + -FileOpsEnabled $effectiveFileOpsEnabled ` -LocalAgentLogsEnabled $effectiveLocalAgentLogsEnabled ` -IncidentCaptureEnabled $effectiveIncidentCaptureEnabled ` -IncidentScreenshotEnabled $effectiveIncidentScreenshotEnabled ` @@ -139,6 +149,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 ', ')" diff --git a/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss b/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss index aaabb72..22c0dde 100644 --- a/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss +++ b/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss @@ -5,8 +5,8 @@ #define AwDefaultServerHost "10.10.10.13" #define AwDefaultServerPort "5600" #define AwDefaultUsers "user1,user2,user3,user4,user5" -#define AwDefaultInstallRoot "C:\\Program Files\\ActivityWatch-Phase2" -#define AwDefaultStateRoot "C:\\ProgramData\\ActivityWatch-Phase2" +#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] @@ -38,6 +38,7 @@ Source: "..\..\deploy-domain-users.ps1"; DestDir: "{app}\windows"; Flags: ignore 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 diff --git a/windows/installkit/innosetup/BUILD.md b/windows/installkit/innosetup/BUILD.md index 5a96501..a432490 100644 --- a/windows/installkit/innosetup/BUILD.md +++ b/windows/installkit/innosetup/BUILD.md @@ -32,4 +32,4 @@ 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\ActivityWatch-Phase2\ensemble-report-*.json`) +- Whether to validate after deploy (`-ValidateAfterDeploy`, report written to `C:\ProgramData\AWatch-rus\ensemble-report-*.json`) diff --git a/windows/installkit/innosetup/innosetup-rdp-package-filelist.md b/windows/installkit/innosetup/innosetup-rdp-package-filelist.md index 4b36595..fba1cc1 100644 --- a/windows/installkit/innosetup/innosetup-rdp-package-filelist.md +++ b/windows/installkit/innosetup/innosetup-rdp-package-filelist.md @@ -10,13 +10,14 @@ - Он описывает, **что не класть** (генерируется уже на целевом хосте). - Он **не меняет** текущие deploy-скрипты и логику проекта. -## Важное уточнение по `phase2` +## Важное уточнение по единой Windows-директории Чтобы исключить путаницу: -1. В каталоге `windows/` нет файлов с именами `phase2-*`. -2. `phase2` в проекте — это обозначение этапа/набора телеметрии (DLP + endpoint signals). -3. Файлы вида `phase2-*.deployment-config.json` — это **примерные конфиги install-kit**, они лежат в `install-kit-*/server-configs-*`. +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 пакета @@ -30,6 +31,7 @@ - `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) @@ -53,11 +55,11 @@ Эти файлы/папки появляются на целевом Windows-хосте во время/после деплоя: -- `C:\ProgramData\ActivityWatch\deployment-config.json` -- `C:\ProgramData\ActivityWatch\web-category-rules.json` -- `C:\ProgramData\ActivityWatch\dlp-policy.json` -- `C:\ProgramData\ActivityWatch\logs\*` -- `%LOCALAPPDATA%\ActivityWatch-Phase2\incident-artifacts\*` +- `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 @@ -75,6 +77,7 @@ - `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` @@ -86,8 +89,8 @@ 1. Все файлы из раздела 1 присутствуют. 2. В .iss не осталось вызова `deploy-ensemble.ps1` без параметров: нужны `-ServerHost` и `-Users`. -3. Для **Phase2** используются пути: - - `InstallRoot = C:\Program Files\ActivityWatch-Phase2` - - `StateRoot = C:\ProgramData\ActivityWatch-Phase2` +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\ActivityWatch-Phase2\`). +5. Для проверки используется `-ValidateAfterDeploy` (отчёт `ensemble-report-*.json` пишется в `C:\ProgramData\AWatch-rus\`). diff --git a/windows/migrate-awatch-rus-paths.ps1 b/windows/migrate-awatch-rus-paths.ps1 new file mode 100644 index 0000000..c588050 --- /dev/null +++ b/windows/migrate-awatch-rus-paths.ps1 @@ -0,0 +1,243 @@ +[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) { + $backupDest = Join-Path $backupRoot $item.Name + New-ActivityWatchDirectory -Path $backupDest + + $excludeDirs = @() + if ($item.Source -eq $NewStateRoot) { + # Avoid infinite recursion: backupRoot is inside NewStateRoot by default. + $excludeDirs += $backupRoot + } + + $robocopyArgs = @( + $item.Source, + $backupDest, + '/E', + '/R:1', + '/W:1', + '/NFL', + '/NDL', + '/NJH', + '/NJS', + '/NP' + ) + if ($excludeDirs.Count -gt 0) { + $robocopyArgs += '/XD' + $robocopyArgs += $excludeDirs + } + + & robocopy @robocopyArgs | Out-Null + if ($LASTEXITCODE -ge 8) { + throw "Backup robocopy failed (exit=$LASTEXITCODE) for source '$($item.Source)' to '$backupDest'" + } + } + } + + 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 + } +} diff --git a/windows/validate-deployment.ps1 b/windows/validate-deployment.ps1 index 9f133a4..41b6bd9 100644 --- a/windows/validate-deployment.ps1 +++ b/windows/validate-deployment.ps1 @@ -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,15 +14,31 @@ $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' } +$fileCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]$config.paths.fileCollectorScript } else { Join-Path $stateRoot 'file-operations-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 } +$fileOpsExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]$config.collectors.fileOpsEnabled } else { $true } +$printServiceOperationalEnabled = $false +try { + $printServiceLog = Get-WinEvent -ListLog 'Microsoft-Windows-PrintService/Operational' -ErrorAction Stop + $printServiceOperationalEnabled = [bool]$printServiceLog.IsEnabled +} +catch { +} +$printJobTitlePolicyEnabled = $false +try { + $printPolicy = Get-ItemProperty -LiteralPath 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' -Name 'ShowJobTitleInEventLogs' -ErrorAction Stop + $printJobTitlePolicyEnabled = ([int]$printPolicy.ShowJobTitleInEventLogs -eq 1) +} +catch { +} $requiredFiles = @( - (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe'), - (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'), $collectorScript, $endpointCollectorScript, $sessionCollectorScript, @@ -32,19 +48,35 @@ $requiredFiles = @( $recoveryScript, $ConfigPath ) +if ($fileOpsExpected) { + $requiredFiles += $fileCollectorScript +} +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 -$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 +$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) { @@ -53,25 +85,28 @@ if ($config.userTasks) { $taskNames += [string]$config.recovery.taskName $taskNames = $taskNames | Sort-Object -Unique -$tasks = foreach ($taskName in $taskNames) { - $task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1 - if ($task) { - [pscustomobject]@{ - taskName = $task.TaskName - state = [string]$task.State - present = $true +$tasks = @( + foreach ($taskName in $taskNames) { + $task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1 + if ($task) { + [pscustomobject]@{ + taskName = $task.TaskName + state = [string]$task.State + present = $true + } + } + else { + [pscustomobject]@{ + taskName = $taskName + state = 'Отсутствует' + present = $false + } } } - else { - [pscustomobject]@{ - taskName = $taskName - state = 'Missing' - present = $false - } - } -} +) $serverUrl = '{0}://{1}:{2}' -f [string]$config.server.scheme, [string]$config.server.host, [int]$config.server.port +$uniqueRunningProcessNames = @($runningProcesses | Select-Object -ExpandProperty Name -Unique) $result = [ordered]@{ generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') configPath = $ConfigPath @@ -88,15 +123,24 @@ $result = [ordered]@{ ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present })) } processes = [ordered]@{ + expected = $processNames list = @($runningProcesses) sessionCollectors = @($sessionCollectorProcesses) ok = [bool]( - (($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2) -and + ( + ($processNames.Count -eq 0) -or + ($uniqueRunningProcessNames.Count -ge $processNames.Count) + ) -and ($sessionCollectorProcesses.Count -ge 1) ) } + printTelemetry = [ordered]@{ + operationalLogEnabled = $printServiceOperationalEnabled + jobTitlePolicyEnabled = $printJobTitlePolicyEnabled + ok = [bool]($printServiceOperationalEnabled -and $printJobTitlePolicyEnabled) + } } -$result.overallOk = [bool]($result.files.ok -and $result.tasks.ok -and $result.processes.ok) +$result.overallOk = [bool]($result.files.ok -and $result.tasks.ok -and $result.processes.ok -and $result.printTelemetry.ok) $result diff --git a/windows/worktime-session-collector.ps1 b/windows/worktime-session-collector.ps1 index b030cc9..f1b04ef 100644 --- a/windows/worktime-session-collector.ps1 +++ b/windows/worktime-session-collector.ps1 @@ -1,5 +1,5 @@ param( - [string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json', + [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$Hostname, [int]$PollSeconds = 30 ) @@ -11,7 +11,7 @@ function Get-Config { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { - throw "Config not found: $Path" + throw "Конфигурация не найдена: $Path" } Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json @@ -70,16 +70,25 @@ function Get-SessionRecords { continue } + $sessionName = '' + $sessionIdIndex = 2 + if ($parts[1] -match '^\d+$') { + $sessionIdIndex = 1 + } + else { + $sessionName = $parts[1] + } + $sessionId = 0 - if ($parts[2] -match '^\d+$') { - $sessionId = [int]$parts[2] + if ($parts[$sessionIdIndex] -match '^\d+$') { + $sessionId = [int]$parts[$sessionIdIndex] } $records += [pscustomobject]@{ username = $parts[0] - sessionName = $parts[1] + sessionName = $sessionName sessionId = $sessionId - state = $parts[3] + state = $parts[$sessionIdIndex + 1] } } }