Merge remote-tracking branch 'origin/devin/1777752962-file-collector-bucket'
This commit is contained in:
@@ -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."
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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'а:
|
||||
|
||||
|
||||
+67
-39
@@ -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_<COMPUTERNAME>` и ожидает свежие `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_<COMPUTERNAME>`;
|
||||
- `aw_windows_fail_on_validation_error: true` — завершать playbook ошибкой, если `validate-deployment.ps1` возвращает `overallOk=false`;
|
||||
- `aw_windows_skip_hardening: true` — пропустить `hardening-recovery.ps1` внутри ensemble-скрипта.
|
||||
|
||||
## pfSense poller rollout
|
||||
## Развёртывание pfSense poller
|
||||
|
||||
1. Подготовьте vars:
|
||||
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.yml`
|
||||
- `cp ansible/group_vars/pfsense-poller.example.yml ansible/group_vars/pfsense-poller.yml`
|
||||
2. Добавьте inventory group `[aw_pfsense_pollers]`.
|
||||
3. Запустите:
|
||||
|
||||
```bash
|
||||
cd /home/igor/tmp/AWatch-rus/ansible
|
||||
cd ansible
|
||||
ansible-playbook -i inventory.ini deploy_aw_pfsense_poller.yml
|
||||
```
|
||||
|
||||
@@ -139,4 +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/`.
|
||||
|
||||
@@ -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
|
||||
|
||||
+545
-111
@@ -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: '</head>'
|
||||
replace: '<script src="/js/sw-cleanup.js?v={{ aw_sw_cleanup_cache_bust }}"></script></head>'
|
||||
- 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: '</body>'
|
||||
replace: '<script defer="defer" src="/js/ru-patch-v5.js?v={{ aw_ru_patch_cache_bust }}"></script></body>'
|
||||
- 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: '<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*></script>'
|
||||
replace: ''
|
||||
|
||||
- name: Добавить cleanup script RU patch в index.html
|
||||
ansible.builtin.replace:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
regexp: '</head>'
|
||||
replace: '<script src="/js/sw-cleanup.js?v={{ aw_sw_cleanup_cache_bust }}"></script></head>'
|
||||
|
||||
- name: Добавить загрузчик RU patch перед закрытием body
|
||||
ansible.builtin.replace:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
regexp: '</body>'
|
||||
replace: '<script defer="defer" src="/js/ru-patch-v5.js?v={{ aw_ru_patch_cache_bust }}"></script></body>'
|
||||
|
||||
- name: Скопировать 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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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/"
|
||||
|
||||
@@ -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/"
|
||||
@@ -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) }}"
|
||||
@@ -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
|
||||
|
||||
@@ -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_<COMPUTERNAME>.
|
||||
# Задайте явное значение только если watcher пишет в нестандартный bucket.
|
||||
aw_windows_api_smoke_check_enabled: true
|
||||
aw_windows_api_smoke_check_bucket: ""
|
||||
aw_windows_api_smoke_check_limit: 10
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
- name: Provision single Proxmox CT and deploy AWatch-rus
|
||||
- name: Создать один Proxmox CT и развернуть AWatch-rus
|
||||
hosts: proxmox
|
||||
gather_facts: false
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
- settings/views-default.json
|
||||
|
||||
tasks:
|
||||
- name: Execute single-CT provisioning workflow
|
||||
- name: Выполнить workflow создания одного CT
|
||||
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
|
||||
vars:
|
||||
ct_id: "{{ proxmox_ct_id }}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
- name: Provision Proxmox CT matrix and deploy AWatch-rus with RU patch
|
||||
- name: Создать матрицу Proxmox CT и развернуть AWatch-rus с RU patch
|
||||
hosts: proxmox
|
||||
gather_facts: false
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
- settings/views-default.json
|
||||
|
||||
tasks:
|
||||
- name: Validate CT matrix is provided
|
||||
- name: Проверить, что матрица CT задана
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- proxmox_ct_matrix is defined
|
||||
- proxmox_ct_matrix | length > 0
|
||||
fail_msg: "Define proxmox_ct_matrix in group_vars/proxmox-matrix.yml"
|
||||
fail_msg: "Задайте proxmox_ct_matrix в group_vars/proxmox-matrix.yml"
|
||||
|
||||
- name: Execute provisioning workflow for each CT
|
||||
- name: Выполнить workflow создания для каждого CT
|
||||
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
|
||||
vars:
|
||||
ct_id: "{{ item.id }}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
- name: Validate required per-CT variables
|
||||
- name: Проверить обязательные переменные CT
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ct_id is defined
|
||||
@@ -27,14 +27,14 @@
|
||||
- aw_server_log_dir is defined
|
||||
- aw_server_user is defined
|
||||
- aw_server_group is defined
|
||||
fail_msg: "Missing required variables for CT provisioning/deploy."
|
||||
fail_msg: "Не заданы обязательные переменные для создания CT и развёртывания."
|
||||
|
||||
- name: Build CT network string
|
||||
- name: Сформировать сетевую строку CT
|
||||
ansible.builtin.set_fact:
|
||||
ct_net0: >-
|
||||
name=eth0,bridge={{ ct_bridge }},ip={{ ct_ip }},gw={{ ct_gw }}{% if (ct_vlan | default('') | string | length) > 0 %},tag={{ ct_vlan }}{% endif %}
|
||||
|
||||
- name: Check whether CT already exists
|
||||
- name: Проверить, существует ли CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -44,7 +44,7 @@
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Create CT when absent
|
||||
- name: Создать CT, если он отсутствует
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -78,8 +78,9 @@
|
||||
- --ostype
|
||||
- debian
|
||||
when: ct_status_check.rc != 0
|
||||
no_log: true
|
||||
|
||||
- name: Check current CT runtime state
|
||||
- name: Проверить текущее состояние CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -88,7 +89,7 @@
|
||||
register: ct_runtime_status
|
||||
changed_when: false
|
||||
|
||||
- name: Start CT when stopped
|
||||
- name: Запустить CT, если он остановлен
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -96,20 +97,23 @@
|
||||
- "{{ ct_id }}"
|
||||
when: "'stopped' in ct_runtime_status.stdout"
|
||||
|
||||
- name: Ensure bootstrap directory on Proxmox host
|
||||
- name: Создать bootstrap каталог на Proxmox host
|
||||
ansible.builtin.file:
|
||||
path: "{{ proxmox_bootstrap_dir }}"
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
mode: "0700"
|
||||
loop:
|
||||
- "{{ proxmox_bootstrap_dir }}"
|
||||
- "{{ proxmox_bootstrap_dir }}/settings"
|
||||
|
||||
- name: Copy AW bootstrap files to Proxmox host temp
|
||||
- name: Скопировать AW bootstrap файлы во временный каталог Proxmox host
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/{{ item }}"
|
||||
dest: "{{ proxmox_bootstrap_dir }}/{{ item }}"
|
||||
mode: "0644"
|
||||
loop: "{{ aw_bootstrap_files }}"
|
||||
|
||||
- name: Bootstrap CT OS dependencies
|
||||
- name: Установить базовые зависимости ОС внутри CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -122,12 +126,16 @@
|
||||
set -euo pipefail
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y curl ca-certificates bash unzip xz-utils jq rsync openssh-server
|
||||
mkdir -p /root/bootstrap /etc/activitywatch
|
||||
apt-get install -y curl ca-certificates bash unzip xz-utils jq rsync openssh-server python3
|
||||
mkdir -p /root/bootstrap/settings /etc/activitywatch
|
||||
systemctl enable ssh || true
|
||||
systemctl restart ssh || true
|
||||
register: ct_bootstrap_result
|
||||
retries: 10
|
||||
delay: 6
|
||||
until: ct_bootstrap_result.rc == 0
|
||||
|
||||
- name: Push bootstrap files into CT
|
||||
- name: Передать bootstrap файлы внутрь CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -137,7 +145,7 @@
|
||||
- "/root/bootstrap/{{ item }}"
|
||||
loop: "{{ aw_bootstrap_files }}"
|
||||
|
||||
- name: Write AW server env file on Proxmox host temp
|
||||
- name: Записать AW server env во временный каталог Proxmox host
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ proxmox_bootstrap_dir }}/aw-server.env"
|
||||
mode: "0600"
|
||||
@@ -151,8 +159,9 @@
|
||||
AW_SERVER_LOG_DIR={{ aw_server_log_dir }}
|
||||
AW_SERVER_USER={{ aw_server_user }}
|
||||
AW_SERVER_GROUP={{ aw_server_group }}
|
||||
no_log: true
|
||||
|
||||
- name: Push AW server env into CT
|
||||
- name: Передать AW server env внутрь CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -160,8 +169,9 @@
|
||||
- "{{ ct_id }}"
|
||||
- "{{ proxmox_bootstrap_dir }}/aw-server.env"
|
||||
- /etc/activitywatch/aw-server.env
|
||||
no_log: true
|
||||
|
||||
- name: Set mode for env inside CT
|
||||
- name: Настроить права env файла внутри CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -172,7 +182,7 @@
|
||||
- "0600"
|
||||
- /etc/activitywatch/aw-server.env
|
||||
|
||||
- name: Install server and apply RU patch inside CT
|
||||
- name: Установить сервер и применить RU patch внутри CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -188,7 +198,7 @@
|
||||
bash /root/bootstrap/apply_webui_ru_patch.sh
|
||||
systemctl restart activitywatch-server.service
|
||||
|
||||
- name: Validate AW API from inside CT
|
||||
- name: Проверить AW API изнутри CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -199,7 +209,7 @@
|
||||
- -lc
|
||||
- "curl -fsS http://127.0.0.1:{{ aw_server_port }}/api/0/info >/dev/null"
|
||||
|
||||
- name: Validate RU patch hooks in index
|
||||
- name: Проверить hooks RU patch в index.html
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -210,8 +220,8 @@
|
||||
- -lc
|
||||
- "grep -q 'ru-patch-v5.js' {{ aw_server_webui_dir }}/index.html && grep -q 'sw-cleanup.js' {{ aw_server_webui_dir }}/index.html"
|
||||
|
||||
- name: Show final endpoint
|
||||
- name: Показать итоговый endpoint
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "CT {{ ct_id }} is provisioned and configured."
|
||||
- "ActivityWatch endpoint: http://{{ ct_ip | regex_replace('/[0-9]+$', '') }}:{{ aw_server_port }}"
|
||||
- "CT {{ ct_id }} создан и настроен."
|
||||
- "Endpoint ActivityWatch: http://{{ ct_ip | regex_replace('/[0-9]+$', '') }}:{{ aw_server_port }}"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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\`
|
||||
|
||||
Задачи планировщика:
|
||||
|
||||
|
||||
@@ -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 '<strong generated 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;
|
||||
```
|
||||
@@ -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 (внедрено)
|
||||
|
||||
|
||||
+55
-27
@@ -8,21 +8,21 @@
|
||||
- `windows/hardening-recovery.ps1` — повторная регистрация задач, ACL и recovery-loop.
|
||||
- `windows/validate-deployment.ps1` — машинная проверка состояния и JSON-отчёт.
|
||||
- `windows/browser-domains-native-collector.ps1` — native collector доменов браузера с категоризацией.
|
||||
- `windows/dlp-endpoint-signals-collector.ps1` — phase-2 collector (clipboard/USB/print signals).
|
||||
- `windows/dlp-endpoint-signals-collector.ps1` — Windows/RDP collector (clipboard/USB/print signals).
|
||||
- `windows/web-category-rules.example.json` — пример кастомных правил категоризации.
|
||||
- `windows/dlp-policy.example.json` — пример DLP-политики (phase-1: alerting incidents).
|
||||
|
||||
## Что делает пакет
|
||||
|
||||
- Ставит `aw-watcher-afk` и `aw-watcher-window` из официального Windows ZIP ActivityWatch.
|
||||
- Копирует browser-domain collector в `C:\ProgramData\ActivityWatch`.
|
||||
- Копирует DLP policy в `C:\ProgramData\ActivityWatch\dlp-policy.json`.
|
||||
- Копирует browser-domain collector в `C:\ProgramData\AWatch-rus`.
|
||||
- Копирует DLP policy в `C:\ProgramData\AWatch-rus\dlp-policy.json`.
|
||||
- Включает `incidentCapture` в `deployment-config.json` для DLP-инцидентов:
|
||||
- `incidentCapture.screenshotEnabled = true`
|
||||
- `incidentCapture.artifactsRoot = <StateRoot>\incident-artifacts`
|
||||
- Создаёт per-user задачи `ActivityWatch Launch [...]` с запуском при логоне.
|
||||
- Создаёт системную задачу `ActivityWatch Recovery`, которая циклически перезапускает per-user launch tasks.
|
||||
- Применяет ACL к `C:\Program Files\ActivityWatch`, `C:\ProgramData\ActivityWatch` и каталогу логов.
|
||||
- Применяет ACL к `C:\Program Files\AWatch-rus\bin`, `C:\ProgramData\AWatch-rus` и каталогу логов.
|
||||
- Не содержит хардкодов инфраструктуры: сервер, домен, список пользователей и правила передаются параметрами.
|
||||
- Корректно регистрирует задачи через `-LogonType Interactive` (совместимо с Windows Server, где `InteractiveToken` не поддерживается).
|
||||
- Поддерживает отключение шумных watcher'ов через `-AfkEnabled:$false` и `-WindowEnabled:$false`.
|
||||
@@ -143,9 +143,37 @@ CSV-формат: колонка `User`, `Username`, `SamAccountName` или `Lo
|
||||
|
||||
Если список уже содержит `DOMAIN\user`, параметр `-Domain` не нужен.
|
||||
|
||||
## Рекомендуемый phased rollout (изолированный профиль)
|
||||
## Безопасная миграция текущего production
|
||||
|
||||
Для безопасного параллельного запуска рядом с legacy-инсталляцией используйте отдельные пути:
|
||||
Если текущий RDP production уже работает в `C:\Program Files\ActivityWatch-Phase2` и
|
||||
`C:\ProgramData\ActivityWatch-Phase2`, не запускайте обычный update без миграции.
|
||||
Сначала выполните перенос в единый профиль AWatch-rus:
|
||||
|
||||
```powershell
|
||||
C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1 -WhatIf
|
||||
C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1
|
||||
```
|
||||
|
||||
Скрипт делает безопасный порядок:
|
||||
|
||||
1. Находит старый `deployment-config.json`.
|
||||
2. Останавливает `ActivityWatch Recovery` и `ActivityWatch Launch *`.
|
||||
3. Создаёт backup старых и новых каталогов в
|
||||
`C:\ProgramData\AWatch-rus\migration-backups\YYYYMMDD-HHMMSS`.
|
||||
4. Копирует бинарники/состояние в единые пути:
|
||||
- `C:\Program Files\AWatch-rus\bin`
|
||||
- `C:\ProgramData\AWatch-rus`
|
||||
5. Переписывает пути в `deployment-config.json`.
|
||||
6. Пересоздаёт launcher/recovery scripts и scheduled tasks.
|
||||
7. Запускает `validate-deployment.ps1`; при ошибке оставляет backup path в сообщении.
|
||||
|
||||
Ansible playbook `ansible/deploy_aw_windows.yml` выполняет этот migration guard
|
||||
автоматически, если на хосте найден
|
||||
`C:\ProgramData\ActivityWatch-Phase2\deployment-config.json`.
|
||||
|
||||
## Рекомендуемый rollout
|
||||
|
||||
Для запуска после миграции используйте единые пути:
|
||||
|
||||
```powershell
|
||||
.\windows\deploy-domain-users.ps1 `
|
||||
@@ -153,10 +181,10 @@ CSV-формат: колонка `User`, `Username`, `SamAccountName` или `Lo
|
||||
-ServerPort 5600 `
|
||||
-Domain SHARKON2025 `
|
||||
-Users user2,user3,user4,user5 `
|
||||
-InstallRoot 'C:\Program Files\ActivityWatch-Phase2-u2u5' `
|
||||
-StateRoot 'C:\ProgramData\ActivityWatch-Phase2-u2u5' `
|
||||
-CustomRulesPath C:\Deploy\AWatch-rus\windows\web-category-rules.example.json `
|
||||
-CustomPolicyPath C:\Deploy\AWatch-rus\windows\dlp-policy.example.json
|
||||
-InstallRoot 'C:\Program Files\AWatch-rus\bin' `
|
||||
-StateRoot 'C:\ProgramData\AWatch-rus' `
|
||||
-CustomRulesPath C:\Program Files\AWatch-rus\windows\web-category-rules.example.json `
|
||||
-CustomPolicyPath C:\Program Files\AWatch-rus\windows\dlp-policy.example.json
|
||||
```
|
||||
|
||||
Single-user pilot в таком же стиле:
|
||||
@@ -166,10 +194,10 @@ Single-user pilot в таком же стиле:
|
||||
-ServerHost 10.10.10.13 `
|
||||
-ServerPort 5600 `
|
||||
-TargetUser 'SHARKON2025\user1' `
|
||||
-InstallRoot 'C:\Program Files\ActivityWatch-Phase2' `
|
||||
-StateRoot 'C:\ProgramData\ActivityWatch-Phase2-user1' `
|
||||
-CustomRulesPath C:\Deploy\AWatch-rus\windows\web-category-rules.example.json `
|
||||
-CustomPolicyPath C:\Deploy\AWatch-rus\windows\dlp-policy.example.json
|
||||
-InstallRoot 'C:\Program Files\AWatch-rus\bin' `
|
||||
-StateRoot 'C:\ProgramData\AWatch-rus' `
|
||||
-CustomRulesPath C:\Program Files\AWatch-rus\windows\web-category-rules.example.json `
|
||||
-CustomPolicyPath C:\Program Files\AWatch-rus\windows\dlp-policy.example.json
|
||||
```
|
||||
|
||||
## Ensemble deploy (production workflow)
|
||||
@@ -186,31 +214,31 @@ Single-user pilot в таком же стиле:
|
||||
|
||||
Итоговый отчёт:
|
||||
|
||||
- `C:\ProgramData\ActivityWatch\ensemble-report-YYYYMMDD-HHMMSS.json`
|
||||
- `C:\ProgramData\AWatch-rus\ensemble-report-YYYYMMDD-HHMMSS.json`
|
||||
|
||||
## Категоризация доменов
|
||||
|
||||
- Встроенные категории покрывают базовые рабочие, нейтральные и личные домены.
|
||||
- Для кастомизации скопируйте `windows/web-category-rules.example.json` и отредактируйте домены.
|
||||
- Передайте файл через `-CustomRulesPath`; он будет сохранён как `C:\ProgramData\ActivityWatch\web-category-rules.json`.
|
||||
- Передайте файл через `-CustomRulesPath`; он будет сохранён как `C:\ProgramData\AWatch-rus\web-category-rules.json`.
|
||||
- Пользовательские правила имеют приоритет над встроенными.
|
||||
|
||||
## Структура после установки
|
||||
|
||||
- `C:\Program Files\ActivityWatch` — бинарники watcher'ов.
|
||||
- `C:\ProgramData\ActivityWatch\deployment-config.json` — итоговая конфигурация.
|
||||
- `C:\ProgramData\ActivityWatch\incident-artifacts\` — скриншоты DLP-инцидентов (если `incidentCapture.screenshotEnabled=true`).
|
||||
- `C:\ProgramData\ActivityWatch\launch-watchers.ps1` — per-user launcher.
|
||||
- `C:\ProgramData\ActivityWatch\recovery-loop.ps1` — system recovery loop.
|
||||
- `C:\ProgramData\ActivityWatch\browser-domains-native-collector.ps1` — runtime collector.
|
||||
- `C:\ProgramData\ActivityWatch\dlp-endpoint-signals-collector.ps1` — runtime endpoint collector.
|
||||
- `C:\ProgramData\ActivityWatch\dlp-policy.json` — активная DLP-политика.
|
||||
- `C:\ProgramData\ActivityWatch\logs\` — логи collector'а.
|
||||
- `C:\Program Files\AWatch-rus\bin` — бинарники watcher'ов.
|
||||
- `C:\ProgramData\AWatch-rus\deployment-config.json` — итоговая конфигурация.
|
||||
- `C:\ProgramData\AWatch-rus\incident-artifacts\` — скриншоты DLP-инцидентов (если `incidentCapture.screenshotEnabled=true`).
|
||||
- `C:\ProgramData\AWatch-rus\launch-watchers.ps1` — per-user launcher.
|
||||
- `C:\ProgramData\AWatch-rus\recovery-loop.ps1` — system recovery loop.
|
||||
- `C:\ProgramData\AWatch-rus\browser-domains-native-collector.ps1` — runtime collector.
|
||||
- `C:\ProgramData\AWatch-rus\dlp-endpoint-signals-collector.ps1` — runtime endpoint collector.
|
||||
- `C:\ProgramData\AWatch-rus\dlp-policy.json` — активная DLP-политика.
|
||||
- `C:\ProgramData\AWatch-rus\logs\` — логи collector'а.
|
||||
|
||||
Для phased rollout те же файлы формируются в каталоге `StateRoot`, переданном параметром.
|
||||
При переопределении `StateRoot` те же файлы формируются в указанном каталоге.
|
||||
|
||||
## Повторный прогон
|
||||
|
||||
- Скрипты идемпотентны: переустанавливают задачи и обновляют runtime-файлы.
|
||||
- Предыдущая установка ActivityWatch бэкапится в `C:\ProgramData\ActivityWatch\backups\install-YYYYMMDD-HHMMSS`.
|
||||
- Предыдущая установка ActivityWatch бэкапится в `C:\ProgramData\AWatch-rus\backups\install-YYYYMMDD-HHMMSS`.
|
||||
- Для жёсткого восстановления запускайте `windows/hardening-recovery.ps1`.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Start-ScheduledTask -TaskName 'ActivityWatch Launch [CONTOSO_user01]'
|
||||
|
||||
- Скрипт работает через UI Automation и foreground window.
|
||||
- Некоторые браузеры/страницы могут скрывать адресную строку или блокировать UIA.
|
||||
- Проверьте лог `C:\ProgramData\ActivityWatch\logs\browser-domains-<user>.log`.
|
||||
- Проверьте лог `C:\ProgramData\AWatch-rus\logs\browser-domains-<user>.log`.
|
||||
- Убедитесь, что активное окно — поддерживаемый браузер: Edge, Chrome, Brave, Vivaldi, Opera, Firefox.
|
||||
|
||||
### Сервер недоступен
|
||||
@@ -44,7 +44,7 @@ Invoke-WebRequest http://aw.example.local:5600/api/0/info
|
||||
|
||||
### Неправильная категоризация домена
|
||||
|
||||
- Проверьте содержимое `C:\ProgramData\ActivityWatch\web-category-rules.json`.
|
||||
- Проверьте содержимое `C:\ProgramData\AWatch-rus\web-category-rules.json`.
|
||||
- Пользовательские правила должны быть валидным JSON.
|
||||
- Один и тот же домен лучше определять только в одной категории.
|
||||
- После изменения правил достаточно перезапустить collector или задачу пользователя:
|
||||
@@ -112,7 +112,7 @@ Get-CimInstance Win32_Process |
|
||||
Проверить конфиг:
|
||||
|
||||
```powershell
|
||||
Get-Content C:\ProgramData\ActivityWatch\deployment-config.json -Raw
|
||||
Get-Content C:\ProgramData\AWatch-rus\deployment-config.json -Raw
|
||||
```
|
||||
|
||||
## Когда запускать hardening/recovery
|
||||
|
||||
+13
-13
@@ -4,7 +4,7 @@
|
||||
|
||||
```powershell
|
||||
$report = .\windows\validate-deployment.ps1 `
|
||||
-ConfigPath C:\ProgramData\ActivityWatch\deployment-config.json
|
||||
-ConfigPath C:\ProgramData\AWatch-rus\deployment-config.json
|
||||
$report | ConvertTo-Json -Depth 12
|
||||
```
|
||||
|
||||
@@ -19,11 +19,11 @@ $report | ConvertTo-Json -Depth 12
|
||||
### 1. Проверить установленные файлы
|
||||
|
||||
```powershell
|
||||
Test-Path 'C:\Program Files\ActivityWatch\aw-watcher-afk\aw-watcher-afk.exe'
|
||||
Test-Path 'C:\Program Files\ActivityWatch\aw-watcher-window\aw-watcher-window.exe'
|
||||
Test-Path 'C:\ProgramData\ActivityWatch\browser-domains-native-collector.ps1'
|
||||
Test-Path 'C:\ProgramData\ActivityWatch\dlp-policy.json'
|
||||
Test-Path 'C:\ProgramData\ActivityWatch\deployment-config.json'
|
||||
Test-Path 'C:\Program Files\AWatch-rus\bin\aw-watcher-afk\aw-watcher-afk.exe'
|
||||
Test-Path 'C:\Program Files\AWatch-rus\bin\aw-watcher-window\aw-watcher-window.exe'
|
||||
Test-Path 'C:\ProgramData\AWatch-rus\browser-domains-native-collector.ps1'
|
||||
Test-Path 'C:\ProgramData\AWatch-rus\dlp-policy.json'
|
||||
Test-Path 'C:\ProgramData\AWatch-rus\deployment-config.json'
|
||||
```
|
||||
|
||||
Ожидаемый результат — везде `True`.
|
||||
@@ -50,7 +50,7 @@ Get-ScheduledTask | Where-Object TaskName -eq 'ActivityWatch Recovery'
|
||||
### 2.1 Проверить incidentCapture в конфиге
|
||||
|
||||
```powershell
|
||||
$cfg = Get-Content 'C:\ProgramData\ActivityWatch\deployment-config.json' -Raw | ConvertFrom-Json
|
||||
$cfg = Get-Content 'C:\ProgramData\AWatch-rus\deployment-config.json' -Raw | ConvertFrom-Json
|
||||
$cfg.incidentCapture
|
||||
```
|
||||
|
||||
@@ -119,7 +119,7 @@ Invoke-WebRequest http://aw.example.local:5600/api/0/buckets | Select-Object -Ex
|
||||
4. Проверьте локальный лог:
|
||||
|
||||
```powershell
|
||||
Get-Content "C:\ProgramData\ActivityWatch\logs\dlp-incidents-$env:USERNAME.log" -Tail 50
|
||||
Get-Content "C:\ProgramData\AWatch-rus\logs\dlp-incidents-$env:USERNAME.log" -Tail 50
|
||||
```
|
||||
|
||||
Если `screenshotEnabled = True`, проверьте наличие скриншота в инциденте:
|
||||
@@ -175,7 +175,7 @@ Invoke-RestMethod -Method Post `
|
||||
- в API появился `ruleId=selftest-dlp-incident`;
|
||||
- в UI (`#/buckets/aw-dlp-incidents_<hostname>`) событие видно в `Events`.
|
||||
|
||||
## Проверка phase-2 endpoint signals
|
||||
## Проверка endpoint signals
|
||||
|
||||
1. Скопируйте любой текст в буфер обмена.
|
||||
2. Отправьте тестовую печать (любой принтер/виртуальный PDF).
|
||||
@@ -188,7 +188,7 @@ Invoke-WebRequest http://aw.example.local:5600/api/0/buckets/aw-dlp-endpoint-sig
|
||||
4. Проверьте локальный лог:
|
||||
|
||||
```powershell
|
||||
Get-Content "C:\ProgramData\ActivityWatch\logs\endpoint-signals-$env:USERNAME.log" -Tail 50
|
||||
Get-Content "C:\ProgramData\AWatch-rus\logs\endpoint-signals-$env:USERNAME.log" -Tail 50
|
||||
```
|
||||
|
||||
## Проверка восстановления
|
||||
@@ -206,9 +206,9 @@ Start-ScheduledTask -TaskName 'ActivityWatch Recovery'
|
||||
## Проверка ACL
|
||||
|
||||
```powershell
|
||||
icacls 'C:\Program Files\ActivityWatch'
|
||||
icacls 'C:\ProgramData\ActivityWatch'
|
||||
icacls 'C:\ProgramData\ActivityWatch\logs'
|
||||
icacls 'C:\Program Files\AWatch-rus\bin'
|
||||
icacls 'C:\ProgramData\AWatch-rus'
|
||||
icacls 'C:\ProgramData\AWatch-rus\logs'
|
||||
```
|
||||
|
||||
Ожидаемо:
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
6e1f304f468d77f12df67face6afaaca5bbcfb1496f43df4e4fa0557cf847829 install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt
|
||||
a11f41827769be915f73d0de2c5503b05f61ccf56f70a50845771bcb79c5ebb7 install-kit-awindows-20260427-211240/ansible/README.md
|
||||
02ca96f5ecc6abf89ab3271bd08add5795dbba2281168f158d96166f557e33f0 install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml
|
||||
7c1ad9363412e802f4272f2e91a1d9f26be722eaf22e654c0a2512b0cebbbfd0 install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml
|
||||
d5c42e6fe49c14a0769517ff28184139e467d40f22d4a632630c42ed1ff34ce5 install-kit-awindows-20260427-211240/ansible/deploy_aw_windows_phase2.yml
|
||||
bc791462b9c00adc8c68ed81e2a7697c560bdbc46f156b4840c7bca1dee2157d install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml
|
||||
0754dcba7c651d67a40e09446868d2fcae623a100d4fb01794dd96272d353b49 install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt
|
||||
089595753398c8b82980919d230dafac548c3ba36817f5c96a68051582f9faa3 install-kit-awindows-20260427-211240/ansible/README.md
|
||||
412bb766bbf0791c3593f38daa771d5d0aa58cc1f2d3c9010fcd4588d0fe87df install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml
|
||||
90ac38a33918fcd3620f078f51fbf7c6a9d7f8fd1a34d16b38cb3ac45678b0d7 install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml
|
||||
a649eeb57472fb259d248983c486f0474bfb7317600feb98f76a80e7af7e4549 install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml
|
||||
531bfec24f86d28a06e5c0d73005489a818e2c7b1cce1d98f524a3f76802b8ee install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml
|
||||
95696c243ab331f06e77a40a9800c4b6668de77675ebbdf2ef54ae49e1b18874 install-kit-awindows-20260427-211240/ansible/group_vars/pfsense-poller.example.yml
|
||||
c5cab36645065815571c99f6d360f910dcccbb54b780c8bfd526a6cdc3684e19 install-kit-awindows-20260427-211240/ansible/group_vars/proxmox-matrix.example.yml
|
||||
35a33c8a1c75ded5e85c6b79e0b3efde07959ff61ee5f66d83b7e0c2abe87fc5 install-kit-awindows-20260427-211240/ansible/group_vars/proxmox.example.yml
|
||||
69368b7adb7711fa81304866373e61ed464bcc23a08e4509fa54655b05f95790 install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml
|
||||
00064ce5187569fb3221ddd45c2ad7eb37cd50b2a0a832ddf7843e4ff461849a install-kit-awindows-20260427-211240/ansible/inventory.example.ini
|
||||
bbef175cb77dd53aa07452dbb2fe8797f38b58f42372005c3404c8dc9d6f8e13 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml
|
||||
b8f8b6bc504a51cd87db3f46c35a27295b395ea516533068f96af35f8b720434 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml
|
||||
d35bc97b6de18f0006cbbad4adf8a8a5db8ed912997d8fc47c7f11fa9247e907 install-kit-awindows-20260427-211240/ansible/tasks/provision_ct_and_deploy_aw.yml
|
||||
7c468f252e328fd3bb7ee776a45feea88efc4b438dfef55b832da4eea867aaf2 install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml
|
||||
195e7dbdb91f4e77db3263bd0812301ddc912a37ba1519688768bb64a2887567 install-kit-awindows-20260427-211240/ansible/install_full_stack.yml
|
||||
2e4e94d90143923fefd3ec1257d0ec57daa3e96450d85471bc2c418aae37e105 install-kit-awindows-20260427-211240/ansible/inventory.example.ini
|
||||
d9e43352fd6bdb647db9754ab2c557b6bb27f88f51b2bf23d8c19227e535e7b9 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml
|
||||
f3d34547f345ad1c635ee44613a60e7f08479f9ae4d1111810bc7e5968bfb084 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml
|
||||
ef4ed198745777bd1227170241b2db21dc853685f962d6116241c55216b466d6 install-kit-awindows-20260427-211240/ansible/tasks/provision_ct_and_deploy_aw.yml
|
||||
a50dbadbf619342c2178e255b68f69a36503756daf80eedd9170311c63964f2e install-kit-awindows-20260427-211240/aw-server/activitywatch-server.service
|
||||
2dbf55d4a8f204ebdc97af926d90435932c0aad7a2431e2b9987e47c5abf71a9 install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh
|
||||
07d4e583f6e9757a11f01558e1f15cfd73c4d82695f1768204f2f50621712168 install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json
|
||||
@@ -21,17 +22,19 @@ a50dbadbf619342c2178e255b68f69a36503756daf80eedd9170311c63964f2e install-kit-aw
|
||||
dce731fdfdcfd773c154d12dbd6b9e621a0bff17ced5a05a65f0fdcb1adcb70f install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh
|
||||
1856e9f44636030b0cb9ece37ba2a0618eb5187fa82c7969976c1bb5f10fc622 install-kit-awindows-20260427-211240/aw-server/settings/classes-worktime.json
|
||||
ff07b90cb6a7f09b27d522307cf55b0359e136a2e695190b8564e859f14f9204 install-kit-awindows-20260427-211240/aw-server/settings/views-default.json
|
||||
59307d284caa74eb3dc129765f9db93b6e8dfd5b1d960b98f347a332b23f82dc install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-admin.deployment-config.json
|
||||
f2cee1872bf274f15dcfb8fb595fb20a11d228a4bae0930dc6cb918a6f800756 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-u2u5.deployment-config.json
|
||||
6aefedcdac8c1d3823c9f4065b051a67a221a3e9424c913a673667b2a23ea1e7 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-user1.deployment-config.json
|
||||
1654cf688560465fcce629468a0be869b0c81b056179e1b5e9bcc7a2d5ed6ce0 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-admin.deployment-config.json
|
||||
ac022b9a074c542ade66d18af8db385f6c14376ac4eadd54ef033dfa7f60fb50 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-u2u5.deployment-config.json
|
||||
98cf4c54d494318b74cfbd3c8892830a34928bd76a2296d5333e5e176c1f3f49 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-user1.deployment-config.json
|
||||
33aa34b89246d6c079ef9afe2f5cd153bd9d5946b69a175ff6fd678c77f61da5 install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1
|
||||
d506614168227fa01fa481289079b432b6d8846d5ee8961cff8c21fd0bf7ea8f install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1
|
||||
2a0b94ddad43a6bc684037243e636a54c168d8d4ad25b29778c4d78180da2532 install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1
|
||||
98bfcf5dca972f1ba1845bbca546133c192824ec40dd2e55f4e6d59c24a834d1 install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1
|
||||
a19c2a98e6483eb921457f472d1cf62a76e344f2ef3621f04fafa3d7dc2df353 install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1
|
||||
2542425e02acd8a8b02ed701ca2382cf7ae612be28441d1a10a40e48c6a13ad0 install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1
|
||||
a2f963927c8b263a21aaffc0a926a058dcec65a04a5fa57c271d3dbe59f9347c install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1
|
||||
0cb9cd8d8b612429f79899c126f4141ab4fbbb6d919425c0b8fd8d0a74b1bd44 install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1
|
||||
7db2d3767ae81c877e8f04ecbc77a2fc26b2d9bbbf77d0e774fba0a7956bd0a6 install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1
|
||||
b497400a1ba57cddf28dc8e217115dc85eccb67150cbdbb6a81abd804ed20109 install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1
|
||||
973db51854fc744539a7b75e13c6749e822a08a79f47447485611f07e8f902a8 install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1
|
||||
0d66dcb551889e6b7bc21b29d53b77e46f41d61dd2e4e0d9913dbf0f8bd5eb18 install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1
|
||||
8857f3e17f3f3ed6f211ce7f0a0c46c586a2548541078401ee3befaa20924b3d install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1
|
||||
aef0032edd9b1e0c54f7b575664ed511dfc6cb53364e7496cbc95e137678e11a install-kit-awindows-20260427-211240/windows/dlp-policy.example.json
|
||||
ade74a55ce00d9295f2efa0fd72f987688154c93f1142ce0eb6e982f07271be7 install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1
|
||||
88ffe06093ef5f7247bd2b990b0c8f801c706a26dc87725bb1194504fab7e306 install-kit-awindows-20260427-211240/windows/validate-deployment.ps1
|
||||
f03886caf56c6838e8a163d6b48d1f229e83a5682aeeb447c3a65f13d62dbca4 install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1
|
||||
71911cd53ad0abd8bf83994f8a79bbbfe2c0eaf4f4c5f6c2d636dd7786191c2f install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1
|
||||
5dcf249742bd82fa0c803c878bfa0a1344b7b14df85c05f12e8c205663aea158 install-kit-awindows-20260427-211240/windows/validate-deployment.ps1
|
||||
731098681d89b9af6f3872abd586ac3b1faba2d7f9340211e503f52ad0243b3f install-kit-awindows-20260427-211240/windows/web-category-rules.example.json
|
||||
41171f0d7ed1e8b00dd0faf1a4b75c9cb063fd09aba8d333851a7a31fb297de1 install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1
|
||||
|
||||
@@ -4,7 +4,7 @@ Includes:
|
||||
- windows/* (deploy scripts, collectors, common module, configs/examples)
|
||||
- ansible/* (Windows and AW server playbooks, examples, inventory, tasks)
|
||||
- aw-server/* (server installer, RU patch loader, host groups, default settings)
|
||||
- server-configs-192.168.100.21/* (working Windows Phase2 config snapshots)
|
||||
- server-configs-192.168.100.21/* (working Windows/RDP config snapshots)
|
||||
|
||||
Source:
|
||||
- Local project snapshot at build time.
|
||||
|
||||
@@ -1,82 +1,102 @@
|
||||
# Ansible ensemble for AWatch-rus
|
||||
|
||||
Эта директория содержит Ansible-ensemble для двух сценариев:
|
||||
Эта директория содержит Ansible-ensemble для полного развёртывания AWatch-rus:
|
||||
|
||||
- деплой на уже существующий Debian host/CT;
|
||||
- полный цикл с нуля в Proxmox: создание CT + bootstrap + установка ActivityWatch + RU patch.
|
||||
- централизованный деплой Windows phase-2 collectors по WinRM.
|
||||
- deployment внешнего pfSense poller'а на Debian/Ubuntu utility VM.
|
||||
- полный цикл с нуля в Proxmox: создание CT + bootstrap + установка ActivityWatch + RU patch;
|
||||
- централизованное развёртывание Windows/RDP collector'ов по WinRM;
|
||||
- развёртывание внешнего pfSense poller'а на Debian/Ubuntu utility VM.
|
||||
|
||||
## Файлы
|
||||
|
||||
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_server.yml` — основной playbook.
|
||||
- `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_and_deploy_aw.yml` — full-stack playbook для Proxmox.
|
||||
- `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml` — массовый full-stack playbook (несколько CT).
|
||||
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_windows_phase2.yml` — WinRM playbook для развёртывания phase-2 Windows collector'ов.
|
||||
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_pfsense_poller.yml` — deployment pfSense poller'а.
|
||||
- `/home/igor/tmp/AWatch-rus/ansible/inventory.example.ini` — шаблон inventory.
|
||||
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/all.example.yml` — шаблон переменных.
|
||||
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox.example.yml` — шаблон переменных CT в Proxmox.
|
||||
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox-matrix.example.yml` — шаблон матрицы CT.
|
||||
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/windows.example.yml` — шаблон переменных Windows phase-2.
|
||||
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.example.yml` — шаблон переменных pfSense poller'а.
|
||||
- `ansible/deploy_aw_server.yml` — основной playbook для уже существующего Debian/CT host.
|
||||
- `ansible/provision_proxmox_ct_and_deploy_aw.yml` — полный playbook для Proxmox.
|
||||
- `ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml` — массовый полный playbook (несколько CT).
|
||||
- `ansible/deploy_aw_windows.yml` — WinRM playbook для развёртывания Windows/RDP collector'ов.
|
||||
- `ansible/deploy_aw_pfsense_poller.yml` — развёртывание pfSense poller'а.
|
||||
- `ansible/install_full_stack.yml` — полный установочный playbook (оркестратор всех этапов).
|
||||
- `ansible/inventory.example.ini` — шаблон inventory.
|
||||
- `ansible/group_vars/*.example.yml` — шаблоны переменных.
|
||||
|
||||
## Быстрый запуск
|
||||
|
||||
1. Скопируйте шаблоны:
|
||||
- `cp /home/igor/tmp/AWatch-rus/ansible/inventory.example.ini /home/igor/tmp/AWatch-rus/ansible/inventory.ini`
|
||||
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/all.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/all.yml`
|
||||
- `cp ansible/inventory.example.ini ansible/inventory.ini`
|
||||
- `cp ansible/group_vars/all.example.yml ansible/group_vars/all.yml`
|
||||
2. Заполните значения в `inventory.ini` и `group_vars/all.yml`.
|
||||
3. Запустите:
|
||||
|
||||
```bash
|
||||
cd /home/igor/tmp/AWatch-rus/ansible
|
||||
cd ansible
|
||||
ansible-playbook -i inventory.ini deploy_aw_server.yml
|
||||
```
|
||||
|
||||
## Полный установочный playbook (всё за один запуск)
|
||||
|
||||
Если нужно прогнать полный цикл одной командой:
|
||||
|
||||
```bash
|
||||
cd ansible
|
||||
ansible-playbook -i inventory.ini install_full_stack.yml
|
||||
```
|
||||
|
||||
Что делает:
|
||||
|
||||
- `provision_proxmox_ct_and_deploy_aw.yml` (если есть хосты в группе `[proxmox]`);
|
||||
- `deploy_aw_server.yml` (группа `[aw_server]`);
|
||||
- `deploy_aw_windows.yml` (группа `[aw_windows]`);
|
||||
- `deploy_aw_pfsense_poller.yml` (группа `[aw_pfsense_pollers]`).
|
||||
|
||||
Пустые группы в `inventory.ini` безопасны: соответствующий play будет пропущен.
|
||||
|
||||
## Полный запуск с нуля в Proxmox
|
||||
|
||||
1. Подготовьте inventory и vars:
|
||||
- `cp /home/igor/tmp/AWatch-rus/ansible/inventory.example.ini /home/igor/tmp/AWatch-rus/ansible/inventory.ini`
|
||||
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/all.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/all.yml`
|
||||
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox.yml`
|
||||
- `cp ansible/inventory.example.ini ansible/inventory.ini`
|
||||
- `cp ansible/group_vars/all.example.yml ansible/group_vars/all.yml`
|
||||
- `cp ansible/group_vars/proxmox.example.yml ansible/group_vars/proxmox.yml`
|
||||
2. Заполните `group_vars/proxmox.yml` и `group_vars/all.yml`.
|
||||
3. Запустите playbook:
|
||||
|
||||
```bash
|
||||
cd /home/igor/tmp/AWatch-rus/ansible
|
||||
cd ansible
|
||||
ansible-playbook -i inventory.ini provision_proxmox_ct_and_deploy_aw.yml
|
||||
```
|
||||
|
||||
## Массовый запуск (матрица CT)
|
||||
|
||||
1. Подготовьте матрицу:
|
||||
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox-matrix.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox-matrix.yml`
|
||||
- `cp ansible/group_vars/proxmox-matrix.example.yml ansible/group_vars/proxmox-matrix.yml`
|
||||
2. Заполните `proxmox-matrix.yml`.
|
||||
3. Запустите:
|
||||
|
||||
```bash
|
||||
cd /home/igor/tmp/AWatch-rus/ansible
|
||||
cd ansible
|
||||
ansible-playbook -i inventory.ini provision_proxmox_ct_matrix_and_deploy_aw.yml
|
||||
```
|
||||
|
||||
## Windows phase-2 rollout (WinRM)
|
||||
## Windows/RDP rollout (WinRM)
|
||||
|
||||
1. Подготовьте inventory и vars:
|
||||
- `cp /home/igor/tmp/AWatch-rus/ansible/inventory.example.ini /home/igor/tmp/AWatch-rus/ansible/inventory.ini`
|
||||
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/windows.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/windows.yml`
|
||||
- `cp ansible/inventory.example.ini ansible/inventory.ini`
|
||||
- `cp ansible/group_vars/windows.example.yml ansible/group_vars/windows.yml`
|
||||
2. Заполните `inventory.ini` (секция `[aw_windows]`) и `group_vars/windows.yml`.
|
||||
- Для русской локализации Windows часто нужен `ansible_user=Администратор` (а не `Administrator`).
|
||||
- Если WinRM закрыт, playbook не сможет стартовать и нужно сначала открыть `5985/5986` и `wsman`.
|
||||
3. Запустите:
|
||||
|
||||
```bash
|
||||
cd /home/igor/tmp/AWatch-rus/ansible
|
||||
ansible-playbook -i inventory.ini deploy_aw_windows_phase2.yml
|
||||
cd ansible
|
||||
ansible-playbook -i inventory.ini deploy_aw_windows.yml
|
||||
```
|
||||
|
||||
Playbook:
|
||||
|
||||
- выгружает `windows/*` toolkit на целевой хост в `C:\Deploy\AWatch-rus\windows`;
|
||||
- выполняет `deploy-ensemble.ps1` (deploy + hardening/recovery) с phase-2 policy/rules;
|
||||
- выгружает полный `windows/*` toolkit на целевой хост в InnoSetup-compatible каталог `C:\Program Files\AWatch-rus\windows`, включая DLP и `worktime-session-collector.ps1`;
|
||||
- если найден legacy config `C:\ProgramData\ActivityWatch-Phase2\deployment-config.json`, выполняет безопасную миграцию через `migrate-awatch-rus-paths.ps1`: backup, остановка задач, перенос данных, переписывание путей, пересоздание scheduled tasks и validation;
|
||||
- выполняет `deploy-ensemble.ps1` (deploy + hardening/recovery) с policy/rules из AWatch-rus toolkit;
|
||||
- после deploy принудительно запускает `ActivityWatch Recovery` и все `ActivityWatch Launch *` задачи;
|
||||
- выполняет API smoke-check bucket `aw-watcher-afk_<COMPUTERNAME>` и ожидает свежие `not-afk` события;
|
||||
- запускает `validate-deployment.ps1`;
|
||||
- забирает JSON-отчёт в локальную директорию (`/tmp/aw-rus-validation` по умолчанию).
|
||||
|
||||
@@ -87,17 +107,27 @@ Playbook:
|
||||
- `aw_windows_incident_capture_enabled: false` — отключить блок incidentCapture;
|
||||
- `aw_windows_incident_screenshot_enabled: false` — не делать скриншот при DLP-инциденте;
|
||||
- `aw_windows_incident_artifacts_root: 'C:\...\incident-artifacts'` — переопределить путь артефактов;
|
||||
- `aw_windows_deploy_root: 'C:\Program Files\AWatch-rus'` — каталог toolkit, совпадает с InnoSetup `{app}`;
|
||||
- `aw_windows_install_root: 'C:\Program Files\AWatch-rus\bin'` — каталог бинарников, совпадает с InnoSetup `AwDefaultInstallRoot`;
|
||||
- `aw_windows_state_root: 'C:\ProgramData\AWatch-rus'` — каталог состояния/отчётов, совпадает с InnoSetup `AwDefaultStateRoot`;
|
||||
- `aw_windows_validation_remote_path: '{{ aw_windows_state_root }}\aw_validate_ansible.json'` — отчёт Ansible-валидации хранится рядом с `ensemble-report-*.json`;
|
||||
- `aw_windows_migration_enabled: true` — включить guard миграции текущего production из `ActivityWatch-Phase2` в единый `AWatch-rus`;
|
||||
- `aw_windows_legacy_install_root` / `aw_windows_legacy_state_root` — старые production paths, откуда выполняется перенос;
|
||||
- `aw_windows_migration_report_remote_path` — JSON-отчёт о миграции на Windows-хосте;
|
||||
- `aw_windows_package_version`, `aw_windows_package_url`, `aw_windows_package_zip_path` — версия и источник Windows-пакета ActivityWatch;
|
||||
- `aw_windows_api_smoke_check_bucket: ""` — автоматически использовать `aw-watcher-afk_<COMPUTERNAME>`;
|
||||
- `aw_windows_fail_on_validation_error: true` — завершать playbook ошибкой, если `validate-deployment.ps1` возвращает `overallOk=false`;
|
||||
- `aw_windows_skip_hardening: true` — пропустить `hardening-recovery.ps1` внутри ensemble-скрипта.
|
||||
|
||||
## pfSense poller rollout
|
||||
## Развёртывание pfSense poller
|
||||
|
||||
1. Подготовьте vars:
|
||||
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.yml`
|
||||
- `cp ansible/group_vars/pfsense-poller.example.yml ansible/group_vars/pfsense-poller.yml`
|
||||
2. Добавьте inventory group `[aw_pfsense_pollers]`.
|
||||
3. Запустите:
|
||||
|
||||
```bash
|
||||
cd /home/igor/tmp/AWatch-rus/ansible
|
||||
cd ansible
|
||||
ansible-playbook -i inventory.ini deploy_aw_pfsense_poller.yml
|
||||
```
|
||||
|
||||
@@ -116,4 +146,6 @@ Playbook:
|
||||
- Для Web UI используется checksum-based cache-bust для `ru-patch-v5.js` и `sw-cleanup.js`, чтобы браузер не держал старую DLP/русскую статику после деплоя.
|
||||
- На `#/home` Web UI делит хосты на `Windows RDP` и `Virtual servers + Proxmox`.
|
||||
- Выполнена валидация API `http://127.0.0.1:5600/api/0/info`.
|
||||
- Для full-stack сценария CT создаётся автоматически через `pct create`.
|
||||
- Для полного сценария CT создаётся автоматически через `pct create`.
|
||||
- На Windows/RDP host развёрнуты AFK/window watchers, browser domain collector, DLP endpoint collector и worktime session collector.
|
||||
- Проверочный JSON-отчёт Windows playbook должен иметь `overallOk=true`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
- name: Deploy pfSense ActivityWatch poller
|
||||
- name: Развернуть pfSense ActivityWatch poller
|
||||
hosts: aw_pfsense_pollers
|
||||
become: true
|
||||
gather_facts: true
|
||||
@@ -10,14 +10,14 @@
|
||||
aw_pfsense_service_name: "aw-pfsense-poller.service"
|
||||
|
||||
tasks:
|
||||
- name: Install required packages
|
||||
- name: Установить обязательные пакеты
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- python3
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Ensure directories exist
|
||||
- name: Создать каталоги
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
@@ -26,29 +26,29 @@
|
||||
- "{{ aw_pfsense_install_root }}"
|
||||
- "{{ aw_pfsense_config_dir }}"
|
||||
|
||||
- name: Install pfSense poller script
|
||||
- name: Установить скрипт pfSense poller
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/pfsense/pfsense-aw-poller.py"
|
||||
dest: "{{ aw_pfsense_install_root }}/pfsense-aw-poller.py"
|
||||
mode: "0755"
|
||||
|
||||
- name: Install systemd service
|
||||
- name: Установить systemd service
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/pfsense/pfsense-aw-poller.service"
|
||||
dest: "/etc/systemd/system/{{ aw_pfsense_service_name }}"
|
||||
mode: "0644"
|
||||
notify:
|
||||
- Reload systemd
|
||||
- Перезагрузить systemd
|
||||
|
||||
- name: Write pfSense poller config
|
||||
- name: Записать конфигурацию pfSense poller
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ aw_pfsense_config_dir }}/poller.json"
|
||||
mode: "0600"
|
||||
content: "{{ aw_pfsense_poller_config | to_nice_json }}"
|
||||
notify:
|
||||
- Restart pfSense poller
|
||||
- Перезапустить pfSense poller
|
||||
|
||||
- name: Enable and start pfSense poller
|
||||
- name: Включить и запустить pfSense poller
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ aw_pfsense_service_name }}"
|
||||
enabled: true
|
||||
@@ -56,11 +56,11 @@
|
||||
daemon_reload: true
|
||||
|
||||
handlers:
|
||||
- name: Reload systemd
|
||||
- name: Перезагрузить systemd
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Restart pfSense poller
|
||||
- name: Перезапустить pfSense poller
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ aw_pfsense_service_name }}"
|
||||
state: restarted
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
- name: Deploy AWatch-rus server
|
||||
- name: Развернуть сервер AWatch-rus
|
||||
hosts: aw_server
|
||||
become: true
|
||||
gather_facts: true
|
||||
@@ -9,22 +9,29 @@
|
||||
aw_release_dir: "{{ aw_release_root }}/{{ aw_server_version }}"
|
||||
aw_archive_path: "/tmp/activitywatch-{{ aw_server_version }}.zip"
|
||||
aw_bootstrap_dir: "/tmp/aw-rus-bootstrap"
|
||||
aw_release_install_dir: "{{ aw_release_root }}/aw-server-rust-{{ aw_server_version }}"
|
||||
aw_ru_patch_cache_bust: "{{ lookup('file', aw_repo_root + '/aw-server/aw-ru-patch.js') | hash('sha1') | truncate(12, true, '') }}"
|
||||
aw_sw_cleanup_cache_bust: "{{ lookup('file', aw_repo_root + '/aw-server/aw-sw-cleanup.js') | hash('sha1') | truncate(12, true, '') }}"
|
||||
aw_host_groups_cache_bust: "{{ lookup('file', aw_repo_root + '/aw-server/aw-host-groups.json') | hash('sha1') | truncate(12, true, '') }}"
|
||||
aw_worktime_classes: "{{ lookup('file', aw_repo_root + '/aw-server/settings/classes-worktime.json') | from_json }}"
|
||||
aw_default_views: "{{ lookup('file', aw_repo_root + '/aw-server/settings/views-default.json') | from_json }}"
|
||||
|
||||
tasks:
|
||||
- name: Install base packages
|
||||
- name: Установить базовые пакеты
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- curl
|
||||
- rsync
|
||||
- unzip
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Ensure service account exists
|
||||
- name: Создать системную группу сервиса
|
||||
ansible.builtin.group:
|
||||
name: "{{ aw_server_group }}"
|
||||
system: true
|
||||
state: present
|
||||
|
||||
- name: Создать системную учётную запись сервиса
|
||||
ansible.builtin.user:
|
||||
name: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
@@ -33,7 +40,25 @@
|
||||
system: true
|
||||
create_home: false
|
||||
|
||||
- name: Ensure required directories
|
||||
- name: Создать обязательные каталоги
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ aw_release_root }}"
|
||||
- "{{ aw_release_dir }}"
|
||||
- "{{ aw_release_install_dir }}"
|
||||
- /opt/activitywatch
|
||||
- /opt/activitywatch/bin
|
||||
- "{{ aw_server_webui_dir }}"
|
||||
- "{{ aw_server_webui_dir }}/js"
|
||||
- "{{ aw_server_data_dir }}"
|
||||
- "{{ aw_server_log_dir }}"
|
||||
- /etc/activitywatch
|
||||
- "{{ aw_bootstrap_dir }}"
|
||||
|
||||
- name: Настроить каталоги ActivityWatch с владельцем сервиса
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
@@ -41,103 +66,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: '<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*></script>'
|
||||
replace: ''
|
||||
|
||||
- name: Добавить cleanup script RU patch в index.html
|
||||
ansible.builtin.replace:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
regexp: '</head>'
|
||||
replace: '<script src="/js/sw-cleanup.js?v={{ aw_sw_cleanup_cache_bust }}"></script></head>'
|
||||
|
||||
- name: Insert RU patch loader before body end
|
||||
- name: Добавить загрузчик RU patch перед закрытием body
|
||||
ansible.builtin.replace:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
regexp: '</body>'
|
||||
replace: '<script defer="defer" src="/js/ru-patch-v5.js?v={{ aw_ru_patch_cache_bust }}"></script></body>'
|
||||
|
||||
- name: Write /etc/activitywatch/aw-server.env
|
||||
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
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
---
|
||||
- name: Развернуть Windows/RDP collector'ы AWatch-rus
|
||||
hosts: aw_windows
|
||||
gather_facts: false
|
||||
|
||||
vars:
|
||||
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
|
||||
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
|
||||
aw_windows_server_scheme: "http"
|
||||
aw_windows_server_host: "10.10.10.13"
|
||||
aw_windows_server_port: 5600
|
||||
aw_windows_package_version: "v0.13.2"
|
||||
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
|
||||
aw_windows_package_zip_path: ""
|
||||
aw_windows_domain: "SHARKON2025"
|
||||
aw_windows_users:
|
||||
- user1
|
||||
- user2
|
||||
- user3
|
||||
- user4
|
||||
- user5
|
||||
aw_windows_extra_users: []
|
||||
aw_windows_users_effective: "{{ (aw_windows_users + aw_windows_extra_users) | unique }}"
|
||||
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
|
||||
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
||||
aw_windows_afk_enabled: true
|
||||
aw_windows_window_enabled: true
|
||||
aw_windows_local_agent_logs_enabled: false
|
||||
aw_windows_incident_capture_enabled: true
|
||||
aw_windows_incident_screenshot_enabled: true
|
||||
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
|
||||
aw_windows_logon_marker_enabled: true
|
||||
aw_windows_skip_hardening: false
|
||||
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
|
||||
aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
|
||||
aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ansible.json"
|
||||
aw_windows_validation_local_dir: "/tmp/aw-rus-validation"
|
||||
aw_windows_launch_task_pattern: "ActivityWatch Launch *"
|
||||
aw_windows_recovery_task_name: "ActivityWatch Recovery"
|
||||
aw_windows_force_task_restart: true
|
||||
aw_windows_api_smoke_check_enabled: true
|
||||
aw_windows_api_smoke_check_bucket: ""
|
||||
aw_windows_api_smoke_check_limit: 10
|
||||
aw_windows_fail_on_validation_error: true
|
||||
aw_windows_migration_enabled: true
|
||||
aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
|
||||
aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2"
|
||||
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
|
||||
|
||||
tasks:
|
||||
- name: Проверить обязательные переменные
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- aw_windows_server_host is defined
|
||||
- aw_windows_server_port is defined
|
||||
- aw_windows_server_scheme is defined
|
||||
- aw_windows_domain is defined
|
||||
- aw_windows_users_effective | length > 0
|
||||
- aw_windows_install_root is defined
|
||||
- aw_windows_state_root is defined
|
||||
fail_msg: "Не заданы обязательные переменные Windows-развёртывания."
|
||||
|
||||
- name: Создать каталоги развёртывания
|
||||
ansible.windows.win_file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
loop:
|
||||
- "{{ aw_windows_deploy_root }}"
|
||||
- "{{ aw_windows_deploy_root }}\\windows"
|
||||
|
||||
- name: Загрузить Windows toolkit развёртывания
|
||||
ansible.windows.win_copy:
|
||||
src: "{{ aw_windows_repo_root }}/windows/{{ item }}"
|
||||
dest: "{{ aw_windows_deploy_root }}\\windows\\{{ item }}"
|
||||
loop:
|
||||
- ActivityWatch.Windows.Common.psd1
|
||||
- ActivityWatch.Windows.Common.psm1
|
||||
- browser-domains-native-collector.ps1
|
||||
- dlp-endpoint-signals-collector.ps1
|
||||
- worktime-session-collector.ps1
|
||||
- migrate-awatch-rus-paths.ps1
|
||||
- deploy-domain-users.ps1
|
||||
- deploy-ensemble.ps1
|
||||
- hardening-recovery.ps1
|
||||
- validate-deployment.ps1
|
||||
- web-category-rules.example.json
|
||||
- dlp-policy.example.json
|
||||
|
||||
- name: Загрузить список пользователей для доменного развёртывания
|
||||
ansible.windows.win_copy:
|
||||
dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
|
||||
content: |
|
||||
{% for user in aw_windows_users_effective -%}
|
||||
{{ user }}
|
||||
{% endfor -%}
|
||||
|
||||
- name: Проверить нужен ли migration с legacy ActivityWatch путей
|
||||
when: aw_windows_migration_enabled | bool
|
||||
ansible.windows.win_stat:
|
||||
path: "{{ aw_windows_legacy_state_root }}\\deployment-config.json"
|
||||
register: aw_windows_legacy_config
|
||||
|
||||
- name: Выполнить безопасную migration legacy prod в AWatch-rus
|
||||
when:
|
||||
- aw_windows_migration_enabled | bool
|
||||
- aw_windows_legacy_config.stat.exists | default(false)
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$result = & "{{ aw_windows_deploy_root }}\windows\migrate-awatch-rus-paths.ps1" `
|
||||
-OldInstallRoot "{{ aw_windows_legacy_install_root }}" `
|
||||
-OldStateRoot "{{ aw_windows_legacy_state_root }}" `
|
||||
-NewInstallRoot "{{ aw_windows_install_root }}" `
|
||||
-NewStateRoot "{{ aw_windows_state_root }}" `
|
||||
-ToolkitRoot "{{ aw_windows_deploy_root }}\windows"
|
||||
$result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_migration_report_remote_path }}" -Encoding utf8
|
||||
|
||||
- name: Запустить Windows/RDP ensemble развёртывание
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$params = @{
|
||||
ServerScheme = "{{ aw_windows_server_scheme }}"
|
||||
ServerHost = "{{ aw_windows_server_host }}"
|
||||
ServerPort = {{ aw_windows_server_port }}
|
||||
Version = "{{ aw_windows_package_version }}"
|
||||
Domain = "{{ aw_windows_domain }}"
|
||||
UserListPath = "{{ aw_windows_deploy_root }}\windows\users.txt"
|
||||
InstallRoot = "{{ aw_windows_install_root }}"
|
||||
StateRoot = "{{ aw_windows_state_root }}"
|
||||
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
|
||||
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
|
||||
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
|
||||
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
|
||||
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
|
||||
IncidentArtifactsRoot = "{{ aw_windows_incident_artifacts_root }}"
|
||||
LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
|
||||
CustomRulesPath = "{{ aw_windows_rules_path }}"
|
||||
CustomPolicyPath = "{{ aw_windows_policy_path }}"
|
||||
}
|
||||
{% if (aw_windows_package_url | default('') | string | length) > 0 %}
|
||||
$params.PackageUrl = "{{ aw_windows_package_url }}"
|
||||
{% endif %}
|
||||
{% if (aw_windows_package_zip_path | default('') | string | length) > 0 %}
|
||||
$params.PackageZipPath = "{{ aw_windows_package_zip_path }}"
|
||||
{% endif %}
|
||||
{% if aw_windows_skip_hardening | bool %}
|
||||
$params.SkipHardening = $true
|
||||
{% endif %}
|
||||
& "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params
|
||||
|
||||
- name: Принудительно запустить ActivityWatch recovery и launch tasks
|
||||
when: aw_windows_force_task_restart | bool
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Start-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}"
|
||||
Get-ScheduledTask |
|
||||
Where-Object TaskName -like "{{ aw_windows_launch_task_pattern }}" |
|
||||
ForEach-Object { Start-ScheduledTask -TaskName $_.TaskName }
|
||||
|
||||
- name: Получить Windows hostname для AW smoke-check bucket
|
||||
when:
|
||||
- aw_windows_api_smoke_check_enabled | bool
|
||||
- aw_windows_afk_enabled | bool
|
||||
ansible.windows.win_command: powershell.exe -NoProfile -Command "$env:COMPUTERNAME"
|
||||
register: aw_windows_hostname_result
|
||||
changed_when: false
|
||||
|
||||
- name: Вычислить AW AFK smoke-check bucket
|
||||
when:
|
||||
- aw_windows_api_smoke_check_enabled | bool
|
||||
- aw_windows_afk_enabled | bool
|
||||
ansible.builtin.set_fact:
|
||||
aw_windows_api_smoke_check_bucket_effective: >-
|
||||
{{
|
||||
aw_windows_api_smoke_check_bucket
|
||||
if (aw_windows_api_smoke_check_bucket | default('') | string | length) > 0
|
||||
else 'aw-watcher-afk_' ~ (aw_windows_hostname_result.stdout | trim)
|
||||
}}
|
||||
|
||||
- name: Дождаться свежих AFK событий на AW server
|
||||
when:
|
||||
- aw_windows_api_smoke_check_enabled | bool
|
||||
- aw_windows_afk_enabled | bool
|
||||
delegate_to: localhost
|
||||
ansible.builtin.uri:
|
||||
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
|
||||
method: GET
|
||||
return_content: true
|
||||
register: aw_windows_api_smoke
|
||||
until: >
|
||||
aw_windows_api_smoke.status == 200 and
|
||||
(aw_windows_api_smoke.json | length) > 0 and
|
||||
(
|
||||
aw_windows_api_smoke.json
|
||||
| selectattr('data.status', 'equalto', 'not-afk')
|
||||
| list
|
||||
| length
|
||||
) > 0
|
||||
retries: 10
|
||||
delay: 6
|
||||
|
||||
- name: Выполнить валидацию и сохранить отчёт на целевом Windows host
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
|
||||
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
|
||||
$report | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
|
||||
if ({{ '$true' if (aw_windows_fail_on_validation_error | bool) else '$false' }} -and -not [bool]$report.overallOk) {
|
||||
throw "Проверка развёртывания ActivityWatch завершилась ошибкой. Отчёт: {{ aw_windows_validation_remote_path }}"
|
||||
}
|
||||
|
||||
- name: Создать локальный каталог для validation reports
|
||||
ansible.builtin.file:
|
||||
path: "{{ aw_windows_validation_local_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Забрать validation report
|
||||
ansible.builtin.fetch:
|
||||
src: "{{ aw_windows_validation_remote_path }}"
|
||||
dest: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
|
||||
flat: true
|
||||
|
||||
- name: Показать путь к отчёту
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Windows/RDP развёртывание завершено на {{ inventory_hostname }}."
|
||||
- "Отчёт проверки: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
|
||||
@@ -1,133 +0,0 @@
|
||||
---
|
||||
- name: Deploy AWatch-rus Windows phase2 collectors
|
||||
hosts: aw_windows
|
||||
gather_facts: false
|
||||
|
||||
vars:
|
||||
aw_windows_repo_root: "/home/igor/tmp/AWatch-rus"
|
||||
aw_windows_deploy_root: "C:\\Deploy\\AWatch-rus"
|
||||
aw_windows_server_host: "10.10.10.13"
|
||||
aw_windows_server_port: 5600
|
||||
aw_windows_domain: "SHARKON2025"
|
||||
aw_windows_users:
|
||||
- user1
|
||||
- user2
|
||||
- user3
|
||||
- user4
|
||||
- user5
|
||||
aw_windows_extra_users: []
|
||||
aw_windows_users_effective: "{{ (aw_windows_users + aw_windows_extra_users) | unique }}"
|
||||
aw_windows_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
|
||||
aw_windows_state_root: "C:\\ProgramData\\ActivityWatch"
|
||||
aw_windows_afk_enabled: true
|
||||
aw_windows_window_enabled: true
|
||||
aw_windows_local_agent_logs_enabled: false
|
||||
aw_windows_incident_capture_enabled: true
|
||||
aw_windows_incident_screenshot_enabled: true
|
||||
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
|
||||
aw_windows_logon_marker_enabled: true
|
||||
aw_windows_skip_hardening: false
|
||||
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
|
||||
aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
|
||||
aw_windows_validation_remote_path: "C:\\Windows\\Temp\\aw_validate_phase2_ansible.json"
|
||||
aw_windows_validation_local_dir: "/tmp/aw-rus-validation"
|
||||
|
||||
tasks:
|
||||
- name: Validate required variables
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- aw_windows_server_host is defined
|
||||
- aw_windows_server_port is defined
|
||||
- aw_windows_domain is defined
|
||||
- aw_windows_users_effective | length > 0
|
||||
- aw_windows_install_root is defined
|
||||
- aw_windows_state_root is defined
|
||||
fail_msg: "Missing required Windows deployment variables."
|
||||
|
||||
- name: Ensure deploy directories exist
|
||||
ansible.windows.win_file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
loop:
|
||||
- "{{ aw_windows_deploy_root }}"
|
||||
- "{{ aw_windows_deploy_root }}\\windows"
|
||||
|
||||
- name: Upload Windows deployment toolkit
|
||||
ansible.windows.win_copy:
|
||||
src: "{{ aw_windows_repo_root }}/windows/{{ item }}"
|
||||
dest: "{{ aw_windows_deploy_root }}\\windows\\{{ item }}"
|
||||
loop:
|
||||
- ActivityWatch.Windows.Common.psd1
|
||||
- ActivityWatch.Windows.Common.psm1
|
||||
- browser-domains-native-collector.ps1
|
||||
- dlp-endpoint-signals-collector.ps1
|
||||
- deploy-domain-users.ps1
|
||||
- deploy-ensemble.ps1
|
||||
- hardening-recovery.ps1
|
||||
- validate-deployment.ps1
|
||||
- web-category-rules.example.json
|
||||
- dlp-policy.example.json
|
||||
|
||||
- name: Upload user list for domain deploy
|
||||
ansible.windows.win_copy:
|
||||
dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
|
||||
content: |
|
||||
{% for user in aw_windows_users -%}
|
||||
{{ user }}
|
||||
{% endfor -%}
|
||||
{% for user in aw_windows_extra_users -%}
|
||||
{{ user }}
|
||||
{% endfor -%}
|
||||
|
||||
- name: Run phase2 ensemble deployment
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$params = @{
|
||||
ServerHost = "{{ aw_windows_server_host }}"
|
||||
ServerPort = {{ aw_windows_server_port }}
|
||||
Domain = "{{ aw_windows_domain }}"
|
||||
UserListPath = "{{ aw_windows_deploy_root }}\windows\users.txt"
|
||||
InstallRoot = "{{ aw_windows_install_root }}"
|
||||
StateRoot = "{{ aw_windows_state_root }}"
|
||||
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
|
||||
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
|
||||
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
|
||||
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
|
||||
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
|
||||
IncidentArtifactsRoot = "{{ aw_windows_incident_artifacts_root }}"
|
||||
LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
|
||||
CustomRulesPath = "{{ aw_windows_rules_path }}"
|
||||
CustomPolicyPath = "{{ aw_windows_policy_path }}"
|
||||
}
|
||||
{% if aw_windows_skip_hardening | bool %}
|
||||
$params.SkipHardening = $true
|
||||
{% endif %}
|
||||
& "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params
|
||||
|
||||
- name: Run validation and store report on target
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
|
||||
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
|
||||
$report | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
|
||||
|
||||
- name: Ensure local validation directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ aw_windows_validation_local_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Fetch validation report
|
||||
ansible.builtin.fetch:
|
||||
src: "{{ aw_windows_validation_remote_path }}"
|
||||
dest: "{{ aw_windows_validation_local_dir }}/"
|
||||
flat: false
|
||||
|
||||
- name: Show report location
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Windows phase2 deploy completed on {{ inventory_hostname }}."
|
||||
- "Validation report: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}/C$/Windows/Temp/aw_validate_phase2_ansible.json"
|
||||
@@ -8,17 +8,17 @@ aw_server_log_dir: "/var/log/activitywatch"
|
||||
aw_server_user: "activitywatch"
|
||||
aw_server_group: "activitywatch"
|
||||
|
||||
aw_repo_root: "/home/igor/tmp/AWatch-rus"
|
||||
aw_repo_root: "{{ playbook_dir | dirname }}"
|
||||
|
||||
# Optional: apply a baseline worktime-focused categorization and views via AW settings API.
|
||||
# WARNING: this overwrites existing server-side settings/classes/views.
|
||||
# Опционально: применить базовые категории и views для рабочего времени через AW settings API.
|
||||
# Внимание: это перезаписывает существующие server-side settings/classes/views.
|
||||
aw_apply_worktime_settings: false
|
||||
|
||||
# Optional defaults for the worktime period in Web UI.
|
||||
# startOfDay controls day-boundary and default report window start.
|
||||
# durationDefault controls default time range (seconds) shown in UI.
|
||||
# Опциональные значения периода рабочего времени в Web UI.
|
||||
# startOfDay задаёт границу дня и стартовое время окна отчёта.
|
||||
# durationDefault задаёт диапазон по умолчанию в секундах.
|
||||
#
|
||||
# Recommended: set worktime window explicitly and let the playbook derive duration.
|
||||
# Рекомендуется явно задать рабочий интервал и дать playbook вычислить duration.
|
||||
aw_worktime_from: "08:00"
|
||||
aw_worktime_to: "17:00"
|
||||
aw_worktime_start_of_day: "{{ aw_worktime_from }}"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
aw_windows_repo_root: "/home/igor/tmp/AWatch-rus"
|
||||
aw_windows_deploy_root: "C:\\Deploy\\AWatch-rus"
|
||||
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
|
||||
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
|
||||
aw_windows_server_scheme: "http"
|
||||
aw_windows_server_host: "10.10.10.13"
|
||||
aw_windows_server_port: 5600
|
||||
aw_windows_package_version: "v0.13.2"
|
||||
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
|
||||
aw_windows_package_zip_path: ""
|
||||
aw_windows_domain: "SHARKON2025"
|
||||
aw_windows_users:
|
||||
- user1
|
||||
@@ -14,9 +18,9 @@ aw_windows_extra_users: []
|
||||
# aw_windows_extra_users:
|
||||
# - Администратор
|
||||
|
||||
# Рекомендуемый изолированный профиль для фазового раската.
|
||||
aw_windows_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
|
||||
aw_windows_state_root: "C:\\ProgramData\\ActivityWatch"
|
||||
# Единые Windows/RDP пути: те же, что использует InnoSetup.
|
||||
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
|
||||
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
||||
aw_windows_afk_enabled: true
|
||||
aw_windows_window_enabled: true
|
||||
aw_windows_local_agent_logs_enabled: false
|
||||
@@ -29,5 +33,18 @@ aw_windows_skip_hardening: false
|
||||
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
|
||||
aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
|
||||
|
||||
aw_windows_validation_remote_path: "C:\\Windows\\Temp\\aw_validate_phase2_ansible.json"
|
||||
aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ansible.json"
|
||||
aw_windows_validation_local_dir: "/tmp/aw-rus-validation"
|
||||
aw_windows_fail_on_validation_error: true
|
||||
|
||||
# Безопасная миграция текущего прода со старых путей в единый профиль AWatch-rus.
|
||||
aw_windows_migration_enabled: true
|
||||
aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
|
||||
aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2"
|
||||
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
|
||||
|
||||
# По умолчанию AFK bucket вычисляется как aw-watcher-afk_<COMPUTERNAME>.
|
||||
# Задайте явное значение только если watcher пишет в нестандартный bucket.
|
||||
aw_windows_api_smoke_check_enabled: true
|
||||
aw_windows_api_smoke_check_bucket: ""
|
||||
aw_windows_api_smoke_check_limit: 10
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
# Полный установщик AWatch-rus.
|
||||
# Выполняет развёртывание одной командой:
|
||||
# 1) создание Proxmox CT + bootstrap AW (если в inventory есть [proxmox])
|
||||
# 2) развёртывание AW server на хостах [aw_server]
|
||||
# 3) развёртывание Windows/RDP collector'ов на [aw_windows]
|
||||
# 4) развёртывание pfSense poller'а на [aw_pfsense_pollers]
|
||||
#
|
||||
# Примечания:
|
||||
# - Заполняйте только нужные группы inventory для своего окружения.
|
||||
# - Play без совпадающих host groups Ansible пропускает автоматически.
|
||||
|
||||
- import_playbook: provision_proxmox_ct_and_deploy_aw.yml
|
||||
- import_playbook: deploy_aw_server.yml
|
||||
- import_playbook: deploy_aw_windows.yml
|
||||
- import_playbook: deploy_aw_pfsense_poller.yml
|
||||
@@ -5,4 +5,8 @@ pve-main ansible_host=192.168.10.2 ansible_user=root ansible_port=22
|
||||
aw-ct ansible_host=10.20.30.13 ansible_user=root ansible_port=22
|
||||
|
||||
[aw_windows]
|
||||
win-node1 ansible_host=192.168.100.21 ansible_user=Administrator ansible_password=CHANGE_ME ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
|
||||
# Примечание: в русифицированных Windows часто нужен "Администратор", а не "Administrator".
|
||||
win-node1 ansible_host=192.168.100.21 ansible_user=Администратор ansible_password=CHANGE_ME ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
|
||||
|
||||
[aw_pfsense_pollers]
|
||||
# pfsense-poller1 ansible_host=192.168.100.30 ansible_user=root ansible_port=22
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
- name: Provision single Proxmox CT and deploy AWatch-rus
|
||||
- name: Создать один Proxmox CT и развернуть AWatch-rus
|
||||
hosts: proxmox
|
||||
gather_facts: false
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
- settings/views-default.json
|
||||
|
||||
tasks:
|
||||
- name: Execute single-CT provisioning workflow
|
||||
- name: Выполнить workflow создания одного CT
|
||||
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
|
||||
vars:
|
||||
ct_id: "{{ proxmox_ct_id }}"
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
---
|
||||
- name: Provision Proxmox CT matrix and deploy AWatch-rus with RU patch
|
||||
- name: Создать матрицу Proxmox CT и развернуть AWatch-rus с RU patch
|
||||
hosts: proxmox
|
||||
gather_facts: false
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
- settings/views-default.json
|
||||
|
||||
tasks:
|
||||
- name: Validate CT matrix is provided
|
||||
- name: Проверить, что матрица CT задана
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- proxmox_ct_matrix is defined
|
||||
- proxmox_ct_matrix | length > 0
|
||||
fail_msg: "Define proxmox_ct_matrix in group_vars/proxmox-matrix.yml"
|
||||
fail_msg: "Задайте proxmox_ct_matrix в group_vars/proxmox-matrix.yml"
|
||||
|
||||
- name: Execute provisioning workflow for each CT
|
||||
- name: Выполнить workflow создания для каждого CT
|
||||
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
|
||||
vars:
|
||||
ct_id: "{{ item.id }}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
- name: Validate required per-CT variables
|
||||
- name: Проверить обязательные переменные CT
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ct_id is defined
|
||||
@@ -27,14 +27,14 @@
|
||||
- aw_server_log_dir is defined
|
||||
- aw_server_user is defined
|
||||
- aw_server_group is defined
|
||||
fail_msg: "Missing required variables for CT provisioning/deploy."
|
||||
fail_msg: "Не заданы обязательные переменные для создания CT и развёртывания."
|
||||
|
||||
- name: Build CT network string
|
||||
- name: Сформировать сетевую строку CT
|
||||
ansible.builtin.set_fact:
|
||||
ct_net0: >-
|
||||
name=eth0,bridge={{ ct_bridge }},ip={{ ct_ip }},gw={{ ct_gw }}{% if (ct_vlan | default('') | string | length) > 0 %},tag={{ ct_vlan }}{% endif %}
|
||||
|
||||
- name: Check whether CT already exists
|
||||
- name: Проверить, существует ли CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -44,7 +44,7 @@
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Create CT when absent
|
||||
- name: Создать CT, если он отсутствует
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -78,8 +78,9 @@
|
||||
- --ostype
|
||||
- debian
|
||||
when: ct_status_check.rc != 0
|
||||
no_log: true
|
||||
|
||||
- name: Check current CT runtime state
|
||||
- name: Проверить текущее состояние CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -88,7 +89,7 @@
|
||||
register: ct_runtime_status
|
||||
changed_when: false
|
||||
|
||||
- name: Start CT when stopped
|
||||
- name: Запустить CT, если он остановлен
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -96,20 +97,23 @@
|
||||
- "{{ ct_id }}"
|
||||
when: "'stopped' in ct_runtime_status.stdout"
|
||||
|
||||
- name: Ensure bootstrap directory on Proxmox host
|
||||
- name: Создать bootstrap каталог на Proxmox host
|
||||
ansible.builtin.file:
|
||||
path: "{{ proxmox_bootstrap_dir }}"
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
mode: "0700"
|
||||
loop:
|
||||
- "{{ proxmox_bootstrap_dir }}"
|
||||
- "{{ proxmox_bootstrap_dir }}/settings"
|
||||
|
||||
- name: Copy AW bootstrap files to Proxmox host temp
|
||||
- name: Скопировать AW bootstrap файлы во временный каталог Proxmox host
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/{{ item }}"
|
||||
dest: "{{ proxmox_bootstrap_dir }}/{{ item }}"
|
||||
mode: "0644"
|
||||
loop: "{{ aw_bootstrap_files }}"
|
||||
|
||||
- name: Bootstrap CT OS dependencies
|
||||
- name: Установить базовые зависимости ОС внутри CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -122,12 +126,16 @@
|
||||
set -euo pipefail
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y curl ca-certificates bash unzip xz-utils jq rsync openssh-server
|
||||
mkdir -p /root/bootstrap /etc/activitywatch
|
||||
apt-get install -y curl ca-certificates bash unzip xz-utils jq rsync openssh-server python3
|
||||
mkdir -p /root/bootstrap/settings /etc/activitywatch
|
||||
systemctl enable ssh || true
|
||||
systemctl restart ssh || true
|
||||
register: ct_bootstrap_result
|
||||
retries: 10
|
||||
delay: 6
|
||||
until: ct_bootstrap_result.rc == 0
|
||||
|
||||
- name: Push bootstrap files into CT
|
||||
- name: Передать bootstrap файлы внутрь CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -137,7 +145,7 @@
|
||||
- "/root/bootstrap/{{ item }}"
|
||||
loop: "{{ aw_bootstrap_files }}"
|
||||
|
||||
- name: Write AW server env file on Proxmox host temp
|
||||
- name: Записать AW server env во временный каталог Proxmox host
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ proxmox_bootstrap_dir }}/aw-server.env"
|
||||
mode: "0600"
|
||||
@@ -151,8 +159,9 @@
|
||||
AW_SERVER_LOG_DIR={{ aw_server_log_dir }}
|
||||
AW_SERVER_USER={{ aw_server_user }}
|
||||
AW_SERVER_GROUP={{ aw_server_group }}
|
||||
no_log: true
|
||||
|
||||
- name: Push AW server env into CT
|
||||
- name: Передать AW server env внутрь CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -160,8 +169,9 @@
|
||||
- "{{ ct_id }}"
|
||||
- "{{ proxmox_bootstrap_dir }}/aw-server.env"
|
||||
- /etc/activitywatch/aw-server.env
|
||||
no_log: true
|
||||
|
||||
- name: Set mode for env inside CT
|
||||
- name: Настроить права env файла внутри CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -172,7 +182,7 @@
|
||||
- "0600"
|
||||
- /etc/activitywatch/aw-server.env
|
||||
|
||||
- name: Install server and apply RU patch inside CT
|
||||
- name: Установить сервер и применить RU patch внутри CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -188,7 +198,7 @@
|
||||
bash /root/bootstrap/apply_webui_ru_patch.sh
|
||||
systemctl restart activitywatch-server.service
|
||||
|
||||
- name: Validate AW API from inside CT
|
||||
- name: Проверить AW API изнутри CT
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -199,7 +209,7 @@
|
||||
- -lc
|
||||
- "curl -fsS http://127.0.0.1:{{ aw_server_port }}/api/0/info >/dev/null"
|
||||
|
||||
- name: Validate RU patch hooks in index
|
||||
- name: Проверить hooks RU patch в index.html
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
@@ -210,8 +220,8 @@
|
||||
- -lc
|
||||
- "grep -q 'ru-patch-v5.js' {{ aw_server_webui_dir }}/index.html && grep -q 'sw-cleanup.js' {{ aw_server_webui_dir }}/index.html"
|
||||
|
||||
- name: Show final endpoint
|
||||
- name: Показать итоговый endpoint
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "CT {{ ct_id }} is provisioned and configured."
|
||||
- "ActivityWatch endpoint: http://{{ ct_ip | regex_replace('/[0-9]+$', '') }}:{{ aw_server_port }}"
|
||||
- "CT {{ ct_id }} создан и настроен."
|
||||
- "Endpoint ActivityWatch: http://{{ ct_ip | regex_replace('/[0-9]+$', '') }}:{{ aw_server_port }}"
|
||||
|
||||
@@ -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; }
|
||||
|
||||
+10
-10
@@ -7,15 +7,15 @@
|
||||
"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"
|
||||
"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,
|
||||
@@ -31,7 +31,7 @@
|
||||
"incidentCapture": {
|
||||
"enabled": true,
|
||||
"screenshotEnabled": true,
|
||||
"artifactsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\incident-artifacts"
|
||||
"artifactsRoot": "C:\\ProgramData\\AWatch-rus\\incident-artifacts"
|
||||
},
|
||||
"sessionEvents": {
|
||||
"logonEnabled": true,
|
||||
+10
-10
@@ -7,15 +7,15 @@
|
||||
"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"
|
||||
"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,
|
||||
@@ -31,7 +31,7 @@
|
||||
"incidentCapture": {
|
||||
"enabled": true,
|
||||
"screenshotEnabled": true,
|
||||
"artifactsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\incident-artifacts"
|
||||
"artifactsRoot": "C:\\ProgramData\\AWatch-rus\\incident-artifacts"
|
||||
},
|
||||
"sessionEvents": {
|
||||
"logonEnabled": true,
|
||||
+10
-10
@@ -7,15 +7,15 @@
|
||||
"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"
|
||||
"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,
|
||||
@@ -31,7 +31,7 @@
|
||||
"incidentCapture": {
|
||||
"enabled": true,
|
||||
"screenshotEnabled": true,
|
||||
"artifactsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\incident-artifacts"
|
||||
"artifactsRoot": "C:\\ProgramData\\AWatch-rus\\incident-artifacts"
|
||||
},
|
||||
"sessionEvents": {
|
||||
"logonEnabled": true,
|
||||
@@ -5,7 +5,7 @@ function Assert-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
||||
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
throw 'Run this script from an elevated PowerShell session.'
|
||||
throw 'Запустите этот скрипт из PowerShell с правами администратора.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ function Get-ActivityWatchPackageRoot {
|
||||
Select-Object -First 1
|
||||
|
||||
if (-not $afkBinary) {
|
||||
throw "Cannot find aw-watcher-afk.exe under $ExpandedRoot."
|
||||
throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot."
|
||||
}
|
||||
|
||||
return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent)
|
||||
@@ -130,7 +130,7 @@ function Get-ActivityWatchExecutableMap {
|
||||
|
||||
foreach ($entry in $map.GetEnumerator()) {
|
||||
if (-not (Test-Path -LiteralPath $entry.Value)) {
|
||||
throw "Missing required ActivityWatch binary: $($entry.Value)"
|
||||
throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ function Normalize-ActivityWatchUsers {
|
||||
Sort-Object -Unique
|
||||
|
||||
if (-not $normalized -or $normalized.Count -eq 0) {
|
||||
throw 'No target users resolved. Provide -Users or -UserListPath.'
|
||||
throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.'
|
||||
}
|
||||
|
||||
return @($normalized)
|
||||
@@ -243,6 +243,8 @@ function Copy-ActivityWatchCollectorAssets {
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$EndpointCollectorScriptSource,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SessionCollectorScriptSource,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ExampleRulesSource,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ExamplePolicySource,
|
||||
@@ -256,6 +258,7 @@ function Copy-ActivityWatchCollectorAssets {
|
||||
|
||||
$collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1'
|
||||
$endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1'
|
||||
$sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1'
|
||||
$exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json'
|
||||
$rulesTarget = Join-Path $StateRoot 'web-category-rules.json'
|
||||
$examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json'
|
||||
@@ -263,6 +266,7 @@ function Copy-ActivityWatchCollectorAssets {
|
||||
|
||||
Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force
|
||||
Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force
|
||||
Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force
|
||||
Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force
|
||||
Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force
|
||||
|
||||
@@ -282,6 +286,7 @@ function Copy-ActivityWatchCollectorAssets {
|
||||
return [pscustomobject]@{
|
||||
CollectorScript = $collectorTarget
|
||||
EndpointCollectorScript = $endpointCollectorTarget
|
||||
SessionCollectorScript = $sessionCollectorTarget
|
||||
ExampleRules = $exampleRulesTarget
|
||||
ActiveRules = $rulesTarget
|
||||
ExamplePolicy = $examplePolicyTarget
|
||||
@@ -308,6 +313,8 @@ function New-ActivityWatchDeploymentConfig {
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$EndpointCollectorScript,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SessionCollectorScript,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RulesPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PolicyPath,
|
||||
@@ -349,6 +356,7 @@ function New-ActivityWatchDeploymentConfig {
|
||||
logsRoot = $LogsRoot
|
||||
collectorScript = $CollectorScript
|
||||
endpointCollectorScript = $EndpointCollectorScript
|
||||
sessionCollectorScript = $SessionCollectorScript
|
||||
rulesPath = $RulesPath
|
||||
policyPath = $PolicyPath
|
||||
launchScript = $LaunchScriptPath
|
||||
@@ -413,7 +421,7 @@ function Read-ActivityWatchDeploymentConfig {
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "Deployment config not found: $Path"
|
||||
throw "Конфигурация развёртывания не найдена: $Path"
|
||||
}
|
||||
|
||||
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||
@@ -539,7 +547,7 @@ function Send-LogonMarkerIfNeeded {
|
||||
`$stateRoot = [string]`$Config.paths.stateRoot
|
||||
`$markerRoots = New-Object System.Collections.Generic.List[string]
|
||||
if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) {
|
||||
`$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'ActivityWatch-Phase2\markers'))
|
||||
`$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers'))
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) {
|
||||
`$markerRoots.Add((Join-Path `$stateRoot 'markers'))
|
||||
@@ -585,7 +593,7 @@ function Send-LogonMarkerIfNeeded {
|
||||
userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)"
|
||||
sessionId = `$SessionId
|
||||
hostname = `$script:Hostname
|
||||
source = 'launch-watchers-phase2'
|
||||
source = 'launch-watchers-awatch-rus'
|
||||
}
|
||||
} | ConvertTo-Json -Depth 5 -Compress
|
||||
|
||||
@@ -631,6 +639,7 @@ function Start-CollectorScriptIfNeeded {
|
||||
`$script:KnownBuckets = @{}
|
||||
`$collectorScript = [string]`$config.paths.collectorScript
|
||||
`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { '' }
|
||||
`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { '' }
|
||||
`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe'
|
||||
`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe'
|
||||
`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port)
|
||||
@@ -639,11 +648,11 @@ function Start-CollectorScriptIfNeeded {
|
||||
`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true }
|
||||
|
||||
if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) {
|
||||
throw "Missing aw-watcher-afk.exe: `$afkExe"
|
||||
throw "Не найден aw-watcher-afk.exe: `$afkExe"
|
||||
}
|
||||
|
||||
if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) {
|
||||
throw "Missing aw-watcher-window.exe: `$windowExe"
|
||||
throw "Не найден aw-watcher-window.exe: `$windowExe"
|
||||
}
|
||||
|
||||
if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) {
|
||||
@@ -661,6 +670,7 @@ catch {
|
||||
}
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
"@
|
||||
|
||||
Set-Content -LiteralPath $Path -Value $content -Encoding UTF8
|
||||
@@ -850,7 +860,7 @@ function Set-ActivityWatchScheduledTaskAction {
|
||||
$taskCommand = ('"{0}" {1}' -f $Execute, $Arguments)
|
||||
& schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "schtasks.exe /Change failed for $TaskName"
|
||||
throw "schtasks.exe /Change завершился с ошибкой для $TaskName"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -951,17 +961,17 @@ function Set-ActivityWatchAcl {
|
||||
|
||||
& icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "icacls failed for $InstallRoot"
|
||||
throw "icacls завершился с ошибкой для $InstallRoot"
|
||||
}
|
||||
|
||||
& icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "icacls failed for $StateRoot"
|
||||
throw "icacls завершился с ошибкой для $StateRoot"
|
||||
}
|
||||
|
||||
& icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "icacls failed for $LogsRoot"
|
||||
throw "icacls завершился с ошибкой для $LogsRoot"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$ServerHost,
|
||||
[int]$ServerPort,
|
||||
[ValidateSet('http', 'https')]
|
||||
@@ -49,18 +49,18 @@ function Get-DeploymentConfig {
|
||||
}
|
||||
|
||||
$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath
|
||||
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' }
|
||||
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' }
|
||||
$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 }
|
||||
$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' }
|
||||
$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\ActivityWatch\web-category-rules.json' }
|
||||
$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\ActivityWatch\dlp-policy.json' }
|
||||
$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\AWatch-rus\web-category-rules.json' }
|
||||
$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' }
|
||||
$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 }
|
||||
$resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pulseSeconds } else { 30 }
|
||||
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\ActivityWatch\logs' }
|
||||
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' }
|
||||
$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) }
|
||||
$resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) }
|
||||
$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true }
|
||||
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'ActivityWatch-Phase2\\incident-artifacts' }
|
||||
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' }
|
||||
$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true }
|
||||
|
||||
if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) {
|
||||
@@ -158,12 +158,12 @@ function Get-HostFromUrl {
|
||||
|
||||
try {
|
||||
$uri = [Uri]$Url
|
||||
$host = $uri.Host.ToLowerInvariant()
|
||||
if ($host.StartsWith('www.')) {
|
||||
return $host.Substring(4)
|
||||
$uriHost = $uri.Host.ToLowerInvariant()
|
||||
if ($uriHost.StartsWith('www.')) {
|
||||
return $uriHost.Substring(4)
|
||||
}
|
||||
|
||||
return $host
|
||||
return $uriHost
|
||||
}
|
||||
catch {
|
||||
return $null
|
||||
@@ -263,11 +263,11 @@ function Load-CustomCategoryRules {
|
||||
|
||||
if ($rules.Count -gt 0) {
|
||||
$script:CategoryRules = @($rules) + @($script:CategoryRules)
|
||||
Write-CollectorLog ("custom rules loaded: {0}" -f $rules.Count)
|
||||
Write-CollectorLog ("пользовательские правила загружены: {0}" -f $rules.Count)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-CollectorLog ("custom rules load failed: {0}" -f $_.Exception.Message)
|
||||
Write-CollectorLog ("не удалось загрузить пользовательские правила: {0}" -f $_.Exception.Message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,7 +338,7 @@ function Load-DlpPolicy {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not $Path -or -not (Test-Path -LiteralPath $Path)) {
|
||||
Write-CollectorLog ("dlp policy not found, disabled: {0}" -f $Path)
|
||||
Write-CollectorLog ("DLP-политика не найдена, DLP отключен: {0}" -f $Path)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ function Load-DlpPolicy {
|
||||
enabled = if ($rule.PSObject.Properties.Name -contains 'enabled') { [bool]$rule.enabled } else { $true }
|
||||
action = if ($rule.action) { [string]$rule.action } else { [string]$script:DlpDefaults.action }
|
||||
severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:DlpDefaults.severity }
|
||||
message = if ($rule.message) { [string]$rule.message } else { "DLP rule matched: $($rule.id)" }
|
||||
message = if ($rule.message) { [string]$rule.message } else { "Сработало DLP-правило: $($rule.id)" }
|
||||
cooldownSeconds = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:DlpDefaults.cooldownSeconds }
|
||||
when = [pscustomobject]@{
|
||||
domains = if ($when.PSObject.Properties.Name -contains 'domains') { @($when.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() }
|
||||
@@ -388,10 +388,10 @@ function Load-DlpPolicy {
|
||||
}
|
||||
|
||||
$script:DlpRules = @($loaded)
|
||||
Write-CollectorLog ("dlp policy loaded: enabled={0}, rules={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count)
|
||||
Write-CollectorLog ("DLP-политика загружена: включена={0}, правил={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count)
|
||||
}
|
||||
catch {
|
||||
Write-CollectorLog ("dlp policy parse failed: {0}" -f $_.Exception.Message)
|
||||
Write-CollectorLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,7 +625,7 @@ function Capture-IncidentScreenshot {
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-CollectorLog ("screenshot capture failed: {0}" -f $_.Exception.Message)
|
||||
Write-CollectorLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message)
|
||||
return @{}
|
||||
}
|
||||
}
|
||||
@@ -775,7 +775,7 @@ function Send-CategoryHeartbeat {
|
||||
|
||||
Load-CustomCategoryRules -Path $resolvedRulesPath
|
||||
Load-DlpPolicy -Path $resolvedPolicyPath
|
||||
Write-CollectorLog ("collector started against {0}" -f $script:ApiBase)
|
||||
Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase)
|
||||
|
||||
while ($true) {
|
||||
try {
|
||||
@@ -815,7 +815,7 @@ while ($true) {
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message)
|
||||
Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message)
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $resolvedPollSeconds
|
||||
|
||||
@@ -11,8 +11,8 @@ param(
|
||||
[string]$Version = 'v0.13.2',
|
||||
[string]$PackageUrl,
|
||||
[string]$PackageZipPath,
|
||||
[string]$InstallRoot = 'C:\Program Files\ActivityWatch',
|
||||
[string]$StateRoot = 'C:\ProgramData\ActivityWatch',
|
||||
[string]$InstallRoot = 'C:\Program Files\AWatch-rus\bin',
|
||||
[string]$StateRoot = 'C:\ProgramData\AWatch-rus',
|
||||
[int]$PollSeconds = 5,
|
||||
[int]$PulseSeconds = 30,
|
||||
[int]$RecoveryIntervalSeconds = 180,
|
||||
@@ -44,6 +44,7 @@ $launchScriptPath = Join-Path $StateRoot 'launch-watchers.ps1'
|
||||
$recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1'
|
||||
$collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1'
|
||||
$endpointCollectorSource = Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1'
|
||||
$sessionCollectorSource = Join-Path $PSScriptRoot 'worktime-session-collector.ps1'
|
||||
$exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json'
|
||||
$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json'
|
||||
|
||||
@@ -57,6 +58,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null
|
||||
$assetResult = Copy-ActivityWatchCollectorAssets `
|
||||
-CollectorScriptSource $collectorSource `
|
||||
-EndpointCollectorScriptSource $endpointCollectorSource `
|
||||
-SessionCollectorScriptSource $sessionCollectorSource `
|
||||
-ExampleRulesSource $exampleRulesSource `
|
||||
-ExamplePolicySource $examplePolicySource `
|
||||
-StateRoot $StateRoot `
|
||||
@@ -76,6 +78,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-LogsRoot $logsRoot `
|
||||
-CollectorScript $assetResult.CollectorScript `
|
||||
-EndpointCollectorScript $assetResult.EndpointCollectorScript `
|
||||
-SessionCollectorScript $assetResult.SessionCollectorScript `
|
||||
-RulesPath $assetResult.ActiveRules `
|
||||
-PolicyPath $assetResult.ActivePolicy `
|
||||
-PollSeconds $PollSeconds `
|
||||
@@ -100,8 +103,8 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||
|
||||
Write-Host 'ActivityWatch deployed for users:'
|
||||
Write-Host 'ActivityWatch развёрнут для пользователей:'
|
||||
$targetUsers | ForEach-Object { Write-Host " - $_" }
|
||||
Write-Host "Server: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||
Write-Host "State root: $StateRoot"
|
||||
Write-Host "Policy file: $($assetResult.ActivePolicy)"
|
||||
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||
Write-Host "Каталог данных: $StateRoot"
|
||||
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||
|
||||
@@ -11,8 +11,8 @@ param(
|
||||
[string]$Version = 'v0.13.2',
|
||||
[string]$PackageUrl,
|
||||
[string]$PackageZipPath,
|
||||
[string]$InstallRoot = 'C:\Program Files\ActivityWatch',
|
||||
[string]$StateRoot = 'C:\ProgramData\ActivityWatch',
|
||||
[string]$InstallRoot = 'C:\Program Files\AWatch-rus\bin',
|
||||
[string]$StateRoot = 'C:\ProgramData\AWatch-rus',
|
||||
[int]$PollSeconds = 5,
|
||||
[int]$PulseSeconds = 30,
|
||||
[int]$RecoveryIntervalSeconds = 180,
|
||||
@@ -46,7 +46,7 @@ $hardeningScript = Join-Path $PSScriptRoot 'hardening-recovery.ps1'
|
||||
$validationScript = Join-Path $PSScriptRoot 'validate-deployment.ps1'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $deployScript)) {
|
||||
throw "Missing script: $deployScript"
|
||||
throw "Не найден скрипт: $deployScript"
|
||||
}
|
||||
|
||||
& $deployScript `
|
||||
@@ -118,7 +118,7 @@ $report = [ordered]@{
|
||||
|
||||
if ($ValidateAfterDeploy) {
|
||||
if (-not (Test-Path -LiteralPath $validationScript)) {
|
||||
throw "Missing script: $validationScript"
|
||||
throw "Не найден скрипт: $validationScript"
|
||||
}
|
||||
|
||||
$validation = & $validationScript -ConfigPath (Join-Path $StateRoot 'deployment-config.json')
|
||||
@@ -132,6 +132,6 @@ if ($reportDirectory) {
|
||||
|
||||
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8
|
||||
|
||||
Write-Host 'ActivityWatch ensemble deploy completed.'
|
||||
Write-Host "Users: $($resolvedUsers -join ', ')"
|
||||
Write-Host "Report: $effectiveReportPath"
|
||||
Write-Host 'Комплексное развёртывание ActivityWatch завершено.'
|
||||
Write-Host "Пользователи: $($resolvedUsers -join ', ')"
|
||||
Write-Host "Отчёт: $effectiveReportPath"
|
||||
|
||||
@@ -10,8 +10,8 @@ param(
|
||||
[string]$Version = 'v0.13.2',
|
||||
[string]$PackageUrl,
|
||||
[string]$PackageZipPath,
|
||||
[string]$InstallRoot = 'C:\Program Files\ActivityWatch',
|
||||
[string]$StateRoot = 'C:\ProgramData\ActivityWatch',
|
||||
[string]$InstallRoot = 'C:\Program Files\AWatch-rus\bin',
|
||||
[string]$StateRoot = 'C:\ProgramData\AWatch-rus',
|
||||
[int]$PollSeconds = 5,
|
||||
[int]$PulseSeconds = 30,
|
||||
[int]$RecoveryIntervalSeconds = 180,
|
||||
@@ -42,6 +42,7 @@ $launchScriptPath = Join-Path $StateRoot 'launch-watchers.ps1'
|
||||
$recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1'
|
||||
$collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1'
|
||||
$endpointCollectorSource = Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1'
|
||||
$sessionCollectorSource = Join-Path $PSScriptRoot 'worktime-session-collector.ps1'
|
||||
$exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json'
|
||||
$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json'
|
||||
|
||||
@@ -55,6 +56,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null
|
||||
$assetResult = Copy-ActivityWatchCollectorAssets `
|
||||
-CollectorScriptSource $collectorSource `
|
||||
-EndpointCollectorScriptSource $endpointCollectorSource `
|
||||
-SessionCollectorScriptSource $sessionCollectorSource `
|
||||
-ExampleRulesSource $exampleRulesSource `
|
||||
-ExamplePolicySource $examplePolicySource `
|
||||
-StateRoot $StateRoot `
|
||||
@@ -74,6 +76,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-LogsRoot $logsRoot `
|
||||
-CollectorScript $assetResult.CollectorScript `
|
||||
-EndpointCollectorScript $assetResult.EndpointCollectorScript `
|
||||
-SessionCollectorScript $assetResult.SessionCollectorScript `
|
||||
-RulesPath $assetResult.ActiveRules `
|
||||
-PolicyPath $assetResult.ActivePolicy `
|
||||
-PollSeconds $PollSeconds `
|
||||
@@ -98,9 +101,9 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||
|
||||
Write-Host "ActivityWatch deployed for $TargetUser"
|
||||
Write-Host "Server: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||
Write-Host "Install root: $InstallRoot"
|
||||
Write-Host "State root: $StateRoot"
|
||||
Write-Host "Rules file: $($assetResult.ActiveRules)"
|
||||
Write-Host "Policy file: $($assetResult.ActivePolicy)"
|
||||
Write-Host "ActivityWatch развёрнут для пользователя: $TargetUser"
|
||||
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||
Write-Host "Каталог установки: $InstallRoot"
|
||||
Write-Host "Каталог данных: $StateRoot"
|
||||
Write-Host "Файл правил: $($assetResult.ActiveRules)"
|
||||
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$ServerHost,
|
||||
[int]$ServerPort,
|
||||
[ValidateSet('http', 'https')]
|
||||
@@ -42,11 +42,11 @@ if (Test-Path -LiteralPath $ConfigPath) {
|
||||
}
|
||||
|
||||
if (-not $existingConfig -and (-not $ServerHost)) {
|
||||
throw 'deployment-config.json is missing. Provide -ServerHost and user parameters, or run a deploy script first.'
|
||||
throw 'deployment-config.json отсутствует. Укажите -ServerHost и параметры пользователей либо сначала выполните скрипт развёртывания.'
|
||||
}
|
||||
|
||||
$effectiveStateRoot = if ($StateRoot) { $StateRoot } elseif ($existingConfig) { [string]$existingConfig.paths.stateRoot } else { 'C:\ProgramData\ActivityWatch' }
|
||||
$effectiveInstallRoot = if ($InstallRoot) { $InstallRoot } elseif ($existingConfig) { [string]$existingConfig.paths.installRoot } else { 'C:\Program Files\ActivityWatch' }
|
||||
$effectiveStateRoot = if ($StateRoot) { $StateRoot } elseif ($existingConfig) { [string]$existingConfig.paths.stateRoot } else { 'C:\ProgramData\AWatch-rus' }
|
||||
$effectiveInstallRoot = if ($InstallRoot) { $InstallRoot } elseif ($existingConfig) { [string]$existingConfig.paths.installRoot } else { 'C:\Program Files\AWatch-rus\bin' }
|
||||
$effectiveLogsRoot = if ($existingConfig) { [string]$existingConfig.paths.logsRoot } else { Join-Path $effectiveStateRoot 'logs' }
|
||||
$effectiveConfigPath = if ($ConfigPath) { $ConfigPath } else { Join-Path $effectiveStateRoot 'deployment-config.json' }
|
||||
$effectiveLaunchScript = Join-Path $effectiveStateRoot 'launch-watchers.ps1'
|
||||
@@ -78,7 +78,7 @@ elseif ($existingConfig) {
|
||||
@($existingConfig.userTasks | ForEach-Object { [string]$_.userId })
|
||||
}
|
||||
else {
|
||||
throw 'Target users are missing.'
|
||||
throw 'Не указаны целевые пользователи.'
|
||||
}
|
||||
|
||||
New-ActivityWatchDirectory -Path $effectiveStateRoot
|
||||
@@ -96,6 +96,7 @@ Get-ActivityWatchExecutableMap -InstallRoot $effectiveInstallRoot | Out-Null
|
||||
$assetResult = Copy-ActivityWatchCollectorAssets `
|
||||
-CollectorScriptSource (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') `
|
||||
-EndpointCollectorScriptSource (Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1') `
|
||||
-SessionCollectorScriptSource (Join-Path $PSScriptRoot 'worktime-session-collector.ps1') `
|
||||
-ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') `
|
||||
-ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') `
|
||||
-StateRoot $effectiveStateRoot `
|
||||
@@ -115,6 +116,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-LogsRoot $effectiveLogsRoot `
|
||||
-CollectorScript $effectiveCollector `
|
||||
-EndpointCollectorScript $effectiveEndpointCollector `
|
||||
-SessionCollectorScript $effectiveSessionCollector `
|
||||
-RulesPath $effectiveRules `
|
||||
-PolicyPath $effectivePolicy `
|
||||
-PollSeconds $effectivePollSeconds `
|
||||
@@ -139,6 +141,6 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
|
||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||
|
||||
Write-Host 'ActivityWatch hardening/recovery completed.'
|
||||
Write-Host "Config: $effectiveConfigPath"
|
||||
Write-Host "Users repaired: $($effectiveUsers -join ', ')"
|
||||
Write-Host 'Укрепление и восстановление ActivityWatch завершены.'
|
||||
Write-Host "Конфигурация: $effectiveConfigPath"
|
||||
Write-Host "Пользователи восстановлены: $($effectiveUsers -join ', ')"
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2',
|
||||
[string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2',
|
||||
[string]$NewInstallRoot = 'C:\Program Files\AWatch-rus\bin',
|
||||
[string]$NewStateRoot = 'C:\ProgramData\AWatch-rus',
|
||||
[string]$ToolkitRoot = 'C:\Program Files\AWatch-rus\windows',
|
||||
[switch]$SkipValidation
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1'
|
||||
Import-Module $modulePath -Force
|
||||
|
||||
Assert-Administrator
|
||||
|
||||
function Copy-DirectoryContents {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Source,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Destination
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Source)) {
|
||||
return
|
||||
}
|
||||
|
||||
New-ActivityWatchDirectory -Path $Destination
|
||||
Copy-Item -Path (Join-Path $Source '*') -Destination $Destination -Recurse -Force
|
||||
}
|
||||
|
||||
function Copy-IfExists {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Source,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Destination
|
||||
)
|
||||
|
||||
if (Test-Path -LiteralPath $Source) {
|
||||
Copy-Item -LiteralPath $Source -Destination $Destination -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Convert-PathValue {
|
||||
param(
|
||||
[AllowNull()]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||||
return $Value
|
||||
}
|
||||
|
||||
return $Value.Replace($OldInstallRoot, $NewInstallRoot).Replace($OldStateRoot, $NewStateRoot)
|
||||
}
|
||||
|
||||
function Stop-AWatchTaskSet {
|
||||
foreach ($task in @(Get-ScheduledTask | Where-Object { $_.TaskName -eq 'ActivityWatch Recovery' -or $_.TaskName -like 'ActivityWatch Launch *' })) {
|
||||
Stop-ScheduledTask -TaskName $task.TaskName -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ExistingAWatchConfig {
|
||||
$newConfigPath = Join-Path $NewStateRoot 'deployment-config.json'
|
||||
$oldConfigPath = Join-Path $OldStateRoot 'deployment-config.json'
|
||||
|
||||
if (Test-Path -LiteralPath $oldConfigPath) {
|
||||
return [pscustomobject]@{
|
||||
Path = $oldConfigPath
|
||||
Config = Read-ActivityWatchDeploymentConfig -Path $oldConfigPath
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $newConfigPath) {
|
||||
return [pscustomobject]@{
|
||||
Path = $newConfigPath
|
||||
Config = Read-ActivityWatchDeploymentConfig -Path $newConfigPath
|
||||
}
|
||||
}
|
||||
|
||||
throw "Не найден deployment-config.json ни в $OldStateRoot, ни в $NewStateRoot."
|
||||
}
|
||||
|
||||
function Update-AWatchConfigPaths {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Config
|
||||
)
|
||||
|
||||
$logsRoot = Join-Path $NewStateRoot 'logs'
|
||||
$Config.paths.installRoot = $NewInstallRoot
|
||||
$Config.paths.stateRoot = $NewStateRoot
|
||||
$Config.paths.logsRoot = $logsRoot
|
||||
$Config.paths.collectorScript = Join-Path $NewStateRoot 'browser-domains-native-collector.ps1'
|
||||
$Config.paths.endpointCollectorScript = Join-Path $NewStateRoot 'dlp-endpoint-signals-collector.ps1'
|
||||
if ($Config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') {
|
||||
$Config.paths.sessionCollectorScript = Join-Path $NewStateRoot 'worktime-session-collector.ps1'
|
||||
}
|
||||
$Config.paths.rulesPath = Join-Path $NewStateRoot 'web-category-rules.json'
|
||||
if ($Config.paths.PSObject.Properties.Name -contains 'policyPath') {
|
||||
$Config.paths.policyPath = Join-Path $NewStateRoot 'dlp-policy.json'
|
||||
}
|
||||
$Config.paths.launchScript = Join-Path $NewStateRoot 'launch-watchers.ps1'
|
||||
$Config.paths.recoveryScript = Join-Path $NewStateRoot 'recovery-loop.ps1'
|
||||
|
||||
if ($Config.PSObject.Properties.Name -contains 'incidentCapture' -and $Config.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') {
|
||||
$Config.incidentCapture.artifactsRoot = Convert-PathValue -Value ([string]$Config.incidentCapture.artifactsRoot)
|
||||
}
|
||||
|
||||
return $Config
|
||||
}
|
||||
|
||||
$existing = Get-ExistingAWatchConfig
|
||||
$backupRoot = Join-Path $NewStateRoot ('migration-backups\' + (Get-Date -Format 'yyyyMMdd-HHmmss'))
|
||||
$newConfigPath = Join-Path $NewStateRoot 'deployment-config.json'
|
||||
$newLogsRoot = Join-Path $NewStateRoot 'logs'
|
||||
|
||||
$summary = [ordered]@{
|
||||
sourceConfig = $existing.Path
|
||||
oldInstallRoot = $OldInstallRoot
|
||||
oldStateRoot = $OldStateRoot
|
||||
newInstallRoot = $NewInstallRoot
|
||||
newStateRoot = $NewStateRoot
|
||||
backupRoot = $backupRoot
|
||||
actions = @(
|
||||
'stop ActivityWatch scheduled tasks',
|
||||
'backup old/new install and state directories',
|
||||
'copy old install/state contents to AWatch-rus paths',
|
||||
'rewrite deployment-config.json paths',
|
||||
'regenerate launcher/recovery scripts',
|
||||
're-register scheduled tasks',
|
||||
'run validate-deployment.ps1'
|
||||
)
|
||||
}
|
||||
|
||||
if ($WhatIfPreference) {
|
||||
return [pscustomobject]$summary
|
||||
}
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Миграция ActivityWatch Windows/RDP путей в AWatch-rus')) {
|
||||
New-ActivityWatchDirectory -Path $NewStateRoot
|
||||
New-ActivityWatchDirectory -Path $backupRoot
|
||||
|
||||
Stop-AWatchTaskSet
|
||||
|
||||
foreach ($item in @(
|
||||
@{ Source = $OldInstallRoot; Name = 'old-install' },
|
||||
@{ Source = $OldStateRoot; Name = 'old-state' },
|
||||
@{ Source = $NewInstallRoot; Name = 'new-install' },
|
||||
@{ Source = $NewStateRoot; Name = 'new-state' }
|
||||
)) {
|
||||
if (Test-Path -LiteralPath $item.Source) {
|
||||
Copy-Item -LiteralPath $item.Source -Destination (Join-Path $backupRoot $item.Name) -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
Copy-DirectoryContents -Source $OldInstallRoot -Destination $NewInstallRoot
|
||||
Copy-DirectoryContents -Source $OldStateRoot -Destination $NewStateRoot
|
||||
New-ActivityWatchDirectory -Path $newLogsRoot
|
||||
|
||||
foreach ($file in @(
|
||||
'browser-domains-native-collector.ps1',
|
||||
'dlp-endpoint-signals-collector.ps1',
|
||||
'worktime-session-collector.ps1',
|
||||
'web-category-rules.example.json',
|
||||
'dlp-policy.example.json'
|
||||
)) {
|
||||
Copy-IfExists -Source (Join-Path $ToolkitRoot $file) -Destination (Join-Path $NewStateRoot $file)
|
||||
}
|
||||
|
||||
Copy-IfExists -Source (Join-Path $OldStateRoot 'web-category-rules.json') -Destination (Join-Path $NewStateRoot 'web-category-rules.json')
|
||||
Copy-IfExists -Source (Join-Path $OldStateRoot 'dlp-policy.json') -Destination (Join-Path $NewStateRoot 'dlp-policy.json')
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $NewStateRoot 'web-category-rules.json'))) {
|
||||
Copy-IfExists -Source (Join-Path $NewStateRoot 'web-category-rules.example.json') -Destination (Join-Path $NewStateRoot 'web-category-rules.json')
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $NewStateRoot 'dlp-policy.json'))) {
|
||||
Copy-IfExists -Source (Join-Path $NewStateRoot 'dlp-policy.example.json') -Destination (Join-Path $NewStateRoot 'dlp-policy.json')
|
||||
}
|
||||
|
||||
$config = Update-AWatchConfigPaths -Config $existing.Config
|
||||
Write-ActivityWatchDeploymentConfig -Config $config -Path $newConfigPath
|
||||
Write-ActivityWatchLaunchScript -Path $config.paths.launchScript -ConfigPath $newConfigPath
|
||||
Write-ActivityWatchRecoveryScript -Path $config.paths.recoveryScript -ConfigPath $newConfigPath
|
||||
|
||||
$taskDefinitions = @($config.userTasks)
|
||||
Set-ActivityWatchAcl -InstallRoot $NewInstallRoot -StateRoot $NewStateRoot -LogsRoot $newLogsRoot
|
||||
Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $config.paths.launchScript -ConfigPath $newConfigPath
|
||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $config.paths.recoveryScript -ConfigPath $newConfigPath
|
||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
if (-not $SkipValidation) {
|
||||
$validateScript = Join-Path $ToolkitRoot 'validate-deployment.ps1'
|
||||
if (-not (Test-Path -LiteralPath $validateScript)) {
|
||||
$validateScript = Join-Path $PSScriptRoot 'validate-deployment.ps1'
|
||||
}
|
||||
$report = & $validateScript -ConfigPath $newConfigPath
|
||||
if (-not [bool]$report.overallOk) {
|
||||
throw "Миграция выполнена, но validation завершился ошибкой. Backup: $backupRoot"
|
||||
}
|
||||
}
|
||||
|
||||
[pscustomobject]@{
|
||||
migrated = $true
|
||||
backupRoot = $backupRoot
|
||||
configPath = $newConfigPath
|
||||
installRoot = $NewInstallRoot
|
||||
stateRoot = $NewStateRoot
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json'
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json'
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -14,29 +14,48 @@ $installRoot = [string]$config.paths.installRoot
|
||||
$stateRoot = [string]$config.paths.stateRoot
|
||||
$collectorScript = [string]$config.paths.collectorScript
|
||||
$endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' }
|
||||
$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' }
|
||||
$rulesPath = [string]$config.paths.rulesPath
|
||||
$policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' }
|
||||
$launchScript = [string]$config.paths.launchScript
|
||||
$recoveryScript = [string]$config.paths.recoveryScript
|
||||
|
||||
$afkExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true }
|
||||
$windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true }
|
||||
$requiredFiles = @(
|
||||
(Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe'),
|
||||
(Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'),
|
||||
$collectorScript,
|
||||
$endpointCollectorScript,
|
||||
$sessionCollectorScript,
|
||||
$rulesPath,
|
||||
$policyPath,
|
||||
$launchScript,
|
||||
$recoveryScript,
|
||||
$ConfigPath
|
||||
)
|
||||
if ($afkExpected) {
|
||||
$requiredFiles += (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe')
|
||||
}
|
||||
if ($windowExpected) {
|
||||
$requiredFiles += (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe')
|
||||
}
|
||||
|
||||
$missingFiles = @(
|
||||
$requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) }
|
||||
)
|
||||
|
||||
$processNames = @('aw-watcher-afk', 'aw-watcher-window')
|
||||
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
|
||||
$processNames = @()
|
||||
if ($afkExpected) { $processNames += 'aw-watcher-afk' }
|
||||
if ($windowExpected) { $processNames += 'aw-watcher-window' }
|
||||
$runningProcesses = @()
|
||||
if ($processNames.Count -gt 0) {
|
||||
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
|
||||
}
|
||||
$sessionCollectorProcesses = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and
|
||||
$_.CommandLine -match [Regex]::Escape($sessionCollectorScript)
|
||||
} |
|
||||
Select-Object Name, ProcessId, SessionId, CommandLine
|
||||
|
||||
$taskNames = @()
|
||||
if ($config.userTasks) {
|
||||
@@ -57,7 +76,7 @@ $tasks = foreach ($taskName in $taskNames) {
|
||||
else {
|
||||
[pscustomobject]@{
|
||||
taskName = $taskName
|
||||
state = 'Missing'
|
||||
state = 'Отсутствует'
|
||||
present = $false
|
||||
}
|
||||
}
|
||||
@@ -80,8 +99,16 @@ $result = [ordered]@{
|
||||
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present }))
|
||||
}
|
||||
processes = [ordered]@{
|
||||
expected = $processNames
|
||||
list = @($runningProcesses)
|
||||
ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2)
|
||||
sessionCollectors = @($sessionCollectorProcesses)
|
||||
ok = [bool](
|
||||
(
|
||||
($processNames.Count -eq 0) -or
|
||||
(($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge $processNames.Count)
|
||||
) -and
|
||||
($sessionCollectorProcesses.Count -ge 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$Hostname,
|
||||
[int]$PollSeconds = 30
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-Config {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "Конфигурация не найдена: $Path"
|
||||
}
|
||||
|
||||
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$Json
|
||||
)
|
||||
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||
}
|
||||
|
||||
function Ensure-Bucket {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||
)
|
||||
|
||||
try {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
return
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
$body = @{
|
||||
client = 'aw-worktime-session-collector'
|
||||
type = 'aw.worktime.session'
|
||||
hostname = $HostnameValue
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||
}
|
||||
|
||||
function Get-SessionRecords {
|
||||
$records = @()
|
||||
|
||||
try {
|
||||
$lines = quser 2>$null
|
||||
if (-not $lines) {
|
||||
return @()
|
||||
}
|
||||
|
||||
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||
if (-not $clean) {
|
||||
continue
|
||||
}
|
||||
|
||||
$parts = $clean -split '\s+'
|
||||
if ($parts.Count -lt 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
$sessionName = ''
|
||||
$sessionIdIndex = 2
|
||||
if ($parts[1] -match '^\d+$') {
|
||||
$sessionIdIndex = 1
|
||||
}
|
||||
else {
|
||||
$sessionName = $parts[1]
|
||||
}
|
||||
|
||||
$sessionId = 0
|
||||
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||
$sessionId = [int]$parts[$sessionIdIndex]
|
||||
}
|
||||
|
||||
$records += [pscustomobject]@{
|
||||
username = $parts[0]
|
||||
sessionName = $sessionName
|
||||
sessionId = $sessionId
|
||||
state = $parts[$sessionIdIndex + 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
return $records
|
||||
}
|
||||
|
||||
$cfg = Get-Config -Path $ConfigPath
|
||||
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||
$pulse = 120
|
||||
$sleepSec = if ($PollSeconds -gt 0) {
|
||||
$PollSeconds
|
||||
}
|
||||
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||
[int]$cfg.collector.pollSeconds
|
||||
}
|
||||
else {
|
||||
30
|
||||
}
|
||||
|
||||
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||
|
||||
while ($true) {
|
||||
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||
$records = Get-SessionRecords
|
||||
if (-not $records -or $records.Count -eq 0) {
|
||||
$records = @([pscustomobject]@{
|
||||
username = $env:USERNAME
|
||||
sessionName = ''
|
||||
sessionId = (Get-Process -Id $PID).SessionId
|
||||
state = 'Unknown'
|
||||
})
|
||||
}
|
||||
|
||||
foreach ($rec in $records) {
|
||||
$payload = @{
|
||||
timestamp = $now
|
||||
duration = 0
|
||||
data = @{
|
||||
username = [string]$rec.username
|
||||
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||
sessionId = [int]$rec.sessionId
|
||||
sessionName = [string]$rec.sessionName
|
||||
state = [string]$rec.state
|
||||
active = ($rec.state -match 'Active')
|
||||
hostname = $hostValue
|
||||
source = 'worktime-session-collector'
|
||||
}
|
||||
} | ConvertTo-Json -Depth 6 -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleepSec
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
Executable
+517
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$ServerHost,
|
||||
[int]$ServerPort,
|
||||
[ValidateSet('http', 'https')]
|
||||
@@ -49,18 +49,18 @@ function Get-DeploymentConfig {
|
||||
}
|
||||
|
||||
$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath
|
||||
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' }
|
||||
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' }
|
||||
$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 }
|
||||
$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' }
|
||||
$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\ActivityWatch\web-category-rules.json' }
|
||||
$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\ActivityWatch\dlp-policy.json' }
|
||||
$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\AWatch-rus\web-category-rules.json' }
|
||||
$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' }
|
||||
$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 }
|
||||
$resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pulseSeconds } else { 30 }
|
||||
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\ActivityWatch\logs' }
|
||||
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' }
|
||||
$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) }
|
||||
$resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) }
|
||||
$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true }
|
||||
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'ActivityWatch-Phase2\\incident-artifacts' }
|
||||
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' }
|
||||
$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true }
|
||||
|
||||
if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) {
|
||||
@@ -158,12 +158,12 @@ function Get-HostFromUrl {
|
||||
|
||||
try {
|
||||
$uri = [Uri]$Url
|
||||
$host = $uri.Host.ToLowerInvariant()
|
||||
if ($host.StartsWith('www.')) {
|
||||
return $host.Substring(4)
|
||||
$uriHost = $uri.Host.ToLowerInvariant()
|
||||
if ($uriHost.StartsWith('www.')) {
|
||||
return $uriHost.Substring(4)
|
||||
}
|
||||
|
||||
return $host
|
||||
return $uriHost
|
||||
}
|
||||
catch {
|
||||
return $null
|
||||
@@ -263,11 +263,11 @@ function Load-CustomCategoryRules {
|
||||
|
||||
if ($rules.Count -gt 0) {
|
||||
$script:CategoryRules = @($rules) + @($script:CategoryRules)
|
||||
Write-CollectorLog ("custom rules loaded: {0}" -f $rules.Count)
|
||||
Write-CollectorLog ("пользовательские правила загружены: {0}" -f $rules.Count)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-CollectorLog ("custom rules load failed: {0}" -f $_.Exception.Message)
|
||||
Write-CollectorLog ("не удалось загрузить пользовательские правила: {0}" -f $_.Exception.Message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,7 +338,7 @@ function Load-DlpPolicy {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not $Path -or -not (Test-Path -LiteralPath $Path)) {
|
||||
Write-CollectorLog ("dlp policy not found, disabled: {0}" -f $Path)
|
||||
Write-CollectorLog ("DLP-политика не найдена, DLP отключен: {0}" -f $Path)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ function Load-DlpPolicy {
|
||||
enabled = if ($rule.PSObject.Properties.Name -contains 'enabled') { [bool]$rule.enabled } else { $true }
|
||||
action = if ($rule.action) { [string]$rule.action } else { [string]$script:DlpDefaults.action }
|
||||
severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:DlpDefaults.severity }
|
||||
message = if ($rule.message) { [string]$rule.message } else { "DLP rule matched: $($rule.id)" }
|
||||
message = if ($rule.message) { [string]$rule.message } else { "Сработало DLP-правило: $($rule.id)" }
|
||||
cooldownSeconds = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:DlpDefaults.cooldownSeconds }
|
||||
when = [pscustomobject]@{
|
||||
domains = if ($when.PSObject.Properties.Name -contains 'domains') { @($when.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() }
|
||||
@@ -388,10 +388,10 @@ function Load-DlpPolicy {
|
||||
}
|
||||
|
||||
$script:DlpRules = @($loaded)
|
||||
Write-CollectorLog ("dlp policy loaded: enabled={0}, rules={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count)
|
||||
Write-CollectorLog ("DLP-политика загружена: включена={0}, правил={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count)
|
||||
}
|
||||
catch {
|
||||
Write-CollectorLog ("dlp policy parse failed: {0}" -f $_.Exception.Message)
|
||||
Write-CollectorLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,7 +625,7 @@ function Capture-IncidentScreenshot {
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-CollectorLog ("screenshot capture failed: {0}" -f $_.Exception.Message)
|
||||
Write-CollectorLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message)
|
||||
return @{}
|
||||
}
|
||||
}
|
||||
@@ -775,7 +775,7 @@ function Send-CategoryHeartbeat {
|
||||
|
||||
Load-CustomCategoryRules -Path $resolvedRulesPath
|
||||
Load-DlpPolicy -Path $resolvedPolicyPath
|
||||
Write-CollectorLog ("collector started against {0}" -f $script:ApiBase)
|
||||
Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase)
|
||||
|
||||
while ($true) {
|
||||
try {
|
||||
@@ -815,7 +815,7 @@ while ($true) {
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message)
|
||||
Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message)
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $resolvedPollSeconds
|
||||
|
||||
@@ -11,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)"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
[CmdletBinding()]
|
||||
\xEF\xBB\xBF-ne \xEF\xBB\xBF
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
|
||||
[string]$ServerHost,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 ', ')"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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\`).
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user