From a1801fca5f0502c9745baedf027fafc0b96428e3 Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Wed, 3 Jun 2026 08:52:20 +0300 Subject: [PATCH] docs(public): sanitize release materials --- .gitignore | 2 + .../SUMMARY.md | 51 - INSTALL_FOR_EXPERT_RU.md | 53 +- SECURITY_OVERVIEW_RU.md | 2 +- adk-rust/RUNBOOK.md | 18 +- adk-rust/crates/aw-contour-smoke/src/main.rs | 22 +- adk-rust/crates/aw-db-health/src/main.rs | 2 +- adk-rust/crates/aw-db-maintenance/src/main.rs | 2 +- adk-rust/crates/aw-health-check/src/main.rs | 4 +- adk-rust/crates/aw-linux-install/src/main.rs | 4 +- adk-rust/crates/aw-rus-healthd/src/main.rs | 4 +- adk-rust/crates/aw-slo-monitor/src/main.rs | 2 +- adk-rust/crates/check-aw-data/src/main.rs | 10 +- adk-rust/crates/check-aw-full/src/main.rs | 8 +- .../check-install-kit-vs-repo/src/main.rs | 4 +- adk-rust/crates/detmir-check/src/main.rs | 12 +- adk-rust/crates/detmir-dlp/src/main.rs | 2 +- .../crates/detmir-grafana-check/src/main.rs | 6 +- adk-rust/crates/detmir-heal-safe/src/main.rs | 2 +- adk-rust/crates/detmir-portal/src/main.rs | 8 +- .../diag-and-manual-restart/src/main.rs | 20 +- adk-rust/crates/dlp-health-check/src/main.rs | 6 +- .../crates/dlp-influx-exporter/src/main.rs | 20 +- .../crates/prod-backup-restore/src/main.rs | 6 +- .../crates/rdp-worktime-report/src/main.rs | 8 +- .../crates/tsj-guardian-status/src/main.rs | 8 +- .../crates/tsj-guardian-watchdog/src/main.rs | 3 +- adk-rust/crates/worktime-api/src/main.rs | 4 +- adk-rust/crates/worktime-autoheal/src/main.rs | 2 +- .../worktime-influx-exporter/src/main.rs | 12 +- adk-rust/crates/worktime-prewarm/src/main.rs | 2 +- .../crates/worktime-ui-bridge/src/main.rs | 2 +- ansible/deploy_aw_server.yml | 24 +- ansible/deploy_aw_windows.yml | 2 +- ansible/deploy_detmir_portal.yml | 6 +- ansible/deploy_dlp_evidence_sync.yml | 2 +- ansible/deploy_file_1c_analytics.yml | 2 +- ansible/deploy_file_1c_windows_telemetry.yml | 6 +- ansible/deploy_grafana_check.yml | 2 +- ansible/deploy_proxmox_web_gateway.yml | 30 +- ansible/deploy_tsj_guardian_bot_proxmox.yml | 50 +- ansible/group_vars/all.example.yml | 8 +- ansible/group_vars/all.yml | 14 +- ansible/group_vars/aw_windows.yml | 2 +- ansible/group_vars/proxmox-bot.example.yml | 2 +- ansible/group_vars/windows.example.yml | 4 +- ansible/inventory.example.ini | 4 +- ansible/tasks/provision_ct_and_deploy_aw.yml | 10 +- .../proxmox-web-gateway-index.html.j2 | 2 +- ansible/templates/proxmox-web-gateway.conf.j2 | 68 +- aw-server/apply_webui_ru_patch.sh | 8 +- aw-server/aw-server.env.example | 6 +- aw-server/aw-worktime-autoheal.service | 4 +- aw-server/aw-worktime-prewarm.sh | 2 +- aw-server/aw-worktime-ui-bridge.service | 2 +- aw-server/hayabusa/README.md | 12 +- aw-server/install_aw_server.sh | 4 +- .../worktime-manager-aliases.example.json | 6 +- check-aw-full.sh | 22 +- clickhouse-1c/.env.example | 2 +- clickhouse-1c/README.md | 2 +- clickhouse-1c/ai/company_intelligence_api.py | 2 +- .../files/1c-financial-reporting.json | 2 +- .../dashboards/files/1c-management-board.json | 8 +- .../dashboards/files/1c-telemetry-board.json | 2 +- clickhouse-1c/sample/seed_demo.sql | 10 +- detmir-mcp/.env.example | 14 +- .../examples/mcpdrill-detmir-readonly.json | 6 +- detmir-mcp/main.py | 14 +- docs/1C_FILE_ANALYTICS_STACK_RU.md | 4 +- ...ETMIR_CHANGE_REPORT_LAST_24H_2026-05-26.md | 4 +- docs/DETMIR_UNIFIED_OPERATING_MODEL_RU.md | 2 +- docs/INSTALL_FOR_EXPERT_RU.md | 336 +++ docs/SBOM_RELEASE_CHECKLIST_RU.md | 277 ++ ...tent-analysis-runtime-status-2026-05-13.md | 2 +- docs/dlp-runtime-chain-status-2026-05-13.md | 4 +- docs/wiki/Windows-Collector-Suite.md | 8 +- docs/windows-deploy-startup-model.md | 2 +- docs/windows/deployment.md | 4 +- docs/windows/ensemble.md | 2 +- docs/windows/validation.md | 2 +- docs/worktime_aql_detmir.md | 16 +- grafana-1c/.env.example | 4 +- grafana-1c/docker-compose.yml | 2 +- .../collectors/aw_activitywatch.py | 2 +- grafana/detmir-aw-main-dashboard.json | 4 +- .../detmir-rdp-user-activity-dashboard.json | 4 +- .../MANIFEST.txt | 59 - .../README-INSTALL-KIT.txt | 14 - .../ansible/README.md | 289 -- .../ansible/deploy_aw_pfsense_poller.yml | 66 - .../ansible/deploy_aw_server.yml | 2569 ----------------- .../ansible/deploy_aw_windows.yml | 559 ---- .../ansible/group_vars/all.example.yml | 86 - .../group_vars/pfsense-poller.example.yml | 30 - .../group_vars/proxmox-matrix.example.yml | 38 - .../ansible/group_vars/proxmox.example.yml | 18 - .../ansible/group_vars/windows.example.yml | 80 - .../ansible/install_full_stack.yml | 18 - .../ansible/inventory.example.ini | 17 - .../provision_proxmox_ct_and_deploy_aw.yml | 14 - ...vision_proxmox_ct_matrix_and_deploy_aw.yml | 14 - .../tasks/provision_ct_and_deploy_aw.yml | 230 -- .../aw-server/activitywatch-server.service | 24 - .../aw-server/apply_webui_ru_patch.sh | 222 -- .../aw-server/aw-browser-smoke.service | 28 - .../aw-server/aw-browser-smoke.timer | 12 - .../aw-server/aw-host-groups.json | 60 - .../aw-server/aw-ru-patch.js | 2220 -------------- .../aw-server/aw-rus-healthd.service | 14 - .../aw-server/aw-rus-healthd.timer | 12 - .../aw-server/aw-server.env.example | 77 - .../aw-server/aw-slo-monitor.service | 16 - .../aw-server/aw-slo-monitor.timer | 12 - .../aw-server/aw-sw-cleanup.js | 18 - .../aw-server/aw-worktime-api.service | 21 - .../aw-server/aw-worktime-panel.js | 68 - .../aw-server/aw-worktime-prewarm.service | 14 - .../aw-server/aw-worktime-prewarm.timer | 11 - .../aw-server/install_aw_server.sh | 182 -- .../aw-server/settings/classes-worktime.json | 90 - .../aw-server/settings/views-default.json | 29 - .../scripts/aw-webui-browser-smoke.mjs | 499 ---- .../scripts/aw-webui-browser-smoke.sh | 23 - .../scripts/check_install_kit_vs_repo.sh | 28 - .../scripts/quality-gate.sh | 97 - .../scripts/rebuild_install_kit.sh | 27 - .../scripts/validate_install_kit.sh | 36 - .../scripts/verify_innosetup_installer.sh | 73 - .../windows/AWatchRusCollectorGuardService.cs | 123 - .../windows/ActivityWatch.Windows.Common.psd1 | 25 - .../windows/ActivityWatch.Windows.Common.psm1 | 2548 ---------------- .../windows/aw-collector-guard.ps1 | 681 ----- .../browser-domains-native-collector.ps1 | 953 ------ .../windows/deploy-domain-users.ps1 | 183 -- .../windows/deploy-ensemble.ps1 | 203 -- .../windows/deploy-single-user.ps1 | 125 - .../dlp-endpoint-signals-collector.ps1 | 1684 ----------- .../windows/dlp-policy.example.json | 123 - .../dlp-policy.native-cross-os.example.json | 145 - .../windows/email-outbound-collector.ps1 | 624 ---- .../windows/hardening-recovery.ps1 | 242 -- .../install-collector-guard-service.ps1 | 88 - .../windows/migrate-awatch-rus-paths.ps1 | 243 -- .../windows/validate-deployment.ps1 | 677 ----- .../windows/web-category-rules.example.json | 37 - .../windows/worktime-session-collector.ps1 | 574 ---- ops/detmir-ai/detmir-auto-rust-shadow.service | 4 +- ops/detmir-ai/detmir-heal-safe | 2 +- pfsense/pfsense-aw-poller.example.json | 4 +- proxmox/create-ct.sh | 2 +- proxmox/pfsense_openvpn_client_export.php | 6 +- proxmox/push-aw-artifacts.sh | 12 +- proxmox/test_tsj_guardian_bot.py | 136 +- proxmox/tsj_guardian_bot.py | 28 +- ...10.10.2.sh => aw-contour-smoke-gateway.sh} | 24 +- scripts/aw-contour-smoke-local.sh | 40 +- scripts/aw-webui-browser-smoke.mjs | 2 +- scripts/diag_and_manual_restart.sh | 20 +- scripts/install_aw_console_ssh_logger.sh | 4 +- scripts/install_aw_linux_client.sh | 4 +- scripts/install_aw_linux_remote_worker.sh | 4 +- .../install_aw_linux_web_category_logger.sh | 4 +- scripts/install_aw_pve_webadmin_logger.sh | 4 +- scripts/install_detmir_powershell_mcp.sh | 2 +- scripts/legacy/check-aw-data.sh | 20 +- scripts/metagpt-aw-scout.sh | 12 +- .../powershell/detmir-powershell-profile.ps1 | 2 +- .../powershell/detmir-windows.psd1.example | 2 +- scripts/prod_rollout.sh | 4 +- scripts/rdp-worktime-report.sh | 6 +- windows/ActivityWatch.Windows.Common.psm1 | 2 +- windows/aw-collector-guard.ps1 | 8 +- .../innosetup/AWatch-rus-InnoSetup.iss | 4 +- windows/run-user1-probe.ps1 | 4 +- 175 files changed, 1121 insertions(+), 17893 deletions(-) delete mode 100644 .planning/phases/17-hayabusa-production-validation/SUMMARY.md create mode 100644 docs/INSTALL_FOR_EXPERT_RU.md create mode 100644 docs/SBOM_RELEASE_CHECKLIST_RU.md delete mode 100644 install-kit-awindows-20260427-211240/MANIFEST.txt delete mode 100644 install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt delete mode 100644 install-kit-awindows-20260427-211240/ansible/README.md delete mode 100644 install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml delete mode 100644 install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml delete mode 100644 install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml delete mode 100644 install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml delete mode 100644 install-kit-awindows-20260427-211240/ansible/group_vars/pfsense-poller.example.yml delete mode 100644 install-kit-awindows-20260427-211240/ansible/group_vars/proxmox-matrix.example.yml delete mode 100644 install-kit-awindows-20260427-211240/ansible/group_vars/proxmox.example.yml delete mode 100644 install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml delete mode 100644 install-kit-awindows-20260427-211240/ansible/install_full_stack.yml delete mode 100644 install-kit-awindows-20260427-211240/ansible/inventory.example.ini delete mode 100644 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml delete mode 100644 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml delete mode 100644 install-kit-awindows-20260427-211240/ansible/tasks/provision_ct_and_deploy_aw.yml delete mode 100755 install-kit-awindows-20260427-211240/aw-server/activitywatch-server.service delete mode 100755 install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh delete mode 100644 install-kit-awindows-20260427-211240/aw-server/aw-browser-smoke.service delete mode 100644 install-kit-awindows-20260427-211240/aw-server/aw-browser-smoke.timer delete mode 100644 install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json delete mode 100755 install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js delete mode 100644 install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.service delete mode 100644 install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.timer delete mode 100755 install-kit-awindows-20260427-211240/aw-server/aw-server.env.example delete mode 100644 install-kit-awindows-20260427-211240/aw-server/aw-slo-monitor.service delete mode 100644 install-kit-awindows-20260427-211240/aw-server/aw-slo-monitor.timer delete mode 100755 install-kit-awindows-20260427-211240/aw-server/aw-sw-cleanup.js delete mode 100644 install-kit-awindows-20260427-211240/aw-server/aw-worktime-api.service delete mode 100644 install-kit-awindows-20260427-211240/aw-server/aw-worktime-panel.js delete mode 100644 install-kit-awindows-20260427-211240/aw-server/aw-worktime-prewarm.service delete mode 100644 install-kit-awindows-20260427-211240/aw-server/aw-worktime-prewarm.timer delete mode 100755 install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh delete mode 100644 install-kit-awindows-20260427-211240/aw-server/settings/classes-worktime.json delete mode 100644 install-kit-awindows-20260427-211240/aw-server/settings/views-default.json delete mode 100644 install-kit-awindows-20260427-211240/scripts/aw-webui-browser-smoke.mjs delete mode 100644 install-kit-awindows-20260427-211240/scripts/aw-webui-browser-smoke.sh delete mode 100644 install-kit-awindows-20260427-211240/scripts/check_install_kit_vs_repo.sh delete mode 100644 install-kit-awindows-20260427-211240/scripts/quality-gate.sh delete mode 100644 install-kit-awindows-20260427-211240/scripts/rebuild_install_kit.sh delete mode 100644 install-kit-awindows-20260427-211240/scripts/validate_install_kit.sh delete mode 100644 install-kit-awindows-20260427-211240/scripts/verify_innosetup_installer.sh delete mode 100644 install-kit-awindows-20260427-211240/windows/AWatchRusCollectorGuardService.cs delete mode 100644 install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1 delete mode 100755 install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 delete mode 100644 install-kit-awindows-20260427-211240/windows/aw-collector-guard.ps1 delete mode 100755 install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 delete mode 100755 install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 delete mode 100644 install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 delete mode 100755 install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 delete mode 100644 install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 delete mode 100644 install-kit-awindows-20260427-211240/windows/dlp-policy.example.json delete mode 100644 install-kit-awindows-20260427-211240/windows/dlp-policy.native-cross-os.example.json delete mode 100644 install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1 delete mode 100755 install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 delete mode 100644 install-kit-awindows-20260427-211240/windows/install-collector-guard-service.ps1 delete mode 100644 install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 delete mode 100644 install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 delete mode 100755 install-kit-awindows-20260427-211240/windows/web-category-rules.example.json delete mode 100644 install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 rename scripts/{aw-contour-smoke-10.10.10.2.sh => aw-contour-smoke-gateway.sh} (88%) diff --git a/.gitignore b/.gitignore index 9698f20..d74af88 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ data/ # Release assets kept outside git /install-kit-awindows-*.zip /install-kit-awindows-*.tar.gz +/install-kit-awindows-*/ +/scripts/configure_pfsense_gateway_nat.py diff --git a/.planning/phases/17-hayabusa-production-validation/SUMMARY.md b/.planning/phases/17-hayabusa-production-validation/SUMMARY.md deleted file mode 100644 index a0aacf8..0000000 --- a/.planning/phases/17-hayabusa-production-validation/SUMMARY.md +++ /dev/null @@ -1,51 +0,0 @@ -# Phase 17 Summary: Production Validation - -## Result - -Phase 17 is closed. - -## What was proven live - -A real end-to-end forensic path was executed and closed on production infrastructure: - -1. Windows EVTX package was exported on `SHARKON2025`. -2. The real zip package was transferred to `10.10.10.13`. -3. `aw-hayabusa accept` and `aw-hayabusa process-inbox --mode incident` were run through the standard wrapper. -4. Hayabusa generated a real report set with bounded traceability metadata. -5. The result was linked back into AW-rus case management as bounded `forensics.hayabusa` metadata. - -## Live proof record - -- host: `SHARKON2025` -- case id: `30` -- intake id: `20260521T125653Z_SHARKON2025-phase17-rerun3` -- package path: `/opt/hayabusa/archive/packages/SHARKON2025/20260521T125653Z_SHARKON2025-phase17-rerun3.zip` -- sha256: `e86b9abbfc1d706ac706c6c8a89509ab17023344c50880641e9175f73f1198d4` -- report dir: `/opt/hayabusa/reports/SHARKON2025/20260521T125654Z_incident_20260521T125653Z_SHARKON2025-phase17-rerun3` -- report artifacts: - - `summary.html` - - `manifest.json` - - `run.log` - - `timeline.jsonl` - - `logon-summary-successful.csv` - - `logon-summary-failed.csv` - -## Production bugs found and fixed during validation - -- `aw-hayabusa` treated `unzip` warning return code `1` as a hard failure for Windows-created zip archives that use backslashes as path separators. -- timeline modes were using the wrong Hayabusa config path; the wrapper must pass `rules/config`, not the rules root. - -Both issues were fixed in `aw-server/hayabusa/aw-hayabusa.sh` and retested live against the same package. - -## Why this closes the phase - -- the path is no longer theoretical or docs-only; it was proven on a real Windows export package -- traceability from host to package to report directory is explicit -- AW-rus case linkage now stores bounded forensic metadata exactly as designed -- the remaining gaps are operational tuning items, not missing core implementation - -## Tuning backlog after the live run - -- keep at least one preserved sample EVTX zip for future regression reruns -- consider a self-check in `aw-hayabusa doctor` for `rules/config` completeness -- optionally persist a compact machine-readable proof manifest for future audits diff --git a/INSTALL_FOR_EXPERT_RU.md b/INSTALL_FOR_EXPERT_RU.md index be7c91d..a955230 100644 --- a/INSTALL_FOR_EXPERT_RU.md +++ b/INSTALL_FOR_EXPERT_RU.md @@ -1,53 +1,8 @@ # Установка экземпляра для эксперта -Этот документ дает короткий воспроизводимый путь проверки экземпляра без -привязки к личному стенду разработчика. +Основной пошаговый документ находится здесь: -## 1. Подготовка +- [`docs/INSTALL_FOR_EXPERT_RU.md`](docs/INSTALL_FOR_EXPERT_RU.md) -1. Склонировать репозиторий. -2. Создать приватную конфигурацию: - - ```bash - cp private-config/deploy.env.example private-config/deploy.env - ``` - -3. Создать локальный Ansible inventory на основе: - - ```bash - cp ansible/inventory.example.ini ansible/inventory.ini - ``` - -4. Заполнить адреса, учетные данные и токены конкретного тестового стенда. - -## 2. Сборка - -```bash -cd adk-rust -cargo build --release --workspace -``` - -## 3. Проверки до установки - -```bash -scripts/quality-gate.sh -ansible-playbook --syntax-check -i ansible/inventory.ini ansible/deploy_aw_server.yml -``` - -## 4. Установка - -Базовый серверный путь описан в `docs/INSTALL_RU.md`. Конкретный playbook -выбирается по проверяемой схеме: серверный runtime, Windows collectors, -Grafana dashboards или портал оператора. - -## 5. Smoke-проверка - -После установки: - -```bash -detmir-check -detmir-status -``` - -Ожидаемый результат: статус `OK`, отсутствуют критичные service failures и -stale/dead buckets для обязательных источников. +Файл в корне оставлен как совместимая ссылка для внешних проверок и старых +закладок. diff --git a/SECURITY_OVERVIEW_RU.md b/SECURITY_OVERVIEW_RU.md index 5e66ac6..70c6f4d 100644 --- a/SECURITY_OVERVIEW_RU.md +++ b/SECURITY_OVERVIEW_RU.md @@ -21,7 +21,7 @@ - `` — основной `AW-rus` server, health, worktime/reporting, DLP server-side services, `Hayabusa` processing. - `` — operator/gateway host, Telegram bot, web gateway, часть `1C` analytics runtime. -- `` — `SHARKON2025`, Windows/RDP host с collector toolkit. +- `` — `HOST-EXAMPLE`, Windows/RDP host с collector toolkit. - `` — Grafana. - `` — `pfSense`, сетевой perimeter и VPN. diff --git a/adk-rust/RUNBOOK.md b/adk-rust/RUNBOOK.md index 94f57e9..32bafc2 100644 --- a/adk-rust/RUNBOOK.md +++ b/adk-rust/RUNBOOK.md @@ -954,7 +954,7 @@ systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg - read-only SQLite audit показал, что `/var/lib/activitywatch/aw-server-rust/sqlite.db` занимает около `6.8G`, `freelist_count=0`; VACUUM сам по себе не освободит место, потому что размер занят live events; - - основной источник роста: `aw-session-events_SHARKON2025` - около + - основной источник роста: `aw-session-events_HOST-EXAMPLE` - около `6.9M` строк и `~5GB` payload, с пиками `1.2M-2.4M` process-level событий в сутки за 2026-05-29..2026-06-01; - production live config на RDP был `pollSeconds=5` и @@ -967,7 +967,7 @@ systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg `C:\ProgramData\AWatch-rus\switch-backups\deployment-config.before-disable-process-events-20260602T061836Z.json`; - старый `worktime-session-collector.ps1` PID `8476` остановлен, collector поднят заново штатными `ActivityWatch Launch [...]` tasks/guard; - - delta-gate: `metadata.end` bucket `aw-session-events_SHARKON2025` + - delta-gate: `metadata.end` bucket `aw-session-events_HOST-EXAMPLE` остался `2026-06-02T06:27:11.197Z` через 75 секунд, постоянный поток остановлен; - базовый сбор не сломан: `detmir-check --json` OK, @@ -982,10 +982,10 @@ systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg - на время операции остановлены AW-related timers/services и `activitywatch-server.service`, чтобы не было writer'ов к SQLite; - scoped delete удалил только события bucket - `aw-session-events_SHARKON2025` с `eventType=process_start` или + `aw-session-events_HOST-EXAMPLE` с `eventType=process_start` или `eventType=process_stop`; - удалено `6,906,190` шумных process-level событий; - - сохранены logon events: после trim в `aw-session-events_SHARKON2025` + - сохранены logon events: после trim в `aw-session-events_HOST-EXAMPLE` осталось `174` события, recent samples имеют `eventType=logon`; - `PRAGMA integrity_check` до и после `VACUUM`: `ok`; - DB уменьшилась с `6.8G` до `350M`, rootfs AW server вернулся к @@ -1108,10 +1108,10 @@ systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg - `hardening-recovery.ps1` выполнен по existing `C:\ProgramData\AWatch-rus\deployment-config.json`; после recovery config остался `processEventsEnabled=false`, `logonEnabled=true`, - `pollSeconds=5`, users включают `SHARKON2025\Администратор`; - - exact task check: `ActivityWatch Launch [SHARKON2025_Администратор]` + `pollSeconds=5`, users включают `HOST-EXAMPLE\Администратор`; + - exact task check: `ActivityWatch Launch [HOST-EXAMPLE_Администратор]` существует, ошибочный - `ActivityWatch Launch [SHARKON2025_Administrator]` отсутствует, + `ActivityWatch Launch [HOST-EXAMPLE_Administrator]` отсутствует, `ActivityWatch Recovery` существует; - `AWatchRusCollectorGuard` running/automatic; Windows `validate-deployment.ps1` вернул `overallOk=True`; @@ -1308,7 +1308,7 @@ systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg `dlp_counts={ok:22,warn:0,fail:0}` and `ok_for_operator=true`. 46. `[done]` Перенести Proxmox DetMir contour smoke на Rust-first helper: - добавлен crate `aw-contour-smoke`; - - `scripts/aw-contour-smoke-.sh` теперь Rust-first wrapper: + - `scripts/aw-contour-smoke-gateway.sh` теперь Rust-first wrapper: ищет `AW_CONTOUR_SMOKE_RUST`, `$CARGO_TARGET_DIR/release/aw-contour-smoke`, `adk-rust/target/release/aw-contour-smoke`, @@ -1575,7 +1575,7 @@ systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg `.playwright-cli/page-2026-06-02T17-34-04-725Z.png`. - portal link repair after Grafana/gateway smoke: quick links now open in a separate tab, `worktime_report` points to explicit HTML - `/reports/worktime/management?format=html&host=SHARKON2025`, + `/reports/worktime/management?format=html&host=HOST-EXAMPLE`, `1С действия` is shown in the portal, and gateway `/r/aw-worktime` is pinned to the same HTML report. Production smoke: AW UI, Worktime, 1C brief, and 1C actions returned `200 text/html`; Grafana links correctly diff --git a/adk-rust/crates/aw-contour-smoke/src/main.rs b/adk-rust/crates/aw-contour-smoke/src/main.rs index 938c89e..7e25a34 100644 --- a/adk-rust/crates/aw-contour-smoke/src/main.rs +++ b/adk-rust/crates/aw-contour-smoke/src/main.rs @@ -116,7 +116,7 @@ fn run_proxmox_remote() -> Result { check_tcp(&mut counts, "nginx http", "127.0.0.1", 80); check_tcp(&mut counts, "nginx https", "127.0.0.1", 443); check_tcp(&mut counts, "proxmox web", "127.0.0.1", 8006); - check_tcp(&mut counts, "1C company API", "10.10.10.2", 8710); + check_tcp(&mut counts, "1C company API", "192.0.2.2", 8710); check_tcp(&mut counts, "clickhouse native", "127.0.0.1", 9000); check_tcp(&mut counts, "clickhouse http", "127.0.0.1", 8123); if let Ok(out) = command_output("ss", &["-tulpn"]) { @@ -141,7 +141,7 @@ fn run_proxmox_remote() -> Result { "go proxmox gui", "https://127.0.0.1/go/proxmox-gui", &[301, 302, 307, 308], - Some("https://10.10.10.2:8006/"), + Some("https://192.0.2.2:8006/"), ); check_http_redirect( &mut counts, @@ -149,7 +149,7 @@ fn run_proxmox_remote() -> Result { "go file1c brief", "https://127.0.0.1/go/file1c-brief", &[301, 302, 307, 308], - Some("http://10.10.10.2:8710/manager/brief"), + Some("http://192.0.2.2:8710/manager/brief"), ); check_http_redirect( &mut counts, @@ -157,7 +157,7 @@ fn run_proxmox_remote() -> Result { "go file1c actions", "https://127.0.0.1/go/file1c-actions", &[301, 302, 307, 308], - Some("http://10.10.10.2:8710/manager/actions"), + Some("http://192.0.2.2:8710/manager/actions"), ); section("1C Company API"); @@ -165,24 +165,24 @@ fn run_proxmox_remote() -> Result { &mut counts, &no_redirect_http, "1C root redirect", - "http://10.10.10.2:8710/", + "http://192.0.2.2:8710/", &[307], ); for (name, url) in [ - ("1C /health", "http://10.10.10.2:8710/health"), - ("1C /api/health", "http://10.10.10.2:8710/api/health"), - ("1C manager brief", "http://10.10.10.2:8710/manager/brief"), + ("1C /health", "http://192.0.2.2:8710/health"), + ("1C /api/health", "http://192.0.2.2:8710/api/health"), + ("1C manager brief", "http://192.0.2.2:8710/manager/brief"), ( "1C manager actions", - "http://10.10.10.2:8710/manager/actions", + "http://192.0.2.2:8710/manager/actions", ), ( "1C manager recovery", - "http://10.10.10.2:8710/manager/recovery", + "http://192.0.2.2:8710/manager/recovery", ), ( "1C weekly digest", - "http://10.10.10.2:8710/manager/digest/weekly", + "http://192.0.2.2:8710/manager/digest/weekly", ), ] { check_http_code(&mut counts, &http, name, url, &[200]); diff --git a/adk-rust/crates/aw-db-health/src/main.rs b/adk-rust/crates/aw-db-health/src/main.rs index 0a6e440..a3fef19 100644 --- a/adk-rust/crates/aw-db-health/src/main.rs +++ b/adk-rust/crates/aw-db-health/src/main.rs @@ -9,7 +9,7 @@ use serde::Serialize; use serde_json::{Value, json}; const DEFAULT_DB_PATH: &str = "/var/lib/activitywatch/aw-server-rust/sqlite.db"; -const DEFAULT_HOST: &str = "SHARKON2025"; +const DEFAULT_HOST: &str = "HOST-EXAMPLE"; #[derive(Debug, Parser)] #[command(author, version, about = "Read-only ActivityWatch SQLite growth guard")] diff --git a/adk-rust/crates/aw-db-maintenance/src/main.rs b/adk-rust/crates/aw-db-maintenance/src/main.rs index 6a03156..b590b49 100644 --- a/adk-rust/crates/aw-db-maintenance/src/main.rs +++ b/adk-rust/crates/aw-db-maintenance/src/main.rs @@ -11,7 +11,7 @@ use serde_json::Value; const DEFAULT_DB_PATH: &str = "/var/lib/activitywatch/aw-server-rust/sqlite.db"; const DEFAULT_BACKUP_DIR: &str = "/var/lib/activitywatch/backups/db"; -const DEFAULT_HOST: &str = "SHARKON2025"; +const DEFAULT_HOST: &str = "HOST-EXAMPLE"; const ALLOWED_EVENT_TYPES: &[&str] = &["process_start", "process_stop"]; #[derive(Debug, Parser)] diff --git a/adk-rust/crates/aw-health-check/src/main.rs b/adk-rust/crates/aw-health-check/src/main.rs index 4e01c57..077941d 100644 --- a/adk-rust/crates/aw-health-check/src/main.rs +++ b/adk-rust/crates/aw-health-check/src/main.rs @@ -472,14 +472,14 @@ mod tests { r#" # comment AW_EXPECT_START_OF_DAY="00:00" - AW_EXPECT_LANDINGPAGE=/#/activity/SHARKON2025/view/ + AW_EXPECT_LANDINGPAGE=/#/activity/HOST-EXAMPLE/view/ BAD KEY=value "#, ); assert_eq!(parsed.get("AW_EXPECT_START_OF_DAY").unwrap(), "00:00"); assert_eq!( parsed.get("AW_EXPECT_LANDINGPAGE").unwrap(), - "/#/activity/SHARKON2025/view/" + "/#/activity/HOST-EXAMPLE/view/" ); assert!(!parsed.contains_key("BAD KEY")); } diff --git a/adk-rust/crates/aw-linux-install/src/main.rs b/adk-rust/crates/aw-linux-install/src/main.rs index d225c69..d6ea7fd 100644 --- a/adk-rust/crates/aw-linux-install/src/main.rs +++ b/adk-rust/crates/aw-linux-install/src/main.rs @@ -24,7 +24,7 @@ struct Cli { #[arg(long)] legacy_script: PathBuf, - #[arg(long, default_value = "10.10.10.13")] + #[arg(long, default_value = "192.0.2.13")] server_host: String, #[arg(long, default_value = "5600")] @@ -249,7 +249,7 @@ mod tests { let cli = Cli { kind: InstallKind::Client, legacy_script: script, - server_host: "10.10.10.13".to_string(), + server_host: "192.0.2.13".to_string(), server_port: "5600".to_string(), poll_interval: "5".to_string(), version: "0.13.2".to_string(), diff --git a/adk-rust/crates/aw-rus-healthd/src/main.rs b/adk-rust/crates/aw-rus-healthd/src/main.rs index af0c75d..d12ad63 100644 --- a/adk-rust/crates/aw-rus-healthd/src/main.rs +++ b/adk-rust/crates/aw-rus-healthd/src/main.rs @@ -25,10 +25,10 @@ struct Cli { #[arg(long, default_value = "http://127.0.0.1:5610")] worktime_api: String, - #[arg(long, default_value = "192.168.100.18")] + #[arg(long, default_value = "198.51.100.18")] rdp_host: String, - #[arg(long, default_value = "SHARKON2025")] + #[arg(long, default_value = "HOST-EXAMPLE")] rdp_hostname: String, #[arg(long, default_value = "/var/lib/activitywatch/health")] diff --git a/adk-rust/crates/aw-slo-monitor/src/main.rs b/adk-rust/crates/aw-slo-monitor/src/main.rs index 3a7f77e..3af61d0 100644 --- a/adk-rust/crates/aw-slo-monitor/src/main.rs +++ b/adk-rust/crates/aw-slo-monitor/src/main.rs @@ -53,7 +53,7 @@ struct Cli { #[arg(long, default_value = "http://127.0.0.1:5610")] worktime_base: String, - #[arg(long, default_value = "SHARKON2025")] + #[arg(long, default_value = "HOST-EXAMPLE")] host: String, #[arg(long, default_value_t = 99.97)] diff --git a/adk-rust/crates/check-aw-data/src/main.rs b/adk-rust/crates/check-aw-data/src/main.rs index c8e60dd..adb44ac 100644 --- a/adk-rust/crates/check-aw-data/src/main.rs +++ b/adk-rust/crates/check-aw-data/src/main.rs @@ -7,8 +7,8 @@ use clap::Parser; use reqwest::blocking::Client; use serde_json::Value; -const DEFAULT_SERVER: &str = "http://10.10.10.13:5600"; -const DEFAULT_HOST: &str = "SHARKON2025"; +const DEFAULT_SERVER: &str = "http://192.0.2.13:5600"; +const DEFAULT_HOST: &str = "HOST-EXAMPLE"; const BUCKETS: &[&str] = &[ "aw-dlp-endpoint-signals", "aw-dlp-incidents", @@ -410,7 +410,7 @@ fn get_json(client: &Client, url: &str, _timeout_seconds: u64) -> Result } fn check_cors(_client: &Client, server: &str) -> u16 { - let origin = "http://10.10.10.13:5600"; + let origin = "http://192.0.2.13:5600"; let first = curl_status(&format!("{server}/api/0/settings/"), origin); if first == 200 { return first; @@ -532,7 +532,7 @@ mod tests { #[test] fn metadata_only_bucket_event_skips_missing_deep_event_read() { let index = serde_json::json!({ - "aw-watcher-window_SHARKON2025": { + "aw-watcher-window_HOST-EXAMPLE": { "metadata": { "end": "2026-06-02T00:00:00Z" } @@ -540,7 +540,7 @@ mod tests { }); let event = bucket_event( "http://127.0.0.1:1", - "aw-watcher-window_SHARKON2025", + "aw-watcher-window_HOST-EXAMPLE", Some(&index), false, 1, diff --git a/adk-rust/crates/check-aw-full/src/main.rs b/adk-rust/crates/check-aw-full/src/main.rs index 56e7c08..50bd964 100644 --- a/adk-rust/crates/check-aw-full/src/main.rs +++ b/adk-rust/crates/check-aw-full/src/main.rs @@ -7,9 +7,9 @@ use clap::Parser; use reqwest::blocking::Client; use serde_json::Value; -const DEFAULT_SERVER: &str = "http://10.10.10.13:5600"; -const DEFAULT_HOST: &str = "SHARKON2025"; -const DEFAULT_RDP_HOST: &str = "192.168.100.18"; +const DEFAULT_SERVER: &str = "http://192.0.2.13:5600"; +const DEFAULT_HOST: &str = "HOST-EXAMPLE"; +const DEFAULT_RDP_HOST: &str = "198.51.100.18"; const BUCKETS: &[(&str, &str)] = &[ ("aw-watcher-afk", "AFK watcher"), ("aw-watcher-window", "Window watcher"), @@ -423,7 +423,7 @@ fn check_cors(client: &Client, server: &str) -> u16 { let url = format!("{server}/api/0/settings/"); client .get(&url) - .header("Origin", "http://10.10.10.13:5600") + .header("Origin", "http://192.0.2.13:5600") .send() .map(|response| response.status().as_u16()) .unwrap_or(0) diff --git a/adk-rust/crates/check-install-kit-vs-repo/src/main.rs b/adk-rust/crates/check-install-kit-vs-repo/src/main.rs index 93f016c..4ba628b 100644 --- a/adk-rust/crates/check-install-kit-vs-repo/src/main.rs +++ b/adk-rust/crates/check-install-kit-vs-repo/src/main.rs @@ -270,9 +270,9 @@ mod tests { let kit = root.join("install-kit-awindows-20260427-211240"); fs::create_dir_all(&kit).unwrap(); fs::write(kit.join("unexpected.txt"), "kit").unwrap(); - fs::create_dir_all(kit.join("server-configs-192.168.100.18")).unwrap(); + fs::create_dir_all(kit.join("server-configs-198.51.100.18")).unwrap(); fs::write( - kit.join("server-configs-192.168.100.18/config.deployment-config.json"), + kit.join("server-configs-198.51.100.18/config.deployment-config.json"), "{}", ) .unwrap(); diff --git a/adk-rust/crates/detmir-check/src/main.rs b/adk-rust/crates/detmir-check/src/main.rs index 96c4d72..91ab69f 100644 --- a/adk-rust/crates/detmir-check/src/main.rs +++ b/adk-rust/crates/detmir-check/src/main.rs @@ -12,10 +12,10 @@ use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use serde::Serialize; use serde_json::Value; -const DEFAULT_AW_API: &str = "http://10.10.10.13:5600/api/0"; -const DEFAULT_WORKTIME_URL: &str = "http://10.10.10.13:5610"; -const DEFAULT_ONE_C_URL: &str = "http://10.10.10.2:8710"; -const DEFAULT_HOSTNAME: &str = "SHARKON2025"; +const DEFAULT_AW_API: &str = "http://192.0.2.13:5600/api/0"; +const DEFAULT_WORKTIME_URL: &str = "http://192.0.2.13:5610"; +const DEFAULT_ONE_C_URL: &str = "http://192.0.2.2:8710"; +const DEFAULT_HOSTNAME: &str = "HOST-EXAMPLE"; const DEFAULT_GATEWAY_HOST: &str = "detmir.example.local"; #[derive(Debug, Parser)] @@ -298,13 +298,13 @@ fn service_checks(args: &Cli) -> Vec { } checks.push(tcp_check( - "192.168.100.18", + "198.51.100.18", 5985, args.tcp_timeout_seconds, true, )); checks.push(tcp_check( - "192.168.100.18", + "198.51.100.18", 22, args.tcp_timeout_seconds, true, diff --git a/adk-rust/crates/detmir-dlp/src/main.rs b/adk-rust/crates/detmir-dlp/src/main.rs index 6448e99..07a0f66 100644 --- a/adk-rust/crates/detmir-dlp/src/main.rs +++ b/adk-rust/crates/detmir-dlp/src/main.rs @@ -4,7 +4,7 @@ use std::process::Command; use anyhow::{Context, Result}; use clap::Parser; -const DEFAULT_SSH_TARGET: &str = "igor@10.10.10.13"; +const DEFAULT_SSH_TARGET: &str = "igor@192.0.2.13"; const DEFAULT_REMOTE_COMMAND: &str = "sudo -n /usr/local/bin/dlp-health-check --json"; #[derive(Debug, Parser)] diff --git a/adk-rust/crates/detmir-grafana-check/src/main.rs b/adk-rust/crates/detmir-grafana-check/src/main.rs index 09e1334..ff6d459 100644 --- a/adk-rust/crates/detmir-grafana-check/src/main.rs +++ b/adk-rust/crates/detmir-grafana-check/src/main.rs @@ -12,7 +12,7 @@ use serde_json::{Value, json}; const DEFAULT_GRAFANA_URL: &str = "http://127.0.0.1:3000"; const DEFAULT_DASHBOARD_UID: &str = "detmir-aw-main"; const DEFAULT_DASHBOARD_FILE: &str = "/etc/grafana/provisioning/dashboards/aw/detmir-aw-main.json"; -const DEFAULT_HOST: &str = "SHARKON2025"; +const DEFAULT_HOST: &str = "HOST-EXAMPLE"; const OLD_MEASUREMENTS: &[&str] = &["aw_window_event", "aw_afk_event"]; const REQUIRED_MEASUREMENTS: &[&str] = &[ "aw_rdp_worktime_hourly", @@ -670,9 +670,9 @@ mod tests { assert_eq!( render_query_vars( "r.host == \"${host}\" or r.host == \"$host\"", - "SHARKON2025" + "HOST-EXAMPLE" ), - "r.host == \"SHARKON2025\" or r.host == \"SHARKON2025\"" + "r.host == \"HOST-EXAMPLE\" or r.host == \"HOST-EXAMPLE\"" ); } diff --git a/adk-rust/crates/detmir-heal-safe/src/main.rs b/adk-rust/crates/detmir-heal-safe/src/main.rs index 6fa072a..14fea1b 100644 --- a/adk-rust/crates/detmir-heal-safe/src/main.rs +++ b/adk-rust/crates/detmir-heal-safe/src/main.rs @@ -6,7 +6,7 @@ use clap::Parser; use detmir_core::{exit_codes, now_utc_rfc3339}; use serde::Serialize; -const DEFAULT_SSH_TARGET: &str = "igor@10.10.10.13"; +const DEFAULT_SSH_TARGET: &str = "igor@192.0.2.13"; const REQUIRED_UNITS: &[&str] = &[ "activitywatch-server.service", "aw-worktime-api.service", diff --git a/adk-rust/crates/detmir-portal/src/main.rs b/adk-rust/crates/detmir-portal/src/main.rs index 84ac478..f1b2e34 100644 --- a/adk-rust/crates/detmir-portal/src/main.rs +++ b/adk-rust/crates/detmir-portal/src/main.rs @@ -52,14 +52,14 @@ struct Cli { #[arg( long, - default_value = "http://10.10.10.13:5610", + default_value = "http://192.0.2.13:5610", env = "DETMIR_PORTAL_WORKTIME_URL" )] worktime_url: String, #[arg( long, - default_value = "http://10.10.10.2:8710", + default_value = "http://192.0.2.2:8710", env = "DETMIR_PORTAL_ONE_C_URL" )] one_c_url: String, @@ -1836,7 +1836,7 @@ fn links() -> PortalLinks { portal: "/portal/".to_string(), grafana_dashboards: "/dashboards".to_string(), detmir_activitywatch: - "/d/detmir-aw-main/detmir-activitywatch?orgId=1&from=now-48h&to=now&timezone=browser&var-host=SHARKON2025&refresh=5m" + "/d/detmir-aw-main/detmir-activitywatch?orgId=1&from=now-48h&to=now&timezone=browser&var-host=HOST-EXAMPLE&refresh=5m" .to_string(), dlp_security_dashboard: "/d/detmir-dlp-security?orgId=1&from=now-30d&to=now&timezone=browser".to_string(), @@ -1845,7 +1845,7 @@ fn links() -> PortalLinks { dlp_overview_dashboard: "/d/awatch-dlp-overview?orgId=1&from=now-30d&to=now&timezone=browser".to_string(), aw_ui: "/r/aw/".to_string(), - worktime_report: "/reports/worktime/management?format=html&host=SHARKON2025".to_string(), + worktime_report: "/reports/worktime/management?format=html&host=HOST-EXAMPLE".to_string(), file1c_brief: "/r/file1c/brief".to_string(), file1c_actions: "/r/file1c/actions".to_string(), } diff --git a/adk-rust/crates/diag-and-manual-restart/src/main.rs b/adk-rust/crates/diag-and-manual-restart/src/main.rs index a814aec..b21bc29 100644 --- a/adk-rust/crates/diag-and-manual-restart/src/main.rs +++ b/adk-rust/crates/diag-and-manual-restart/src/main.rs @@ -136,20 +136,20 @@ fn seed_server_dlp_events(inventory: &str) -> Result<()> { let ts = Utc::now().format("%Y-%m-%dT%H:%M:%SZ"); let script = format!( r#"cat >/tmp/aw-endpoint-seed.json <<'JSON' -{{"timestamp":"{ts}","duration":0.0,"data":{{"hostname":"SHARKON2025","signalType":"self_test","source":"diag_and_manual_restart","username":"system","queueDepth":0,"eventsEnqueued":0,"eventsFlushed":0,"sendFailures":0}}}} +{{"timestamp":"{ts}","duration":0.0,"data":{{"hostname":"HOST-EXAMPLE","signalType":"self_test","source":"diag_and_manual_restart","username":"system","queueDepth":0,"eventsEnqueued":0,"eventsFlushed":0,"sendFailures":0}}}} JSON cat >/tmp/aw-fileops-seed-host.json <<'JSON' -{{"timestamp":"{ts}","duration":0.0,"data":{{"hostname":"SHARKON2025","operation":"self_test","source":"diag_and_manual_restart"}}}} +{{"timestamp":"{ts}","duration":0.0,"data":{{"hostname":"HOST-EXAMPLE","operation":"self_test","source":"diag_and_manual_restart"}}}} JSON cat >/tmp/aw-fileops-seed-server.json <<'JSON' -{{"timestamp":"{ts}","duration":0.0,"data":{{"hostname":"10.10.10.13","operation":"self_test","source":"diag_and_manual_restart"}}}} +{{"timestamp":"{ts}","duration":0.0,"data":{{"hostname":"192.0.2.13","operation":"self_test","source":"diag_and_manual_restart"}}}} JSON -curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-dlp-endpoint-signals_SHARKON2025' -H 'Content-Type: application/json' -d '{{"client":"aw-dlp-endpoint-signals","type":"aw.dlp.endpoint.signal","hostname":"SHARKON2025"}}' >/dev/null 2>&1 || true -curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_SHARKON2025' -H 'Content-Type: application/json' -d '{{"client":"aw-file-operations","type":"aw.file.operation","hostname":"SHARKON2025"}}' >/dev/null 2>&1 || true -curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_10.10.10.13' -H 'Content-Type: application/json' -d '{{"client":"aw-file-operations","type":"aw.file.operation","hostname":"10.10.10.13"}}' >/dev/null 2>&1 || true -curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-dlp-endpoint-signals_SHARKON2025/heartbeat?pulsetime=30' -H 'Content-Type: application/json' --data-binary @/tmp/aw-endpoint-seed.json >/dev/null -curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_SHARKON2025/heartbeat?pulsetime=30' -H 'Content-Type: application/json' --data-binary @/tmp/aw-fileops-seed-host.json >/dev/null -curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_10.10.10.13/heartbeat?pulsetime=30' -H 'Content-Type: application/json' --data-binary @/tmp/aw-fileops-seed-server.json >/dev/null +curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-dlp-endpoint-signals_HOST-EXAMPLE' -H 'Content-Type: application/json' -d '{{"client":"aw-dlp-endpoint-signals","type":"aw.dlp.endpoint.signal","hostname":"HOST-EXAMPLE"}}' >/dev/null 2>&1 || true +curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_HOST-EXAMPLE' -H 'Content-Type: application/json' -d '{{"client":"aw-file-operations","type":"aw.file.operation","hostname":"HOST-EXAMPLE"}}' >/dev/null 2>&1 || true +curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_192.0.2.13' -H 'Content-Type: application/json' -d '{{"client":"aw-file-operations","type":"aw.file.operation","hostname":"192.0.2.13"}}' >/dev/null 2>&1 || true +curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-dlp-endpoint-signals_HOST-EXAMPLE/heartbeat?pulsetime=30' -H 'Content-Type: application/json' --data-binary @/tmp/aw-endpoint-seed.json >/dev/null +curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_HOST-EXAMPLE/heartbeat?pulsetime=30' -H 'Content-Type: application/json' --data-binary @/tmp/aw-fileops-seed-host.json >/dev/null +curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_192.0.2.13/heartbeat?pulsetime=30' -H 'Content-Type: application/json' --data-binary @/tmp/aw-fileops-seed-server.json >/dev/null "# ); let _ = ansible_command( @@ -170,7 +170,7 @@ fn restart_windows_collectors(inventory: &str) { fn seed_windows_dlp_events(inventory: &str) { log("Seeding endpoint/file-ops events from aw_windows..."); - let script = r#"powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $ts = (Get-Date).ToUniversalTime().ToString('o'); $api='http://10.10.10.13:5600/api/0'; $endpoint=@{timestamp=$ts;duration=0.0;data=@{hostname='SHARKON2025';signalType='self_test';source='diag_and_manual_restart';username=$env:USERNAME;queueDepth=0;eventsEnqueued=0;eventsFlushed=0;sendFailures=0}} | ConvertTo-Json -Depth 8 -Compress; $fileops=@{timestamp=$ts;duration=0.0;data=@{hostname='SHARKON2025';operation='self_test';source='diag_and_manual_restart';username=$env:USERNAME}} | ConvertTo-Json -Depth 8 -Compress; Invoke-RestMethod -Method Post -Uri $api'/buckets/aw-dlp-endpoint-signals_SHARKON2025' -ContentType 'application/json' -Body '{\"client\":\"aw-dlp-endpoint-signals\",\"type\":\"aw.dlp.endpoint.signal\",\"hostname\":\"SHARKON2025\"}' -TimeoutSec 15 -DisableKeepAlive -ErrorAction SilentlyContinue | Out-Null; Invoke-RestMethod -Method Post -Uri $api'/buckets/aw-file-operations_SHARKON2025' -ContentType 'application/json' -Body '{\"client\":\"aw-file-operations\",\"type\":\"aw.file.operation\",\"hostname\":\"SHARKON2025\"}' -TimeoutSec 15 -DisableKeepAlive -ErrorAction SilentlyContinue | Out-Null; Invoke-RestMethod -Method Post -Uri $api'/buckets/aw-dlp-endpoint-signals_SHARKON2025/heartbeat?pulsetime=30' -ContentType 'application/json' -Body $endpoint -TimeoutSec 15 -DisableKeepAlive | Out-Null; Invoke-RestMethod -Method Post -Uri $api'/buckets/aw-file-operations_SHARKON2025/heartbeat?pulsetime=30' -ContentType 'application/json' -Body $fileops -TimeoutSec 15 -DisableKeepAlive | Out-Null; Write-Output 'windows-dlp-seeded'""#; + let script = r#"powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $ts = (Get-Date).ToUniversalTime().ToString('o'); $api='http://192.0.2.13:5600/api/0'; $endpoint=@{timestamp=$ts;duration=0.0;data=@{hostname='HOST-EXAMPLE';signalType='self_test';source='diag_and_manual_restart';username=$env:USERNAME;queueDepth=0;eventsEnqueued=0;eventsFlushed=0;sendFailures=0}} | ConvertTo-Json -Depth 8 -Compress; $fileops=@{timestamp=$ts;duration=0.0;data=@{hostname='HOST-EXAMPLE';operation='self_test';source='diag_and_manual_restart';username=$env:USERNAME}} | ConvertTo-Json -Depth 8 -Compress; Invoke-RestMethod -Method Post -Uri $api'/buckets/aw-dlp-endpoint-signals_HOST-EXAMPLE' -ContentType 'application/json' -Body '{\"client\":\"aw-dlp-endpoint-signals\",\"type\":\"aw.dlp.endpoint.signal\",\"hostname\":\"HOST-EXAMPLE\"}' -TimeoutSec 15 -DisableKeepAlive -ErrorAction SilentlyContinue | Out-Null; Invoke-RestMethod -Method Post -Uri $api'/buckets/aw-file-operations_HOST-EXAMPLE' -ContentType 'application/json' -Body '{\"client\":\"aw-file-operations\",\"type\":\"aw.file.operation\",\"hostname\":\"HOST-EXAMPLE\"}' -TimeoutSec 15 -DisableKeepAlive -ErrorAction SilentlyContinue | Out-Null; Invoke-RestMethod -Method Post -Uri $api'/buckets/aw-dlp-endpoint-signals_HOST-EXAMPLE/heartbeat?pulsetime=30' -ContentType 'application/json' -Body $endpoint -TimeoutSec 15 -DisableKeepAlive | Out-Null; Invoke-RestMethod -Method Post -Uri $api'/buckets/aw-file-operations_HOST-EXAMPLE/heartbeat?pulsetime=30' -ContentType 'application/json' -Body $fileops -TimeoutSec 15 -DisableKeepAlive | Out-Null; Write-Output 'windows-dlp-seeded'""#; let _ = ansible_windows_shell(inventory, script); } diff --git a/adk-rust/crates/dlp-health-check/src/main.rs b/adk-rust/crates/dlp-health-check/src/main.rs index 4e216ec..9fbf799 100644 --- a/adk-rust/crates/dlp-health-check/src/main.rs +++ b/adk-rust/crates/dlp-health-check/src/main.rs @@ -1513,18 +1513,18 @@ mod tests { #[test] fn transport_counter_key_separates_sessions() { let data = json!({ - "hostname": "SHARKON2025", + "hostname": "HOST-EXAMPLE", "sessionId": 4, "username": "USER4" }); assert_eq!( transport_counter_key( "file-operations", - "aw-file-operations_SHARKON2025", + "aw-file-operations_HOST-EXAMPLE", &data, "sendFailures" ), - "file-operations:aw-file-operations_SHARKON2025:SHARKON2025:4:USER4:sendFailures" + "file-operations:aw-file-operations_HOST-EXAMPLE:HOST-EXAMPLE:4:USER4:sendFailures" ); } diff --git a/adk-rust/crates/dlp-influx-exporter/src/main.rs b/adk-rust/crates/dlp-influx-exporter/src/main.rs index aacefc1..5c5c72a 100644 --- a/adk-rust/crates/dlp-influx-exporter/src/main.rs +++ b/adk-rust/crates/dlp-influx-exporter/src/main.rs @@ -11,7 +11,7 @@ const DEFAULT_AW_API_BASE: &str = "http://127.0.0.1:5600/api/0"; const DEFAULT_CASE_API_BASE: &str = "http://127.0.0.1:5602/api/0/dlp/cases"; const DEFAULT_INFLUX_ORG: &str = "proxmox"; const DEFAULT_INFLUX_BUCKET: &str = "aw_metrics"; -const DEFAULT_HOSTS: &str = "SHARKON2025"; +const DEFAULT_HOSTS: &str = "HOST-EXAMPLE"; #[derive(Debug, Parser)] #[command(about = "AW DLP InfluxDB exporter")] @@ -1009,7 +1009,7 @@ mod tests { "id": 10, "timestamp": "2026-05-15T10:00:00Z", "data": { - "hostname": "SHARKON2025", + "hostname": "HOST-EXAMPLE", "username": "Администратор", "signalType": "self_test", "policyMode": "server", @@ -1025,7 +1025,7 @@ mod tests { "id": 11, "timestamp": "2026-05-15T10:01:00Z", "data": { - "hostname": "SHARKON2025", + "hostname": "HOST-EXAMPLE", "username": "Администратор", "signalType": "print_job", "source": "endpoint-signals-phase2", @@ -1034,7 +1034,7 @@ mod tests { } }), ]; - let lines = build_endpoint_lines("SHARKON2025", &events); + let lines = build_endpoint_lines("HOST-EXAMPLE", &events); assert!( lines .iter() @@ -1048,13 +1048,13 @@ mod tests { let item = json!({ "timestamp": "2026-05-15T10:02:00Z", "data": { - "host": "SHARKON2025", + "host": "HOST-EXAMPLE", "incident": {"status": "open", "verdict": "incident"}, - "sourceBucket": "aw-dlp-endpoint-signals_SHARKON2025", + "sourceBucket": "aw-dlp-endpoint-signals_HOST-EXAMPLE", "sourceEvent": { "data": { "signalType": "print_job", - "hostname": "SHARKON2025", + "hostname": "HOST-EXAMPLE", "username": "Администратор", "documentName": "Письмо", "printerName": "HP", @@ -1063,7 +1063,7 @@ mod tests { } } }); - let normalized = normalize_incident(&item, "SHARKON2025"); + let normalized = normalize_incident(&item, "HOST-EXAMPLE"); assert_eq!(normalized.signal_type, "print_job"); assert_eq!(normalized.username, "Администратор"); assert_eq!(normalized.action, "incident"); @@ -1074,7 +1074,7 @@ mod tests { fn case_lines_emit_case_state() { let cases = vec![json!({ "id": 28, - "host": "SHARKON2025", + "host": "HOST-EXAMPLE", "status": "open", "severity": "medium", "assignee": null, @@ -1084,7 +1084,7 @@ mod tests { "forensics": null, "updated_at": "2026-05-15T10:03:00+00:00" })]; - let lines = build_case_lines("SHARKON2025", &cases); + let lines = build_case_lines("HOST-EXAMPLE", &cases); assert_eq!(lines.len(), 1); assert!(lines[0].starts_with("aw_dlp_case,")); } diff --git a/adk-rust/crates/prod-backup-restore/src/main.rs b/adk-rust/crates/prod-backup-restore/src/main.rs index ff465ab..344555f 100644 --- a/adk-rust/crates/prod-backup-restore/src/main.rs +++ b/adk-rust/crates/prod-backup-restore/src/main.rs @@ -8,9 +8,9 @@ use chrono::Local; use clap::Parser; use serde::Serialize; -const DEFAULT_SERVER_HOST: &str = "10.10.10.13"; +const DEFAULT_SERVER_HOST: &str = "192.0.2.13"; const DEFAULT_SERVER_USER: &str = "igor"; -const DEFAULT_LEGACY_DB: &str = "/root/.local/share/activitywatch/aw-server-rust/sqlite.db"; +const DEFAULT_LEGACY_DB: &str = "/var/lib/activitywatch/aw-server-rust/sqlite.db"; const DEFAULT_TARGET_DB: &str = "/var/lib/activitywatch/.local/share/activitywatch/aw-server-rust/sqlite.db"; const DEFAULT_REMOTE_MERGE_BIN: &str = "/tmp/merge-aw-server-dbs"; @@ -454,7 +454,7 @@ mod tests { dir.path(), &dir.path().join("private-config/runtime.env"), &env, - "10.10.10.13", + "192.0.2.13", "igor", "20260602-000000", "/var/lib/activitywatch/backups/prod-restore-20260602-000000", diff --git a/adk-rust/crates/rdp-worktime-report/src/main.rs b/adk-rust/crates/rdp-worktime-report/src/main.rs index 3649e6a..a6dc770 100644 --- a/adk-rust/crates/rdp-worktime-report/src/main.rs +++ b/adk-rust/crates/rdp-worktime-report/src/main.rs @@ -10,8 +10,8 @@ use reqwest::blocking::Client; use serde::{Deserialize, Serialize}; use serde_json::Value; -const DEFAULT_AW_BASE_URL: &str = "http://10.10.10.13:5600/api/0"; -const DEFAULT_HOST: &str = "SHARKON2025"; +const DEFAULT_AW_BASE_URL: &str = "http://192.0.2.13:5600/api/0"; +const DEFAULT_HOST: &str = "HOST-EXAMPLE"; const DEFAULT_SAMPLE_SECONDS: f64 = 30.0; const DEFAULT_MAX_SAMPLE_SECONDS: f64 = 300.0; const DEFAULT_OUT_DIR: &str = "reports"; @@ -550,7 +550,7 @@ mod tests { ]; let rows = build_rows( events, - "SHARKON2025", + "HOST-EXAMPLE", NaiveDate::from_ymd_opt(2026, 6, 1).unwrap(), NaiveDate::from_ymd_opt(2026, 6, 1).unwrap(), 30.0, @@ -563,7 +563,7 @@ mod tests { assert_eq!(rows[0].sessions_count, 2); assert_eq!(rows[0].samples_count, 3); assert_eq!(rows[0].active_samples, 2); - assert_eq!(rows[0].user_id, "SHARKON2025\\user5"); + assert_eq!(rows[0].user_id, "HOST-EXAMPLE\\user5"); } #[test] diff --git a/adk-rust/crates/tsj-guardian-status/src/main.rs b/adk-rust/crates/tsj-guardian-status/src/main.rs index 31f3fd0..25a5422 100644 --- a/adk-rust/crates/tsj-guardian-status/src/main.rs +++ b/adk-rust/crates/tsj-guardian-status/src/main.rs @@ -388,7 +388,7 @@ fn suggestions_from_failures(failures: &[String]) -> Vec { } if text.contains("pfsense_web") { suggestions - .push("Проверить доступность pfSense 10.10.10.1:8443, перезапустить WebGUI/nginx."); + .push("Проверить доступность pfSense 192.0.2.1:8443, перезапустить WebGUI/nginx."); } if text.contains("pfsense_mcp") { suggestions.push("Проверить локальный pfsense-mcp-server.service, bearer token и endpoint 127.0.0.1:3010/mcp."); @@ -397,18 +397,18 @@ fn suggestions_from_failures(failures: &[String]) -> Vec { suggestions.push("Проверить контейнер InfluxDB и restart сервиса influxdb."); } if text.contains("grafana") { - suggestions.push("Проверить grafana-server и NO_PROXY для 10.10.10.0/24."); + suggestions.push("Проверить grafana-server и NO_PROXY для 192.0.2.0/24."); } if text.contains("loki") || text.contains("alloy") { suggestions.push("Проверить LXC логов и restart сервисов loki/alloy."); } if text.contains("aw-rus:watcher-") || text.contains("aw-rus:worktime:") { - suggestions.push("Проверить Windows collector recovery: worktime-session-collector, ActivityWatch Recovery и Launch tasks на 192.168.100.18."); + suggestions.push("Проверить Windows collector recovery: worktime-session-collector, ActivityWatch Recovery и Launch tasks на 198.51.100.18."); suggestions.push("После Windows recovery проверить server-side aw-worktime-autoheal/ui-bridge для пересборки afk/window bucket'ов."); } if text.contains("aw-rus:dlp-") { suggestions.push( - "Проверить DLP endpoint/fileops collectors и server-side DLP transport на 10.10.10.13.", + "Проверить DLP endpoint/fileops collectors и server-side DLP transport на 192.0.2.13.", ); } if text.contains("filesystem_usage") { diff --git a/adk-rust/crates/tsj-guardian-watchdog/src/main.rs b/adk-rust/crates/tsj-guardian-watchdog/src/main.rs index e100d43..aa6b6d4 100644 --- a/adk-rust/crates/tsj-guardian-watchdog/src/main.rs +++ b/adk-rust/crates/tsj-guardian-watchdog/src/main.rs @@ -11,7 +11,8 @@ use serde::Serialize; const DEFAULT_HEARTBEAT_FILE: &str = "/opt/infra-admin/.state/tsj_guardian_heartbeat"; const DEFAULT_SERVICE_NAME: &str = "tsj-guardian-bot.service"; const DEFAULT_GOST_SERVICE_NAME: &str = "gost-tg.service"; -const DEFAULT_GOST_PATTERN: &str = "/usr/local/bin/gost -L http+socks5://127.0.0.1:11090 -F socks5+wss://gw.example.local:4443"; +const DEFAULT_GOST_PATTERN: &str = + "/usr/local/bin/gost -L http+socks5://127.0.0.1:11090 -F socks5+wss://gw.example.local:4443"; #[derive(Debug, Parser)] #[command(about = "TSJ Guardian bot heartbeat watchdog and gost duplicate guard.")] diff --git a/adk-rust/crates/worktime-api/src/main.rs b/adk-rust/crates/worktime-api/src/main.rs index 1530572..5c70aa3 100644 --- a/adk-rust/crates/worktime-api/src/main.rs +++ b/adk-rust/crates/worktime-api/src/main.rs @@ -22,7 +22,7 @@ use tiny_http::{Header, Method, Request, Response, Server, StatusCode}; use url::form_urlencoded; const DEFAULT_AW_URL: &str = "http://127.0.0.1:5600"; -const DEFAULT_HOST: &str = "SHARKON2025"; +const DEFAULT_HOST: &str = "HOST-EXAMPLE"; #[derive(Debug, Parser)] #[command(about = "AW Worktime Report API")] @@ -2536,7 +2536,7 @@ mod tests { ], start, end, - "SHARKON2025", + "HOST-EXAMPLE", false, ); assert_eq!(rows[0]["active_seconds"], 60); diff --git a/adk-rust/crates/worktime-autoheal/src/main.rs b/adk-rust/crates/worktime-autoheal/src/main.rs index e47f7a9..e4644a1 100644 --- a/adk-rust/crates/worktime-autoheal/src/main.rs +++ b/adk-rust/crates/worktime-autoheal/src/main.rs @@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; const DEFAULT_AW_URL: &str = "http://127.0.0.1:5600"; -const DEFAULT_HOST: &str = "SHARKON2025"; +const DEFAULT_HOST: &str = "HOST-EXAMPLE"; const AUTOHEAL_SOURCE: &str = "aw-worktime-autoheal"; #[derive(Debug, Parser)] diff --git a/adk-rust/crates/worktime-influx-exporter/src/main.rs b/adk-rust/crates/worktime-influx-exporter/src/main.rs index c6cb560..dbf4ef6 100644 --- a/adk-rust/crates/worktime-influx-exporter/src/main.rs +++ b/adk-rust/crates/worktime-influx-exporter/src/main.rs @@ -11,7 +11,7 @@ const DEFAULT_AW_BASE: &str = "http://127.0.0.1:5600"; const DEFAULT_WORKTIME_REPORT_BASE: &str = "http://127.0.0.1:5610"; const DEFAULT_INFLUX_ORG: &str = "proxmox"; const DEFAULT_INFLUX_BUCKET: &str = "aw_metrics"; -const DEFAULT_HOST: &str = "SHARKON2025"; +const DEFAULT_HOST: &str = "HOST-EXAMPLE"; const DEFAULT_DAYS: &str = "today,yesterday"; #[derive(Debug, Parser)] @@ -993,14 +993,14 @@ mod tests { fn heartbeat_line_uses_current_exporter_timestamp() { let out = line( "aw_worktime_exporter_heartbeat", - vec![("host", "SHARKON2025".to_string())], + vec![("host", "HOST-EXAMPLE".to_string())], vec![("run", FieldValue::Int(1))], 42, ) .unwrap(); assert_eq!( out, - "aw_worktime_exporter_heartbeat,host=SHARKON2025 run=1i 42" + "aw_worktime_exporter_heartbeat,host=HOST-EXAMPLE run=1i 42" ); } @@ -1023,11 +1023,11 @@ mod tests { } })]; - let daily = aggregate_daily_rows(&events, start, end_exclusive, "SHARKON2025", &config); - let hourly = aggregate_hourly_rows(&events, end_exclusive, "SHARKON2025", &config); + let daily = aggregate_daily_rows(&events, start, end_exclusive, "HOST-EXAMPLE", &config); + let hourly = aggregate_hourly_rows(&events, end_exclusive, "HOST-EXAMPLE", &config); assert_eq!(daily.len(), 1); - assert_eq!(daily[0].user_id, "SHARKON2025\\user5"); + assert_eq!(daily[0].user_id, "HOST-EXAMPLE\\user5"); assert_eq!(daily[0].active_seconds, 300); assert_eq!(daily[0].active_samples, 1); assert_eq!(hourly.len(), 1); diff --git a/adk-rust/crates/worktime-prewarm/src/main.rs b/adk-rust/crates/worktime-prewarm/src/main.rs index f57f7a8..a31b3be 100644 --- a/adk-rust/crates/worktime-prewarm/src/main.rs +++ b/adk-rust/crates/worktime-prewarm/src/main.rs @@ -7,7 +7,7 @@ use reqwest::blocking::Client; use serde::Serialize; const DEFAULT_BASE_URL: &str = "http://127.0.0.1:5610"; -const DEFAULT_HOST: &str = "SHARKON2025"; +const DEFAULT_HOST: &str = "HOST-EXAMPLE"; #[derive(Debug, Parser)] #[command(about = "AW Worktime report cache prewarm")] diff --git a/adk-rust/crates/worktime-ui-bridge/src/main.rs b/adk-rust/crates/worktime-ui-bridge/src/main.rs index f4cd608..528b3c3 100644 --- a/adk-rust/crates/worktime-ui-bridge/src/main.rs +++ b/adk-rust/crates/worktime-ui-bridge/src/main.rs @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; const DEFAULT_AW_URL: &str = "http://127.0.0.1:5600"; -const DEFAULT_HOST: &str = "SHARKON2025"; +const DEFAULT_HOST: &str = "HOST-EXAMPLE"; const DEFAULT_STATE_PATH: &str = "/var/lib/activitywatch/aw-worktime-ui-bridge-state.json"; const BRIDGE_SOURCE: &str = "aw-worktime-ui-bridge"; diff --git a/ansible/deploy_aw_server.yml b/ansible/deploy_aw_server.yml index 6a81578..3b26a60 100644 --- a/ansible/deploy_aw_server.yml +++ b/ansible/deploy_aw_server.yml @@ -451,9 +451,9 @@ - { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "{{ aw_server_webui_dir }}/js/aw-worktime-panel.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 + - name: Создать каталог /opt/detmir/bootstrap для apply_webui_ru_patch.sh ansible.builtin.file: - path: /root/bootstrap + path: /opt/detmir/bootstrap state: directory mode: "0755" @@ -463,10 +463,10 @@ 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-worktime-panel.js", dest: "/root/bootstrap/aw-worktime-panel.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/opt/detmir/bootstrap/aw-ru-patch.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/opt/detmir/bootstrap/aw-sw-cleanup.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "/opt/detmir/bootstrap/aw-worktime-panel.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/opt/detmir/bootstrap/aw-host-groups.json", mode: "0644" } - name: Скопировать apply_webui_ru_patch.sh скрипт ansible.builtin.copy: @@ -655,14 +655,14 @@ AW_WORKTIME_INFLUX_URL={{ aw_worktime_influx_url | default('') }} AW_WORKTIME_INFLUX_ORG={{ aw_worktime_influx_org | default('proxmox') }} AW_WORKTIME_INFLUX_BUCKET={{ aw_worktime_influx_bucket | default('aw_metrics') }} - AW_WORKTIME_INFLUX_HOSTS={{ aw_worktime_influx_hosts | default('SHARKON2025') }} + AW_WORKTIME_INFLUX_HOSTS={{ aw_worktime_influx_hosts | default('HOST-EXAMPLE') }} AW_WORKTIME_INFLUX_DAYS={{ aw_worktime_influx_days | default('today,yesterday') }} AW_WORKTIME_INFLUX_TOKEN={{ aw_effective_worktime_influx_token | default('') }} AW_DLP_INFLUX_ENABLED={{ 'true' if (aw_dlp_influx_enabled | default(false) | bool) else 'false' }} AW_DLP_INFLUX_URL={{ aw_dlp_influx_url | default('') }} AW_DLP_INFLUX_ORG={{ aw_dlp_influx_org | default('proxmox') }} AW_DLP_INFLUX_BUCKET={{ aw_dlp_influx_bucket | default('aw_metrics') }} - AW_DLP_INFLUX_HOSTS={{ aw_dlp_influx_hosts | default('SHARKON2025') }} + AW_DLP_INFLUX_HOSTS={{ aw_dlp_influx_hosts | default('HOST-EXAMPLE') }} AW_DLP_INFLUX_LOOKBACK_DAYS={{ aw_dlp_influx_lookback_days | default(30) }} AW_DLP_INFLUX_EVENT_LIMIT={{ aw_dlp_influx_event_limit | default(2000) }} AW_DLP_INFLUX_TOKEN={{ aw_effective_dlp_influx_token | default('') }} @@ -1748,7 +1748,7 @@ - name: Проверить наличие legacy root DB ansible.builtin.stat: - path: /root/.local/share/activitywatch/aw-server-rust/sqlite.db + path: /var/lib/activitywatch/aw-server-rust/sqlite.db register: aw_legacy_root_db - name: Проверить наличие target DB @@ -1791,7 +1791,7 @@ - name: Backup legacy root DB перед merge ansible.builtin.copy: remote_src: true - src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db + src: /var/lib/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 }}" @@ -1805,7 +1805,7 @@ argv: - /usr/local/bin/merge-aw-server-dbs - --base - - /root/.local/share/activitywatch/aw-server-rust/sqlite.db + - /var/lib/activitywatch/aw-server-rust/sqlite.db - --overlay - "{{ aw_server_db_path }}" - --output @@ -1843,7 +1843,7 @@ - name: Скопировать legacy root DB в target DB если target ещё не существует ansible.builtin.copy: remote_src: true - src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db + src: /var/lib/activitywatch/aw-server-rust/sqlite.db dest: "{{ aw_server_db_path }}" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" diff --git a/ansible/deploy_aw_windows.yml b/ansible/deploy_aw_windows.yml index 4a1734c..14096d7 100644 --- a/ansible/deploy_aw_windows.yml +++ b/ansible/deploy_aw_windows.yml @@ -13,7 +13,7 @@ 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_domain: "HOST-EXAMPLE" aw_windows_builtin_administrator_name: "Администратор" aw_windows_users: - Администратор diff --git a/ansible/deploy_detmir_portal.yml b/ansible/deploy_detmir_portal.yml index f7abf01..b39c2b3 100644 --- a/ansible/deploy_detmir_portal.yml +++ b/ansible/deploy_detmir_portal.yml @@ -42,8 +42,8 @@ DETMIR_PORTAL_STATUS_CMD=detmir-status --json DETMIR_PORTAL_CHECK_CMD=detmir-check --json DETMIR_PORTAL_FAILED_UNITS_CMD=systemctl --failed --no-pager - DETMIR_PORTAL_WORKTIME_URL=http://10.10.10.13:5610 - DETMIR_PORTAL_ONE_C_URL=http://10.10.10.2:8710 + DETMIR_PORTAL_WORKTIME_URL=http://192.0.2.13:5610 + DETMIR_PORTAL_ONE_C_URL=http://192.0.2.2:8710 DETMIR_PORTAL_TIMEOUT_SECONDS=10 DETMIR_PORTAL_STATE_DIR=/var/lib/detmir-portal DETMIR_PORTAL_DLP_DB_PATH=/var/lib/activitywatch/dlp_warehouse.sqlite @@ -117,7 +117,7 @@ vars: aw_repo_root: "{{ playbook_dir | dirname }}" aw_rust_release_dir: "{{ (lookup('env', 'CARGO_TARGET_DIR') | default(aw_repo_root + '/adk-rust/target', true)) + '/release' }}" - detmir_evidence_bind: "{{ detmir_evidence_bind_override | default('10.10.10.13:8721') }}" + detmir_evidence_bind: "{{ detmir_evidence_bind_override | default('192.0.2.13:8721') }}" detmir_evidence_env_path: "/etc/detmir-portal-evidence.env" detmir_evidence_upload_token_path: "/var/lib/activitywatch/dlp-evidence/upload-token" diff --git a/ansible/deploy_dlp_evidence_sync.yml b/ansible/deploy_dlp_evidence_sync.yml index 947d63f..02bf066 100644 --- a/ansible/deploy_dlp_evidence_sync.yml +++ b/ansible/deploy_dlp_evidence_sync.yml @@ -9,7 +9,7 @@ aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus" aw_windows_evidence_sync_task_name: "ActivityWatch DLP Evidence Sync" aw_windows_evidence_sync_interval_minutes: 5 - aw_windows_evidence_sync_api_url: "http://10.10.10.13:8721/api/dlp/evidence/upload" + aw_windows_evidence_sync_api_url: "http://192.0.2.13:8721/api/dlp/evidence/upload" aw_windows_evidence_sync_script: "{{ aw_windows_state_root }}\\sync-dlp-evidence-artifacts.ps1" aw_windows_evidence_sync_token_path: "{{ aw_windows_state_root }}\\dlp-evidence-upload-token.txt" aw_windows_evidence_sync_state_path: "{{ aw_windows_state_root }}\\dlp-evidence-sync-state.json" diff --git a/ansible/deploy_file_1c_analytics.yml b/ansible/deploy_file_1c_analytics.yml index 52357ac..aa3baa9 100644 --- a/ansible/deploy_file_1c_analytics.yml +++ b/ansible/deploy_file_1c_analytics.yml @@ -16,7 +16,7 @@ aw_file_1c_clickhouse_native_port: 9000 aw_file_1c_company_api_host: "{{ ansible_host | default('127.0.0.1') }}" aw_file_1c_company_api_port: 8710 - aw_file_1c_manager_brief_grafana_url: "http://10.10.10.11:3000/d/1c-file-mgmt/1c-file-management-board?orgId=1" + aw_file_1c_manager_brief_grafana_url: "http://192.0.2.11:3000/d/1c-file-mgmt/1c-file-management-board?orgId=1" aw_file_1c_manager_brief_state_dir: /opt/activitywatch/clickhouse-1c/state/manager-brief aw_file_1c_manager_brief_model: gpt-5.3-codex aw_file_1c_manager_brief_codex_user: igor diff --git a/ansible/deploy_file_1c_windows_telemetry.yml b/ansible/deploy_file_1c_windows_telemetry.yml index 4b29fbe..c76ef1e 100644 --- a/ansible/deploy_file_1c_windows_telemetry.yml +++ b/ansible/deploy_file_1c_windows_telemetry.yml @@ -134,9 +134,9 @@ $user = Split-Path $profile.LocalPath -Leaf $sshDir = Join-Path $profile.LocalPath '.ssh' $dst = Join-Path $sshDir 'awops_ed25519' - $grantDir = 'SHARKON2025\' + $user + ':(OI)(CI)(F)' - $grantRead = 'SHARKON2025\' + $user + ':(R)' - $grantModify = 'SHARKON2025\' + $user + ':(M)' + $grantDir = 'HOST-EXAMPLE\' + $user + ':(OI)(CI)(F)' + $grantRead = 'HOST-EXAMPLE\' + $user + ':(R)' + $grantModify = 'HOST-EXAMPLE\' + $user + ':(M)' New-Item -ItemType Directory -Path $sshDir -Force | Out-Null & icacls.exe $sshDir /grant $grantDir | Out-Null diff --git a/ansible/deploy_grafana_check.yml b/ansible/deploy_grafana_check.yml index 221ee45..0e32aee 100644 --- a/ansible/deploy_grafana_check.yml +++ b/ansible/deploy_grafana_check.yml @@ -14,7 +14,7 @@ grafana_check_dashboard_uid: "{{ detmir_grafana_check_dashboard_uid | default('detmir-aw-main') }}" grafana_check_dashboard_file_src: "{{ aw_repo_root }}/grafana/detmir-aw-main-dashboard.json" grafana_check_dashboard_file_dest: /etc/grafana/provisioning/dashboards/aw/detmir-aw-main.json - grafana_check_host: "{{ detmir_grafana_check_host | default('SHARKON2025') }}" + grafana_check_host: "{{ detmir_grafana_check_host | default('HOST-EXAMPLE') }}" grafana_check_max_freshness_minutes: "{{ detmir_grafana_check_max_freshness_minutes | default(360) }}" grafana_check_on_calendar: "{{ detmir_grafana_check_on_calendar | default('*:0/15') }}" diff --git a/ansible/deploy_proxmox_web_gateway.yml b/ansible/deploy_proxmox_web_gateway.yml index f951d14..195aaad 100644 --- a/ansible/deploy_proxmox_web_gateway.yml +++ b/ansible/deploy_proxmox_web_gateway.yml @@ -23,84 +23,84 @@ title: "Proxmox VE" category: "Host" description: "Основная панель управления Proxmox VE." - target_url: "https://10.10.10.2:8006/" + target_url: "https://192.0.2.2:8006/" external_enabled: false - slug: "file1c-brief" title: "1C Executive Brief" category: "Management" description: "Сводка по предприятиям и рискам 1С." - target_url: "http://10.10.10.2:8710/manager/brief" + target_url: "http://192.0.2.2:8710/manager/brief" proxy_path: "/r/file1c/brief" - proxy_target_url: "http://10.10.10.2:8710/manager/brief" + proxy_target_url: "http://192.0.2.2:8710/manager/brief" external_enabled: true - slug: "file1c-actions" title: "1C Management Actions" category: "Management" description: "Очередь действий по предприятиям в 1С." - target_url: "http://10.10.10.2:8710/manager/actions" + target_url: "http://192.0.2.2:8710/manager/actions" proxy_path: "/r/file1c/actions" - proxy_target_url: "http://10.10.10.2:8710/manager/actions" + proxy_target_url: "http://192.0.2.2:8710/manager/actions" external_enabled: true - slug: "file1c-finance" title: "1C Financial Reporting" category: "Management" description: "Первый financial board по файловой 1С с разделением ledger/proxy." - target_url: "http://10.10.10.11:3000/d/1c-file-finance/1c-file-financial-reporting?orgId=1" + target_url: "http://192.0.2.11:3000/d/1c-file-finance/1c-file-financial-reporting?orgId=1" proxy_path: "/d/1c-file-finance/1c-file-financial-reporting?orgId=1" external_enabled: true - slug: "file1c-telemetry" title: "1C Telemetry Board" category: "Dashboards" description: "Read-only telemetry экран по состоянию файловых баз, reglog и host." - target_url: "http://10.10.10.11:3000/d/1c-file-telemetry/1c-file-telemetry-board?orgId=1" + target_url: "http://192.0.2.11:3000/d/1c-file-telemetry/1c-file-telemetry-board?orgId=1" proxy_path: "/d/1c-file-telemetry/1c-file-telemetry-board?orgId=1" external_enabled: true - slug: "grafana-1c" title: "Grafana 1C" category: "Dashboards" description: "Рабочий file-1c dashboard contour в внешней Grafana." - target_url: "http://10.10.10.11:3000/d/1c-file-mgmt/1c-file-management-board?orgId=1" + target_url: "http://192.0.2.11:3000/d/1c-file-mgmt/1c-file-management-board?orgId=1" proxy_path: "/d/1c-file-mgmt/1c-file-management-board?orgId=1" external_enabled: true - slug: "clickhouse-http" title: "ClickHouse HTTP" category: "Data" description: "HTTP endpoint ClickHouse для file-1C analytics." - target_url: "http://10.10.10.2:8123/" + target_url: "http://192.0.2.2:8123/" external_enabled: false - slug: "influxdb" title: "InfluxDB" category: "Data" description: "InfluxDB LXC на CT 200." - target_url: "http://10.10.10.10:8086/" + target_url: "http://192.0.2.10:8086/" external_enabled: false - slug: "grafana-core" title: "Grafana Core" category: "Dashboards" description: "Отдельный Grafana CT 201." - target_url: "http://10.10.10.11:3000/" + target_url: "http://192.0.2.11:3000/" proxy_path: "/dashboards" external_enabled: true - slug: "loki-alloy" title: "Grafana Alloy" category: "Logs" description: "Web UI Alloy на CT 202." - target_url: "http://10.10.10.12:12345/" + target_url: "http://192.0.2.12:12345/" external_enabled: false - slug: "aw-ui" title: "AW-rus UI" category: "Operations" description: "Основной ActivityWatch-Russian Web UI на CT 203." - target_url: "http://10.10.10.13:5600/" + target_url: "http://192.0.2.13:5600/" proxy_path: "/r/aw/" external_enabled: true - slug: "aw-worktime" title: "AW-rus Management Report" category: "Operations" description: "Управленческий worktime/report API на CT 203." - target_url: "http://10.10.10.13:5610/reports/worktime/management?format=html&host=SHARKON2025" + target_url: "http://192.0.2.13:5610/reports/worktime/management?format=html&host=HOST-EXAMPLE" proxy_path: "/r/aw-worktime" - proxy_target_url: "http://10.10.10.13:5610/reports/worktime/management?format=html&host=SHARKON2025" + proxy_target_url: "http://192.0.2.13:5610/reports/worktime/management?format=html&host=HOST-EXAMPLE" external_enabled: true - slug: "detmir-portal" title: "DetMir Portal" diff --git a/ansible/deploy_tsj_guardian_bot_proxmox.yml b/ansible/deploy_tsj_guardian_bot_proxmox.yml index deecab3..35bb316 100644 --- a/ansible/deploy_tsj_guardian_bot_proxmox.yml +++ b/ansible/deploy_tsj_guardian_bot_proxmox.yml @@ -201,11 +201,11 @@ TELEGRAM_DEFAULT_CHAT_ID={{ tsj_bot_default_chat_id }} HTTPS_PROXY={{ tsj_bot_https_proxy_url | default(tsj_bot_telegram_proxy_url | default('http://127.0.0.1:11090')) }} HTTP_PROXY={{ tsj_bot_http_proxy_url | default(tsj_bot_telegram_proxy_url | default('http://127.0.0.1:11090')) }} - NO_PROXY={{ tsj_bot_no_proxy | default('localhost,127.0.0.1,10.10.10.0/24') }} - NODE_13_HOST={{ tsj_bot_node_13_host | default('10.10.10.13') }} - NODE_16_HOST={{ tsj_bot_node_16_host | default('10.10.10.16') }} - NODE_13_URL={{ tsj_bot_node_13_url | default('http://10.10.10.13:5600/api/0/info') }} - NODE_16_URL={{ tsj_bot_node_16_url | default('http://10.10.10.16/') }} + NO_PROXY={{ tsj_bot_no_proxy | default('localhost,127.0.0.1,192.0.2.0/24') }} + NODE_13_HOST={{ tsj_bot_node_13_host | default('192.0.2.13') }} + NODE_16_HOST={{ tsj_bot_node_16_host | default('192.0.2.16') }} + NODE_13_URL={{ tsj_bot_node_13_url | default('http://192.0.2.13:5600/api/0/info') }} + NODE_16_URL={{ tsj_bot_node_16_url | default('http://192.0.2.16/') }} NODE_16_ENABLED={{ tsj_bot_node_16_enabled | default('false') }} INFRA_ADMIN_ROOT={{ tsj_bot_runtime_root }} CHECK_SCRIPT={{ tsj_bot_check_script | default(tsj_bot_runtime_root + '/scripts/system_self_support.sh --check') }} @@ -254,24 +254,24 @@ UPDATE_TARGETS={{ tsj_bot_update_targets | default('auto') }} DETMIR_AI_STATE_FILE={{ tsj_bot_detmir_ai_state_file | default('/var/lib/detmir-ai/latest-state.json') }} TSJ_GUARDIAN_STATUS_BIN={{ tsj_bot_guardian_status_bin | default('/usr/local/bin/tsj-guardian-status') }} - AW_RUS_API_BASE={{ tsj_bot_aw_rus_api_base | default('http://10.10.10.13:5600/api/0') }} - AW_RUS_WORKTIME_BASE={{ tsj_bot_aw_rus_worktime_base | default('http://10.10.10.13:5610') }} - AW_DLP_POLICY_API_BASE={{ tsj_bot_aw_dlp_policy_api_base | default('http://10.10.10.13:5601/api/0') }} + AW_RUS_API_BASE={{ tsj_bot_aw_rus_api_base | default('http://192.0.2.13:5600/api/0') }} + AW_RUS_WORKTIME_BASE={{ tsj_bot_aw_rus_worktime_base | default('http://192.0.2.13:5610') }} + AW_DLP_POLICY_API_BASE={{ tsj_bot_aw_dlp_policy_api_base | default('http://192.0.2.13:5601/api/0') }} AW_DLP_POLICY_ACTOR={{ tsj_bot_aw_dlp_policy_actor | default('tsj-guardian-bot') }} - AW_RUS_WORKTIME_HEAL_CMD={{ tsj_bot_aw_rus_worktime_heal_cmd | default("ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@10.10.10.13 'sudo -n /usr/local/bin/aw-worktime-autoheal-rust && sudo -n systemctl reset-failed aw-worktime-ui-bridge.service && sudo -n systemctl start aw-worktime-ui-bridge.service'") }} - AW_RUS_DLP_HEAL_CMD={{ tsj_bot_aw_rus_dlp_heal_cmd | default("ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@10.10.10.13 'sudo -n systemctl restart activitywatch-server.service && (sudo -n systemctl start activitywatch-dlp-aggregator.service || true) && sudo -n /usr/local/bin/aw-health-check && sudo -n /usr/local/bin/dlp-health-check'") }} - AW_RUS_CASE_API_BASE={{ tsj_bot_aw_rus_case_api_base | default('http://10.10.10.13:5602') }} + AW_RUS_WORKTIME_HEAL_CMD={{ tsj_bot_aw_rus_worktime_heal_cmd | default("ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@192.0.2.13 'sudo -n /usr/local/bin/aw-worktime-autoheal-rust && sudo -n systemctl reset-failed aw-worktime-ui-bridge.service && sudo -n systemctl start aw-worktime-ui-bridge.service'") }} + AW_RUS_DLP_HEAL_CMD={{ tsj_bot_aw_rus_dlp_heal_cmd | default("ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@192.0.2.13 'sudo -n systemctl restart activitywatch-server.service && (sudo -n systemctl start activitywatch-dlp-aggregator.service || true) && sudo -n /usr/local/bin/aw-health-check && sudo -n /usr/local/bin/dlp-health-check'") }} + AW_RUS_CASE_API_BASE={{ tsj_bot_aw_rus_case_api_base | default('http://192.0.2.13:5602') }} AW_RUS_HAYABUSA_ENABLED={{ tsj_bot_aw_rus_hayabusa_enabled | default('true') }} - AW_RUS_HAYABUSA_SSH_CMD={{ tsj_bot_aw_rus_hayabusa_ssh_cmd | default("ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@10.10.10.13") }} - AW_RUS_HOST={{ tsj_bot_aw_rus_host | default('SHARKON2025') }} + AW_RUS_HAYABUSA_SSH_CMD={{ tsj_bot_aw_rus_hayabusa_ssh_cmd | default("ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@192.0.2.13") }} + AW_RUS_HOST={{ tsj_bot_aw_rus_host | default('HOST-EXAMPLE') }} AW_RUS_PRIMARY_USER={{ tsj_bot_aw_rus_primary_user | default('USER1') }} AW_RUS_STALE_SEC={{ tsj_bot_aw_rus_stale_sec | default(900) }} AW_RUS_SLO_ENABLED={{ tsj_bot_aw_rus_slo_enabled | default('true') }} AW_RUS_SLO_ALERT_WINDOW={{ tsj_bot_aw_rus_slo_alert_window | default('24h') }} AW_RUS_SLO_MIN_SAMPLES={{ tsj_bot_aw_rus_slo_min_samples | default(4) }} AW_RUS_SLO_MAX_AGE_SEC={{ tsj_bot_aw_rus_slo_max_age_sec | default(90) }} - AW_RUS_SLO_SUMMARY_CMD={{ tsj_bot_aw_rus_slo_summary_cmd | default("ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@10.10.10.13 'cat /var/lib/activitywatch/slo/aw-slo-summary.json'") }} - AW_RUS_WINDOWS_HOST={{ tsj_bot_aw_rus_windows_host | default(hostvars[(groups['aw_windows'] | first)].ansible_host | default('192.168.100.18')) }} + AW_RUS_SLO_SUMMARY_CMD={{ tsj_bot_aw_rus_slo_summary_cmd | default("ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@192.0.2.13 'cat /var/lib/activitywatch/slo/aw-slo-summary.json'") }} + AW_RUS_WINDOWS_HOST={{ tsj_bot_aw_rus_windows_host | default(hostvars[(groups['aw_windows'] | first)].ansible_host | default('198.51.100.18')) }} AW_RUS_WINDOWS_SSH_USER={{ tsj_bot_aw_rus_windows_ssh_user | default(hostvars[(groups['aw_windows'] | first)].ansible_user | default('Администратор')) }} AW_RUS_WINDOWS_SSH_PASSWORD={{ tsj_bot_aw_rus_windows_ssh_password | default(hostvars[(groups['aw_windows'] | first)].ansible_password | default('')) }} AW_RUS_WINDOWS_CONFIG_PATH={{ tsj_bot_aw_rus_windows_config_path | default('C:\ProgramData\AWatch-rus\deployment-config.json') }} @@ -301,24 +301,24 @@ group: "{{ tsj_bot_group }}" mode: "0640" loop: - - { key: "AW_RUS_API_BASE", value: "{{ tsj_bot_aw_rus_api_base | default('http://10.10.10.13:5600/api/0') }}" } - - { key: "AW_RUS_WORKTIME_BASE", value: "{{ tsj_bot_aw_rus_worktime_base | default('http://10.10.10.13:5610') }}" } - - { key: "AW_DLP_POLICY_API_BASE", value: "{{ tsj_bot_aw_dlp_policy_api_base | default('http://10.10.10.13:5601/api/0') }}" } + - { key: "AW_RUS_API_BASE", value: "{{ tsj_bot_aw_rus_api_base | default('http://192.0.2.13:5600/api/0') }}" } + - { key: "AW_RUS_WORKTIME_BASE", value: "{{ tsj_bot_aw_rus_worktime_base | default('http://192.0.2.13:5610') }}" } + - { key: "AW_DLP_POLICY_API_BASE", value: "{{ tsj_bot_aw_dlp_policy_api_base | default('http://192.0.2.13:5601/api/0') }}" } - { key: "AW_DLP_POLICY_ACTOR", value: "{{ tsj_bot_aw_dlp_policy_actor | default('tsj-guardian-bot') }}" } - - { key: "AW_RUS_WORKTIME_HEAL_CMD", value: "{{ tsj_bot_aw_rus_worktime_heal_cmd | default(\"ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@10.10.10.13 'sudo -n /usr/local/bin/aw-worktime-autoheal-rust && sudo -n systemctl reset-failed aw-worktime-ui-bridge.service && sudo -n systemctl start aw-worktime-ui-bridge.service'\") }}" } - - { key: "AW_RUS_DLP_HEAL_CMD", value: "{{ tsj_bot_aw_rus_dlp_heal_cmd | default(\"ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@10.10.10.13 'sudo -n systemctl restart activitywatch-server.service && (sudo -n systemctl start activitywatch-dlp-aggregator.service || true) && sudo -n /usr/local/bin/aw-health-check && sudo -n /usr/local/bin/dlp-health-check'\") }}" } - - { key: "AW_RUS_CASE_API_BASE", value: "{{ tsj_bot_aw_rus_case_api_base | default('http://10.10.10.13:5602') }}" } + - { key: "AW_RUS_WORKTIME_HEAL_CMD", value: "{{ tsj_bot_aw_rus_worktime_heal_cmd | default(\"ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@192.0.2.13 'sudo -n /usr/local/bin/aw-worktime-autoheal-rust && sudo -n systemctl reset-failed aw-worktime-ui-bridge.service && sudo -n systemctl start aw-worktime-ui-bridge.service'\") }}" } + - { key: "AW_RUS_DLP_HEAL_CMD", value: "{{ tsj_bot_aw_rus_dlp_heal_cmd | default(\"ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@192.0.2.13 'sudo -n systemctl restart activitywatch-server.service && (sudo -n systemctl start activitywatch-dlp-aggregator.service || true) && sudo -n /usr/local/bin/aw-health-check && sudo -n /usr/local/bin/dlp-health-check'\") }}" } + - { key: "AW_RUS_CASE_API_BASE", value: "{{ tsj_bot_aw_rus_case_api_base | default('http://192.0.2.13:5602') }}" } - { key: "AW_RUS_HAYABUSA_ENABLED", value: "{{ tsj_bot_aw_rus_hayabusa_enabled | default('true') }}" } - - { key: "AW_RUS_HAYABUSA_SSH_CMD", value: "{{ tsj_bot_aw_rus_hayabusa_ssh_cmd | default(\"ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@10.10.10.13\") }}" } - - { key: "AW_RUS_HOST", value: "{{ tsj_bot_aw_rus_host | default('SHARKON2025') }}" } + - { key: "AW_RUS_HAYABUSA_SSH_CMD", value: "{{ tsj_bot_aw_rus_hayabusa_ssh_cmd | default(\"ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@192.0.2.13\") }}" } + - { key: "AW_RUS_HOST", value: "{{ tsj_bot_aw_rus_host | default('HOST-EXAMPLE') }}" } - { key: "AW_RUS_PRIMARY_USER", value: "{{ tsj_bot_aw_rus_primary_user | default('USER1') }}" } - { key: "AW_RUS_STALE_SEC", value: "{{ tsj_bot_aw_rus_stale_sec | default(900) }}" } - { key: "AW_RUS_SLO_ENABLED", value: "{{ tsj_bot_aw_rus_slo_enabled | default('true') }}" } - { key: "AW_RUS_SLO_ALERT_WINDOW", value: "{{ tsj_bot_aw_rus_slo_alert_window | default('24h') }}" } - { key: "AW_RUS_SLO_MIN_SAMPLES", value: "{{ tsj_bot_aw_rus_slo_min_samples | default(4) }}" } - { key: "AW_RUS_SLO_MAX_AGE_SEC", value: "{{ tsj_bot_aw_rus_slo_max_age_sec | default(90) }}" } - - { key: "AW_RUS_SLO_SUMMARY_CMD", value: "{{ tsj_bot_aw_rus_slo_summary_cmd | default(\"ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@10.10.10.13 'cat /var/lib/activitywatch/slo/aw-slo-summary.json'\") }}" } - - { key: "AW_RUS_WINDOWS_HOST", value: "{{ tsj_bot_aw_rus_windows_host | default(hostvars[(groups['aw_windows'] | first)].ansible_host | default('192.168.100.18')) }}" } + - { key: "AW_RUS_SLO_SUMMARY_CMD", value: "{{ tsj_bot_aw_rus_slo_summary_cmd | default(\"ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new igor@192.0.2.13 'cat /var/lib/activitywatch/slo/aw-slo-summary.json'\") }}" } + - { key: "AW_RUS_WINDOWS_HOST", value: "{{ tsj_bot_aw_rus_windows_host | default(hostvars[(groups['aw_windows'] | first)].ansible_host | default('198.51.100.18')) }}" } - { key: "AW_RUS_WINDOWS_SSH_USER", value: "{{ tsj_bot_aw_rus_windows_ssh_user | default(hostvars[(groups['aw_windows'] | first)].ansible_user | default('Администратор')) }}" } - { key: "AW_RUS_WINDOWS_SSH_PASSWORD", value: "{{ tsj_bot_aw_rus_windows_ssh_password | default(hostvars[(groups['aw_windows'] | first)].ansible_password | default('')) }}" } - { key: "AW_RUS_WINDOWS_CONFIG_PATH", value: "{{ tsj_bot_aw_rus_windows_config_path | default('C:\\ProgramData\\AWatch-rus\\deployment-config.json') }}" } diff --git a/ansible/group_vars/all.example.yml b/ansible/group_vars/all.example.yml index 7897f05..5bcf2d6 100644 --- a/ansible/group_vars/all.example.yml +++ b/ansible/group_vars/all.example.yml @@ -16,19 +16,19 @@ aw_worktime_influx_enabled: false aw_worktime_influx_url: "http://:8086" aw_worktime_influx_org: "proxmox" aw_worktime_influx_bucket: "aw_metrics" -aw_worktime_influx_hosts: "SHARKON2025" +aw_worktime_influx_hosts: "HOST-EXAMPLE" aw_worktime_influx_days: "today,yesterday" aw_worktime_influx_token: "" aw_dlp_influx_enabled: false aw_dlp_influx_url: "http://:8086" aw_dlp_influx_org: "proxmox" aw_dlp_influx_bucket: "aw_metrics" -aw_dlp_influx_hosts: "SHARKON2025" +aw_dlp_influx_hosts: "HOST-EXAMPLE" aw_dlp_influx_lookback_days: 30 aw_dlp_influx_event_limit: 2000 aw_dlp_influx_token: "" aw_monitored_windows_host: "" -aw_monitored_windows_hostname: "SHARKON2025" +aw_monitored_windows_hostname: "HOST-EXAMPLE" aw_rus_health_worktime_api_base: "http://127.0.0.1:5610" aw_rus_health_state_dir: "{{ aw_server_data_dir }}/health" aw_rus_health_validation_dir: "{{ aw_rus_health_state_dir }}/windows-validation" @@ -67,7 +67,7 @@ aw_worktime_from: "00: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/" +aw_server_landingpage: "/#/activity/HOST-EXAMPLE/view/" aw_health_strict_fileops: 0 aw_dlp_policy_engine_enabled: true diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index bf3a9c6..2b9eb92 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -13,22 +13,22 @@ aw_server_public_host: "{{ (hostvars[aw_server_inventory_host].ansible_host | de aw_worktime_report_base: "http://{{ aw_server_public_host }}:5610" aw_worktime_timezone: "Europe/Moscow" aw_worktime_influx_enabled: true -aw_worktime_influx_url: "http://10.10.10.10:8086" +aw_worktime_influx_url: "http://192.0.2.10:8086" aw_worktime_influx_org: "proxmox" aw_worktime_influx_bucket: "aw_metrics" -aw_worktime_influx_hosts: "SHARKON2025" +aw_worktime_influx_hosts: "HOST-EXAMPLE" aw_worktime_influx_days: "today,yesterday" aw_worktime_influx_token: "{{ lookup('env', 'AW_WORKTIME_INFLUX_TOKEN') }}" aw_dlp_influx_enabled: true -aw_dlp_influx_url: "http://10.10.10.10:8086" +aw_dlp_influx_url: "http://192.0.2.10:8086" aw_dlp_influx_org: "proxmox" aw_dlp_influx_bucket: "aw_metrics" -aw_dlp_influx_hosts: "SHARKON2025" +aw_dlp_influx_hosts: "HOST-EXAMPLE" aw_dlp_influx_lookback_days: 30 aw_dlp_influx_event_limit: 2000 aw_dlp_influx_token: "{{ lookup('env', 'AW_DLP_INFLUX_TOKEN') }}" -aw_monitored_windows_host: "192.168.100.18" -aw_monitored_windows_hostname: "SHARKON2025" +aw_monitored_windows_host: "198.51.100.18" +aw_monitored_windows_hostname: "HOST-EXAMPLE" aw_rus_health_worktime_api_base: "http://127.0.0.1:5610" aw_rus_health_state_dir: "{{ aw_server_data_dir }}/health" aw_rus_health_validation_dir: "{{ aw_rus_health_state_dir }}/windows-validation" @@ -74,5 +74,5 @@ aw_worktime_from: "00: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/" +aw_server_landingpage: "/#/activity/HOST-EXAMPLE/view/" aw_health_strict_fileops: 0 diff --git a/ansible/group_vars/aw_windows.yml b/ansible/group_vars/aw_windows.yml index 21f26fe..c32ea75 100644 --- a/ansible/group_vars/aw_windows.yml +++ b/ansible/group_vars/aw_windows.yml @@ -14,7 +14,7 @@ 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_domain: "HOST-EXAMPLE" aw_windows_builtin_administrator_name: "Администратор" aw_windows_users: - Администратор diff --git a/ansible/group_vars/proxmox-bot.example.yml b/ansible/group_vars/proxmox-bot.example.yml index a0ec9f9..8ae702e 100644 --- a/ansible/group_vars/proxmox-bot.example.yml +++ b/ansible/group_vars/proxmox-bot.example.yml @@ -54,7 +54,7 @@ tsj_bot_aw_rus_dlp_heal_cmd: "sshpass -p 'CHANGE_ME' ssh -o PubkeyAuthentication tsj_bot_aw_rus_case_api_base: "http://:5602" tsj_bot_aw_rus_hayabusa_enabled: "true" tsj_bot_aw_rus_hayabusa_ssh_cmd: "sshpass -p 'CHANGE_ME' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@" -tsj_bot_aw_rus_host: "SHARKON2025" +tsj_bot_aw_rus_host: "HOST-EXAMPLE" tsj_bot_aw_rus_primary_user: "USER1" tsj_bot_aw_rus_stale_sec: 900 tsj_bot_aw_rus_slo_enabled: "true" diff --git a/ansible/group_vars/windows.example.yml b/ansible/group_vars/windows.example.yml index 9f6ad99..c30c8f0 100644 --- a/ansible/group_vars/windows.example.yml +++ b/ansible/group_vars/windows.example.yml @@ -12,7 +12,7 @@ aw_windows_hayabusa_auto_upload_task_name: "ActivityWatch Hayabusa Upload" 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_domain: "HOST-EXAMPLE" # Localized name of the built-in local Administrator account (SID ending in -500). # On the current Russian Windows host this must stay "Администратор"; # do not replace it with "Administrator" unless the target OS account is actually named that way. @@ -32,7 +32,7 @@ aw_windows_extra_users: [] # Единые Windows/RDP пути: те же, что использует InnoSetup. aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin" aw_windows_state_root: "C:\\ProgramData\\AWatch-rus" -aw_windows_hostname_override: "" # Например: SHARKON2025 +aw_windows_hostname_override: "" # Например: HOST-EXAMPLE aw_windows_afk_enabled: true aw_windows_window_enabled: true aw_windows_file_ops_enabled: true diff --git a/ansible/inventory.example.ini b/ansible/inventory.example.ini index 87aac03..9f8afbf 100644 --- a/ansible/inventory.example.ini +++ b/ansible/inventory.example.ini @@ -1,5 +1,5 @@ [proxmox] -pve-main ansible_host=192.168.10.2 ansible_user=root ansible_port=22 +pve-main ansible_host=198.51.100.2 ansible_user=root ansible_port=22 [aw_server] aw-ct ansible_host=10.20.30.13 ansible_user=root ansible_port=22 @@ -9,7 +9,7 @@ aw-ct ansible_host=10.20.30.13 ansible_user=root ansible_port=22 win-node1 ansible_host= 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 +# pfsense-poller1 ansible_host=198.51.100.30 ansible_user=root ansible_port=22 [grafana] # Для API-import playbook достаточно указать grafana_url. diff --git a/ansible/tasks/provision_ct_and_deploy_aw.yml b/ansible/tasks/provision_ct_and_deploy_aw.yml index a2610fd..7c1c9d5 100644 --- a/ansible/tasks/provision_ct_and_deploy_aw.yml +++ b/ansible/tasks/provision_ct_and_deploy_aw.yml @@ -127,7 +127,7 @@ export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y curl ca-certificates bash unzip xz-utils jq rsync openssh-server python3 - mkdir -p /root/bootstrap/settings /etc/activitywatch + mkdir -p /opt/detmir/bootstrap/settings /etc/activitywatch systemctl enable ssh || true systemctl restart ssh || true register: ct_bootstrap_result @@ -142,7 +142,7 @@ - push - "{{ ct_id }}" - "{{ proxmox_bootstrap_dir }}/{{ item }}" - - "/root/bootstrap/{{ item }}" + - "/opt/detmir/bootstrap/{{ item }}" loop: "{{ aw_bootstrap_files }}" - name: Записать AW server env во временный каталог Proxmox host @@ -196,9 +196,9 @@ - -lc - | set -euo pipefail - chmod +x /root/bootstrap/install_aw_server.sh /root/bootstrap/apply_webui_ru_patch.sh - bash /root/bootstrap/install_aw_server.sh - bash /root/bootstrap/apply_webui_ru_patch.sh + chmod +x /opt/detmir/bootstrap/install_aw_server.sh /opt/detmir/bootstrap/apply_webui_ru_patch.sh + bash /opt/detmir/bootstrap/install_aw_server.sh + bash /opt/detmir/bootstrap/apply_webui_ru_patch.sh systemctl restart activitywatch-server.service - name: Проверить AW API изнутри CT diff --git a/ansible/templates/proxmox-web-gateway-index.html.j2 b/ansible/templates/proxmox-web-gateway-index.html.j2 index c77b3aa..6ba2a46 100644 --- a/ansible/templates/proxmox-web-gateway-index.html.j2 +++ b/ansible/templates/proxmox-web-gateway-index.html.j2 @@ -132,7 +132,7 @@

Proxmox Web Gateway

Единая стартовая точка для web-сервисов контура на Proxmox host - 10.10.10.2. Внешний вход закрыт gateway-auth, + 192.0.2.2. Внешний вход закрыт gateway-auth, а основные операторские страницы идут через reverse proxy без выдачи внутренних адресов наружу.

diff --git a/ansible/templates/proxmox-web-gateway.conf.j2 b/ansible/templates/proxmox-web-gateway.conf.j2 index 35f1757..281a2c2 100644 --- a/ansible/templates/proxmox-web-gateway.conf.j2 +++ b/ansible/templates/proxmox-web-gateway.conf.j2 @@ -6,7 +6,7 @@ map $http_upgrade $connection_upgrade { server { listen 80 default_server; listen [::]:80 default_server; - server_name {{ proxmox_web_gateway_public_hostname }} 10.10.10.2 _; + server_name {{ proxmox_web_gateway_public_hostname }} 192.0.2.2 _; access_log /var/log/nginx/proxmox-web-gateway.access.log; error_log /var/log/nginx/proxmox-web-gateway.error.log; @@ -17,7 +17,7 @@ server { server { listen 443 ssl http2 default_server; listen [::]:443 ssl http2 default_server; - server_name {{ proxmox_web_gateway_public_hostname }} 10.10.10.2 _; + server_name {{ proxmox_web_gateway_public_hostname }} 192.0.2.2 _; access_log /var/log/nginx/proxmox-web-gateway.access.log; error_log /var/log/nginx/proxmox-web-gateway.error.log; @@ -67,32 +67,32 @@ server { location /api/0/ { proxy_set_header Origin ""; proxy_set_header Authorization ""; - proxy_pass http://10.10.10.13:5600; + proxy_pass http://192.0.2.13:5600; proxy_redirect off; } location /r/grafana/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000/; - proxy_redirect http://10.10.10.11:3000/ /r/grafana/; + proxy_pass http://192.0.2.11:3000/; + proxy_redirect http://192.0.2.11:3000/ /r/grafana/; } location = /login { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location = /logout { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /public/ { proxy_set_header Origin ""; proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } @@ -103,138 +103,138 @@ server { proxy_set_header Authorization ""; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /api/ { proxy_set_header Origin ""; proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /apis/ { proxy_set_header Origin ""; proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /d/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /dashboards { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /dashboard/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /avatar/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /profile/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /org/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /user/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /plugins/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /explore { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /alerting/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /connections/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /datasources/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.11:3000; + proxy_pass http://192.0.2.11:3000; proxy_redirect off; } location /r/aw/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.13:5600/; - proxy_redirect http://10.10.10.13:5600/ /r/aw/; + proxy_pass http://192.0.2.13:5600/; + proxy_redirect http://192.0.2.13:5600/ /r/aw/; } location /reports/worktime/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.13:5610; + proxy_pass http://192.0.2.13:5610; proxy_redirect off; } location = /dark.css { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.13:5600; + proxy_pass http://192.0.2.13:5600; proxy_redirect off; } location /css/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.13:5600; + proxy_pass http://192.0.2.13:5600; proxy_redirect off; } location = /js/aw-worktime-panel.js { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.13:5600; + proxy_pass http://192.0.2.13:5600; proxy_redirect off; sub_filter_once off; sub_filter_types application/javascript text/javascript; - sub_filter 'http://10.10.10.13:5610' ''; + sub_filter 'http://192.0.2.13:5610' ''; } location /js/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.13:5600; + proxy_pass http://192.0.2.13:5600; proxy_redirect off; } location /img/ { proxy_set_header Authorization ""; - proxy_pass http://10.10.10.13:5600; + proxy_pass http://192.0.2.13:5600; proxy_redirect off; } @@ -242,7 +242,7 @@ server { auth_basic off; proxy_set_header Origin ""; proxy_set_header Authorization ""; - proxy_pass http://10.10.10.13:5600; + proxy_pass http://192.0.2.13:5600; proxy_redirect off; } @@ -264,7 +264,7 @@ server { location ^~ /portal/api/dlp/evidence { proxy_set_header Authorization ""; proxy_set_header X-Remote-User $remote_user; - proxy_pass http://10.10.10.13:8721/api/dlp/evidence; + proxy_pass http://192.0.2.13:8721/api/dlp/evidence; proxy_redirect off; } diff --git a/aw-server/apply_webui_ru_patch.sh b/aw-server/apply_webui_ru_patch.sh index 64e8826..51cc171 100755 --- a/aw-server/apply_webui_ru_patch.sh +++ b/aw-server/apply_webui_ru_patch.sh @@ -14,10 +14,10 @@ SERVER_PUBLIC_HOST="${AW_SERVER_PUBLIC_HOST:-${AW_SERVER_HOST:-$(hostname -f 2>/ REPORT_BASE="${AW_WORKTIME_REPORT_BASE:-http://${SERVER_PUBLIC_HOST}:5610}" CASE_PORT="${AW_DLP_CASE_PORT:-5602}" CASE_BASE="${AW_DLP_CASE_PUBLIC_BASE:-}" -PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js" -SW_CLEANUP_SRC="/root/bootstrap/aw-sw-cleanup.js" -WORKTIME_PANEL_SRC="/root/bootstrap/aw-worktime-panel.js" -HOST_GROUPS_SRC="/root/bootstrap/aw-host-groups.json" +PATCH_JS_SRC="/opt/detmir/bootstrap/aw-ru-patch.js" +SW_CLEANUP_SRC="/opt/detmir/bootstrap/aw-sw-cleanup.js" +WORKTIME_PANEL_SRC="/opt/detmir/bootstrap/aw-worktime-panel.js" +HOST_GROUPS_SRC="/opt/detmir/bootstrap/aw-host-groups.json" INDEX_HTML="$WEBUI_DIR/index.html" SERVICE_WORKER="$WEBUI_DIR/service-worker.js" TS=$(date +%Y%m%d%H%M%S) diff --git a/aw-server/aw-server.env.example b/aw-server/aw-server.env.example index 6e26bad..69c8db8 100755 --- a/aw-server/aw-server.env.example +++ b/aw-server/aw-server.env.example @@ -43,10 +43,10 @@ AW_HEALTH_CHECK_ENABLED=true AW_HEALTH_CHECK_INTERVAL=60 AW_EXPECT_START_OF_DAY=00:00 AW_EXPECT_ALWAYS_ACTIVE_PATTERN=aw-watcher-window -AW_EXPECT_LANDINGPAGE=/#/activity/SHARKON2025/view/ +AW_EXPECT_LANDINGPAGE=/#/activity/HOST-EXAMPLE/view/ AW_HEALTH_STRICT_FILEOPS=0 AW_MONITORED_WINDOWS_HOST= -AW_MONITORED_WINDOWS_HOSTNAME=SHARKON2025 +AW_MONITORED_WINDOWS_HOSTNAME=HOST-EXAMPLE AW_RUS_HEALTH_WORKTIME_API=http://127.0.0.1:5610 AW_RUS_HEALTH_STATE_DIR=/var/lib/activitywatch/health AW_RUS_HEALTH_VALIDATION_DIR=/var/lib/activitywatch/health/windows-validation @@ -58,7 +58,7 @@ AW_RUS_SLO_WORKTIME_BASE=http://127.0.0.1:5610 AW_RUS_SLO_TARGET_PERCENT=99.97 AW_BROWSER_SMOKE_AW_BASE=http://127.0.0.1:5600 AW_BROWSER_SMOKE_WORKTIME_BASE=http://127.0.0.1:5610 -AW_BROWSER_SMOKE_HOST=SHARKON2025 +AW_BROWSER_SMOKE_HOST=HOST-EXAMPLE AW_BROWSER_SMOKE_OUTPUT_DIR=/var/lib/activitywatch/browser-smoke AW_BROWSER_SMOKE_KEEP_RUNS=24 AW_BROWSER_SMOKE_ENGINE=chromium-cli diff --git a/aw-server/aw-worktime-autoheal.service b/aw-server/aw-worktime-autoheal.service index 37244ff..9c257db 100644 --- a/aw-server/aw-worktime-autoheal.service +++ b/aw-server/aw-worktime-autoheal.service @@ -1,12 +1,12 @@ [Unit] -Description=AW Worktime Autoheal (SHARKON2025) +Description=AW Worktime Autoheal (HOST-EXAMPLE) After=network-online.target activitywatch-server.service Wants=network-online.target [Service] Type=oneshot Environment=AW_URL=http://127.0.0.1:5600 -Environment=AW_WORKTIME_HOST=SHARKON2025 +Environment=AW_WORKTIME_HOST=HOST-EXAMPLE ExecStart=/usr/local/bin/aw-worktime-autoheal-rust User=root Group=root diff --git a/aw-server/aw-worktime-prewarm.sh b/aw-server/aw-worktime-prewarm.sh index 7ae33e9..3c19617 100644 --- a/aw-server/aw-worktime-prewarm.sh +++ b/aw-server/aw-worktime-prewarm.sh @@ -6,7 +6,7 @@ WORKTIME_PREWARM_TIMEOUT_SECONDS="${WORKTIME_PREWARM_TIMEOUT_SECONDS:-45}" WORKTIME_PREWARM_HEALTH_TIMEOUT_SECONDS="${WORKTIME_PREWARM_HEALTH_TIMEOUT_SECONDS:-10}" WORKTIME_PREWARM_READY_TIMEOUT_SECONDS="${WORKTIME_PREWARM_READY_TIMEOUT_SECONDS:-60}" WORKTIME_PREWARM_READY_INTERVAL_SECONDS="${WORKTIME_PREWARM_READY_INTERVAL_SECONDS:-2}" -WORKTIME_PREWARM_HOST="${WORKTIME_PREWARM_HOST:-${AW_WORKTIME_HOST:-SHARKON2025}}" +WORKTIME_PREWARM_HOST="${WORKTIME_PREWARM_HOST:-${AW_WORKTIME_HOST:-HOST-EXAMPLE}}" WORKTIME_PREWARM_PROFILE="${WORKTIME_PREWARM_PROFILE:-full}" log() { diff --git a/aw-server/aw-worktime-ui-bridge.service b/aw-server/aw-worktime-ui-bridge.service index b836e4f..dc9eb15 100644 --- a/aw-server/aw-worktime-ui-bridge.service +++ b/aw-server/aw-worktime-ui-bridge.service @@ -8,7 +8,7 @@ StartLimitIntervalSec=120 [Service] Type=simple Environment=AW_SERVER_URL=http://127.0.0.1:5600 -Environment=AW_WORKTIME_HOST=SHARKON2025 +Environment=AW_WORKTIME_HOST=HOST-EXAMPLE ExecStart=/usr/local/bin/aw-worktime-ui-bridge-rust Restart=on-failure RestartSec=10 diff --git a/aw-server/hayabusa/README.md b/aw-server/hayabusa/README.md index d51460a..68799c1 100644 --- a/aw-server/hayabusa/README.md +++ b/aw-server/hayabusa/README.md @@ -1,6 +1,6 @@ # aw-rus Hayabusa Server Ops Bundle -This directory is the server-side operational bundle for Hayabusa on `10.10.10.13`. +This directory is the server-side operational bundle for Hayabusa on `192.0.2.13`. ## Goal @@ -44,7 +44,7 @@ Prerequisites: - `/opt/activitywatch/aw-rus-ops/venv` contains `ansible` and `pywinrm` - `/opt/activitywatch/aw-rus-ops/ansible/inventory.ini` contains the live Windows connection details -- WinRM from `10.10.10.13` to the Windows host is reachable +- WinRM from `192.0.2.13` to the Windows host is reachable Run: @@ -55,14 +55,14 @@ aw-hayabusa-from-windows --days-back 1 --mode incident --case-id 30 This performs: - Windows EVTX export via WinRM -- fetch of the newest zip directly onto `10.10.10.13` +- fetch of the newest zip directly onto `192.0.2.13` - `aw-hayabusa accept` - `aw-hayabusa process-inbox` - bounded case linkage via case API If WinRM from the server to Windows is blocked by network policy, use the drop-zone workflow below instead. -## Drop-zone automation on 10.10.10.13 +## Drop-zone automation on 192.0.2.13 The server can auto-process packages dropped into: @@ -95,9 +95,9 @@ powershell.exe -ExecutionPolicy Bypass -File C:\ProgramData\AWatch-rus\export-up - run `C:\ProgramData\AWatch-rus\export-evtx-for-hayabusa.ps1` - upload matching `.caseid` first when `-CaseId` is specified - upload the newest zip to `/opt/activitywatch/aw-rus-ops/drop` -- let `aw-hayabusa-drop.path` process the package automatically on `10.10.10.13` +- let `aw-hayabusa-drop.path` process the package automatically on `192.0.2.13` -This path was validated live against case `30` after the `awops` SSH authorization was installed on `10.10.10.13`. +This path was validated live against case `30` after the `awops` SSH authorization was installed on `192.0.2.13`. Server-side prerequisite for user `awops`: diff --git a/aw-server/install_aw_server.sh b/aw-server/install_aw_server.sh index cdc1241..e85c7fa 100755 --- a/aw-server/install_aw_server.sh +++ b/aw-server/install_aw_server.sh @@ -21,7 +21,7 @@ required_vars=( AW_SERVER_GROUP ) -BOOTSTRAP_DIR="/root/bootstrap" +BOOTSTRAP_DIR="/opt/detmir/bootstrap" VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json" CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json" WORKTIME_API_RUST_SRC="$BOOTSTRAP_DIR/worktime-api" @@ -91,7 +91,7 @@ sed \ -e "s#__AW_SERVER_USER__#$AW_SERVER_USER#g" \ -e "s#__AW_SERVER_GROUP__#$AW_SERVER_GROUP#g" \ -e "s#__AW_SERVER_DATA_DIR__#$AW_SERVER_DATA_DIR#g" \ - /root/bootstrap/activitywatch-server.service > /etc/systemd/system/activitywatch-server.service + /opt/detmir/bootstrap/activitywatch-server.service > /etc/systemd/system/activitywatch-server.service chmod 0644 /etc/systemd/system/activitywatch-server.service systemctl daemon-reload diff --git a/aw-server/worktime-manager-aliases.example.json b/aw-server/worktime-manager-aliases.example.json index 6ffc702..2b5ca3f 100644 --- a/aw-server/worktime-manager-aliases.example.json +++ b/aw-server/worktime-manager-aliases.example.json @@ -1,6 +1,6 @@ { "users": { - "SHARKON2025\\USER1": { + "HOST-EXAMPLE\\USER1": { "display_name": "Иван Петров", "manager": "Руководитель смены", "department": "Бухгалтерия", @@ -8,14 +8,14 @@ "notes": "Дневная смена", "exclude": false }, - "SHARKON2025\\USER4": { + "HOST-EXAMPLE\\USER4": { "display_name": "Мария Сидорова", "manager": "Руководитель смены", "department": "Бухгалтерия", "role": "Оператор 1С", "exclude": false }, - "SHARKON2025\\ADMINISTRATOR": { + "HOST-EXAMPLE\\ADMINISTRATOR": { "display_name": "Администратор", "manager": "Ops", "department": "Инфраструктура", diff --git a/check-aw-full.sh b/check-aw-full.sh index 43eb40c..29e2cb1 100644 --- a/check-aw-full.sh +++ b/check-aw-full.sh @@ -1,7 +1,7 @@ #!/bin/bash # check-aw-full.sh - Полная проверка ActivityWatch: сервер + RDP-хост -# Сервер: 10.10.10.13:5600 -# RDP-хост: 192.168.100.18 (SHARKON2025) +# Сервер: 192.0.2.13:5600 +# RDP-хост: 198.51.100.18 (HOST-EXAMPLE) if [[ "${CHECK_AW_FULL_FORCE_LEGACY:-0}" != "1" ]]; then ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -16,9 +16,9 @@ if [[ "${CHECK_AW_FULL_FORCE_LEGACY:-0}" != "1" ]]; then done fi -SERVER="http://10.10.10.13:5600" -HOSTNAME_FILTER="SHARKON2025" -RDP_HOST="192.168.100.18" +SERVER="http://192.0.2.13:5600" +HOSTNAME_FILTER="HOST-EXAMPLE" +RDP_HOST="198.51.100.18" NOW=$(date -u +%s) HOST_INACTIVE=false GUARD_HEALTHY=false @@ -100,7 +100,7 @@ echo "" # 1. Проверка сервера echo -e "${CYAN}--- 1. AW Server ($SERVER) ---${NC}" echo -n " Connectivity... " -RESP=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/info" 2>&1) +RESP=$(no_proxy=192.0.2.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/info" 2>&1) if [ $? -eq 0 ] && echo "$RESP" | jq -e '.version' > /dev/null 2>&1; then VERSION=$(echo "$RESP" | jq -r '.version') echo -e " ${GREEN}OK${NC} (aw-server $VERSION)" @@ -110,7 +110,7 @@ else fi echo -n " CORS... " -CORS_RESP=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 -o /dev/null -w '%{http_code}' -H "Origin: http://10.10.10.13:5600" "$SERVER/api/0/settings/" 2>&1) +CORS_RESP=$(no_proxy=192.0.2.13 curl -s --connect-timeout 10 --max-time 15 -o /dev/null -w '%{http_code}' -H "Origin: http://192.0.2.13:5600" "$SERVER/api/0/settings/" 2>&1) if [ "$CORS_RESP" = "200" ]; then echo -e "${GREEN}OK${NC}" else @@ -119,7 +119,7 @@ fi echo "" # 1b. Context for inactive/event-driven classification -WORKTIME_EVENT_DATA=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/aw-worktime-sessions_$HOSTNAME_FILTER/events?limit=1" 2>&1) +WORKTIME_EVENT_DATA=$(no_proxy=192.0.2.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/aw-worktime-sessions_$HOSTNAME_FILTER/events?limit=1" 2>&1) WORKTIME_TS=$(echo "$WORKTIME_EVENT_DATA" | jq -r '.[0].timestamp // ""' 2>/dev/null) WORKTIME_ACTIVE=$(echo "$WORKTIME_EVENT_DATA" | jq -r '.[0].data.active // false' 2>/dev/null) if [ -n "$WORKTIME_TS" ]; then @@ -132,7 +132,7 @@ if [ -n "$WORKTIME_TS" ]; then fi fi -GUARD_EVENT_DATA=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/aw-rus-collector-guard_$HOSTNAME_FILTER/events?limit=1" 2>&1) +GUARD_EVENT_DATA=$(no_proxy=192.0.2.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/aw-rus-collector-guard_$HOSTNAME_FILTER/events?limit=1" 2>&1) GUARD_TS=$(echo "$GUARD_EVENT_DATA" | jq -r '.[0].timestamp // ""' 2>/dev/null) GUARD_STATUS=$(echo "$GUARD_EVENT_DATA" | jq -r '.[0].data.status // ""' 2>/dev/null) GUARD_PROBLEMS=$(echo "$GUARD_EVENT_DATA" | jq -r '([.[0].data.problems[]?] | length) // 0' 2>/dev/null) @@ -167,7 +167,7 @@ for entry in "${BUCKETS[@]}"; do label="${entry##*|}" bucket_full="${bucket}_${HOSTNAME_FILTER}" - EVENT_DATA=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/$bucket_full/events?limit=1" 2>&1) + EVENT_DATA=$(no_proxy=192.0.2.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/$bucket_full/events?limit=1" 2>&1) LAST_ID=$(echo "$EVENT_DATA" | jq '.[0].id // 0') LAST_TS=$(echo "$EVENT_DATA" | jq -r '.[0].timestamp // "no events"') @@ -231,7 +231,7 @@ DEAD_COUNT=0 for entry in "${BUCKETS[@]}"; do bucket="${entry%%|*}" bucket_full="${bucket}_${HOSTNAME_FILTER}" - EVENT_DATA=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/$bucket_full/events?limit=1" 2>&1) + EVENT_DATA=$(no_proxy=192.0.2.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/$bucket_full/events?limit=1" 2>&1) LAST_TS=$(echo "$EVENT_DATA" | jq -r '.[0].timestamp // "no events"') if [ "$LAST_TS" != "no events" ] && [ -n "$LAST_TS" ]; then diff --git a/clickhouse-1c/.env.example b/clickhouse-1c/.env.example index 884a039..565f442 100644 --- a/clickhouse-1c/.env.example +++ b/clickhouse-1c/.env.example @@ -7,7 +7,7 @@ CLICKHOUSE_HOST=clickhouse AW_1C_CLICKHOUSE_RUNTIME_HOST=127.0.0.1 AW_1C_COMPANY_API_HOST=127.0.0.1 AW_1C_COMPANY_API_PORT=8710 -AW_1C_MANAGER_BRIEF_GRAFANA_URL=http://10.10.10.11:3000/dashboards/f/file-1c/?orgId=1 +AW_1C_MANAGER_BRIEF_GRAFANA_URL=http://192.0.2.11:3000/dashboards/f/file-1c/?orgId=1 AW_1C_COMPANY_LOOKBACK_DAYS=30 AW_1C_COMPANY_MIN_DAYS=1 AW_1C_COMPANY_HORIZONS=7,30 diff --git a/clickhouse-1c/README.md b/clickhouse-1c/README.md index d777be3..8e9a6f6 100644 --- a/clickhouse-1c/README.md +++ b/clickhouse-1c/README.md @@ -156,7 +156,7 @@ python ai/generate_manager_brief.py --host localhost --port 8123 --user default Этот слой делает не raw LLM-чат, а промышленный pipeline: - строит компактный context из `v_company_portfolio_overview`; -- вызывает локальный `codex exec` на `10.10.10.2`; +- вызывает локальный `codex exec` на `192.0.2.2`; - валидирует structured output по JSON schema; - при сбое `codex` отдаёт deterministic fallback, чтобы контур не пустел; - пишет `latest.json` и `latest.md` в `state/manager-brief/`. diff --git a/clickhouse-1c/ai/company_intelligence_api.py b/clickhouse-1c/ai/company_intelligence_api.py index f591802..2016562 100644 --- a/clickhouse-1c/ai/company_intelligence_api.py +++ b/clickhouse-1c/ai/company_intelligence_api.py @@ -459,7 +459,7 @@ def build_company_recovery_context(summary_payload: dict[str, Any], infobase: st def grafana_company_dashboard_url() -> str: return os.getenv( "AW_1C_MANAGER_BRIEF_GRAFANA_URL", - "http://10.10.10.11:3000/d/1c-file-mgmt/1c-file-management-board", + "http://192.0.2.11:3000/d/1c-file-mgmt/1c-file-management-board", ) diff --git a/clickhouse-1c/grafana/provisioning/dashboards/files/1c-financial-reporting.json b/clickhouse-1c/grafana/provisioning/dashboards/files/1c-financial-reporting.json index 8990daf..c3cfda6 100644 --- a/clickhouse-1c/grafana/provisioning/dashboards/files/1c-financial-reporting.json +++ b/clickhouse-1c/grafana/provisioning/dashboards/files/1c-financial-reporting.json @@ -17,7 +17,7 @@ }, { "title": "Manager Brief", - "url": "http://10.10.10.2:8710/manager/brief" + "url": "http://192.0.2.2:8710/manager/brief" } ], "panels": [ diff --git a/clickhouse-1c/grafana/provisioning/dashboards/files/1c-management-board.json b/clickhouse-1c/grafana/provisioning/dashboards/files/1c-management-board.json index 4484f6d..ece75b9 100644 --- a/clickhouse-1c/grafana/provisioning/dashboards/files/1c-management-board.json +++ b/clickhouse-1c/grafana/provisioning/dashboards/files/1c-management-board.json @@ -9,19 +9,19 @@ "links": [ { "title": "Manager Brief", - "url": "http://10.10.10.2:8710/manager/brief" + "url": "http://192.0.2.2:8710/manager/brief" }, { "title": "Manager Actions", - "url": "http://10.10.10.2:8710/manager/actions" + "url": "http://192.0.2.2:8710/manager/actions" }, { "title": "Weekly Digest", - "url": "http://10.10.10.2:8710/manager/digest/weekly" + "url": "http://192.0.2.2:8710/manager/digest/weekly" }, { "title": "Recovery", - "url": "http://10.10.10.2:8710/manager/recovery" + "url": "http://192.0.2.2:8710/manager/recovery" }, { "title": "Company Intelligence", diff --git a/clickhouse-1c/grafana/provisioning/dashboards/files/1c-telemetry-board.json b/clickhouse-1c/grafana/provisioning/dashboards/files/1c-telemetry-board.json index 1b6f778..b7c9101 100644 --- a/clickhouse-1c/grafana/provisioning/dashboards/files/1c-telemetry-board.json +++ b/clickhouse-1c/grafana/provisioning/dashboards/files/1c-telemetry-board.json @@ -21,7 +21,7 @@ }, { "title": "Manager Brief", - "url": "http://10.10.10.2:8710/manager/brief" + "url": "http://192.0.2.2:8710/manager/brief" } ], "panels": [ diff --git a/clickhouse-1c/sample/seed_demo.sql b/clickhouse-1c/sample/seed_demo.sql index 1957314..2058f8b 100644 --- a/clickhouse-1c/sample/seed_demo.sql +++ b/clickhouse-1c/sample/seed_demo.sql @@ -8,14 +8,14 @@ INSERT INTO analytics_1c.postings (ts, infobase, registrar, operation_type, acco ('2026-05-21 10:16:00','ФЕЛИЦТ ГРУПП 2026','DOC-3','adjustment','91.02','62.01',54000.00,'demo-postings.jsonl'); INSERT INTO analytics_1c.reglog_events (ts, infobase, user, host, app, event_name, level, duration_ms, message, source_file) VALUES -('2026-05-21 07:15:00','ТРАНСГАЗ 2026','USER1','SHARKON2025','1cv8c','Login','warn',0,'Вход вне рабочего времени','demo-reglog.jsonl'), -('2026-05-21 10:20:00','ФЕЛИЦТ ГРУПП 2026','USER4','SHARKON2025','1cv8c','PostingError','error',4200,'Ошибка проведения документа','demo-reglog.jsonl'), -('2026-05-21 10:25:00','ФЕЛИЦТ ГРУПП 2026','USER4','SHARKON2025','1cv8c','ExchangeFailure','error',6100,'Ошибка обмена с внешней системой','demo-reglog.jsonl'); +('2026-05-21 07:15:00','ТРАНСГАЗ 2026','USER1','HOST-EXAMPLE','1cv8c','Login','warn',0,'Вход вне рабочего времени','demo-reglog.jsonl'), +('2026-05-21 10:20:00','ФЕЛИЦТ ГРУПП 2026','USER4','HOST-EXAMPLE','1cv8c','PostingError','error',4200,'Ошибка проведения документа','demo-reglog.jsonl'), +('2026-05-21 10:25:00','ФЕЛИЦТ ГРУПП 2026','USER4','HOST-EXAMPLE','1cv8c','ExchangeFailure','error',6100,'Ошибка обмена с внешней системой','demo-reglog.jsonl'); INSERT INTO analytics_1c.audit_events (ts, infobase, user, object_type, object_id, action, before_hash, after_hash, risk_tag, source_file) VALUES ('2026-05-21 10:17:00','ФЕЛИЦТ ГРУПП 2026','USER4','document','DOC-3','repost','abc','def','repost','demo-audit.jsonl'), ('2026-05-21 10:18:00','ФЕЛИЦТ ГРУПП 2026','USER4','counterparty','CP-77','change','old','new','critical_ref','demo-audit.jsonl'); INSERT INTO analytics_1c.host_events (ts, host, cpu_pct, ram_pct, disk_free_gb, disk_latency_ms, smb_errors, rdp_sessions, backup_ok, source_file) VALUES -('2026-05-21 10:00:00','SHARKON2025',41.2,68.4,120.0,12.5,0,4,1,'demo-host.jsonl'), -('2026-05-21 11:00:00','SHARKON2025',57.8,72.0,118.0,61.0,2,4,0,'demo-host.jsonl'); +('2026-05-21 10:00:00','HOST-EXAMPLE',41.2,68.4,120.0,12.5,0,4,1,'demo-host.jsonl'), +('2026-05-21 11:00:00','HOST-EXAMPLE',57.8,72.0,118.0,61.0,2,4,0,'demo-host.jsonl'); diff --git a/detmir-mcp/.env.example b/detmir-mcp/.env.example index 95d9b21..4309ff5 100644 --- a/detmir-mcp/.env.example +++ b/detmir-mcp/.env.example @@ -1,12 +1,12 @@ -DETMIR_AW_BASE=http://10.10.10.13:5600 -DETMIR_WORKTIME_BASE=http://10.10.10.13:5610 -DETMIR_DLP_POLICY_BASE=http://10.10.10.13:5601 -DETMIR_DLP_CASE_BASE=http://10.10.10.13:5602 -DETMIR_ONEC_BASE=http://10.10.10.2:8710 -DETMIR_GRAFANA_URL=http://10.10.10.11:3000 +DETMIR_AW_BASE=http://192.0.2.13:5600 +DETMIR_WORKTIME_BASE=http://192.0.2.13:5610 +DETMIR_DLP_POLICY_BASE=http://192.0.2.13:5601 +DETMIR_DLP_CASE_BASE=http://192.0.2.13:5602 +DETMIR_ONEC_BASE=http://192.0.2.2:8710 +DETMIR_GRAFANA_URL=http://192.0.2.11:3000 DETMIR_GRAFANA_USER= DETMIR_GRAFANA_PASSWORD= -DETMIR_DEFAULT_HOST=SHARKON2025 +DETMIR_DEFAULT_HOST=HOST-EXAMPLE DETMIR_HTTP_TIMEOUT_SECONDS=10 DETMIR_MCP_TRANSPORT=stdio DETMIR_MCP_HOST=127.0.0.1 diff --git a/detmir-mcp/examples/mcpdrill-detmir-readonly.json b/detmir-mcp/examples/mcpdrill-detmir-readonly.json index 751d4da..6670cc0 100644 --- a/detmir-mcp/examples/mcpdrill-detmir-readonly.json +++ b/detmir-mcp/examples/mcpdrill-detmir-readonly.json @@ -95,7 +95,7 @@ "tool_name": "worktime_today", "weight": 3, "arguments": { - "host": "SHARKON2025", + "host": "HOST-EXAMPLE", "day": "today", "top_n": 5 } @@ -105,7 +105,7 @@ "tool_name": "worktime_management", "weight": 2, "arguments": { - "host": "SHARKON2025", + "host": "HOST-EXAMPLE", "day": "today", "top_n": 5 } @@ -115,7 +115,7 @@ "tool_name": "dlp_health_summary", "weight": 2, "arguments": { - "host": "SHARKON2025" + "host": "HOST-EXAMPLE" } }, { diff --git a/detmir-mcp/main.py b/detmir-mcp/main.py index 4a7b20c..8b4702b 100644 --- a/detmir-mcp/main.py +++ b/detmir-mcp/main.py @@ -26,15 +26,15 @@ def _env_int(name: str, default: int) -> int: PROJECT_ROOT = Path(__file__).resolve().parent.parent GRAFANA_DASHBOARDS_DIR = PROJECT_ROOT / "grafana" -AW_BASE = _env("DETMIR_AW_BASE", "http://10.10.10.13:5600").rstrip("/") -WORKTIME_BASE = _env("DETMIR_WORKTIME_BASE", "http://10.10.10.13:5610").rstrip("/") -DLP_POLICY_BASE = _env("DETMIR_DLP_POLICY_BASE", "http://10.10.10.13:5601").rstrip("/") -DLP_CASE_BASE = _env("DETMIR_DLP_CASE_BASE", "http://10.10.10.13:5602").rstrip("/") -ONEC_BASE = _env("DETMIR_ONEC_BASE", "http://10.10.10.2:8710").rstrip("/") -GRAFANA_URL = _env("DETMIR_GRAFANA_URL", "http://10.10.10.11:3000").rstrip("/") +AW_BASE = _env("DETMIR_AW_BASE", "http://192.0.2.13:5600").rstrip("/") +WORKTIME_BASE = _env("DETMIR_WORKTIME_BASE", "http://192.0.2.13:5610").rstrip("/") +DLP_POLICY_BASE = _env("DETMIR_DLP_POLICY_BASE", "http://192.0.2.13:5601").rstrip("/") +DLP_CASE_BASE = _env("DETMIR_DLP_CASE_BASE", "http://192.0.2.13:5602").rstrip("/") +ONEC_BASE = _env("DETMIR_ONEC_BASE", "http://192.0.2.2:8710").rstrip("/") +GRAFANA_URL = _env("DETMIR_GRAFANA_URL", "http://192.0.2.11:3000").rstrip("/") GRAFANA_USER = os.environ.get("DETMIR_GRAFANA_USER", "") GRAFANA_PASSWORD = os.environ.get("DETMIR_GRAFANA_PASSWORD", "") -DEFAULT_HOST = _env("DETMIR_DEFAULT_HOST", "SHARKON2025") +DEFAULT_HOST = _env("DETMIR_DEFAULT_HOST", "HOST-EXAMPLE") HTTP_TIMEOUT_SECONDS = _env_int("DETMIR_HTTP_TIMEOUT_SECONDS", 10) MCP_TRANSPORT = _env("DETMIR_MCP_TRANSPORT", "stdio") MCP_HOST = _env("DETMIR_MCP_HOST", "127.0.0.1") diff --git a/docs/1C_FILE_ANALYTICS_STACK_RU.md b/docs/1C_FILE_ANALYTICS_STACK_RU.md index dc2a98a..94d1b37 100644 --- a/docs/1C_FILE_ANALYTICS_STACK_RU.md +++ b/docs/1C_FILE_ANALYTICS_STACK_RU.md @@ -192,14 +192,14 @@ Grafana уже должна содержать: Проверенная рабочая учётка: -- `SHARKON2025\Администратор` +- `HOST-EXAMPLE\Администратор` ### 6.2 Команда переключения principal На ``: ```cmd -schtasks /Change /TN "\ActivityWatch File1C Upload" /RU "SHARKON2025\Администратор" /RP "" +schtasks /Change /TN "\ActivityWatch File1C Upload" /RU "HOST-EXAMPLE\Администратор" /RP "" ``` ### 6.3 Проверка diff --git a/docs/DETMIR_CHANGE_REPORT_LAST_24H_2026-05-26.md b/docs/DETMIR_CHANGE_REPORT_LAST_24H_2026-05-26.md index 2b714ec..cc21fbe 100644 --- a/docs/DETMIR_CHANGE_REPORT_LAST_24H_2026-05-26.md +++ b/docs/DETMIR_CHANGE_REPORT_LAST_24H_2026-05-26.md @@ -25,7 +25,7 @@ ### 1. Восстановление ActivityWatch-Russian Симптом: -- на `:5600` UI не обновлялся для `SHARKON2025`; +- на `:5600` UI не обновлялся для `HOST-EXAMPLE`; - `aw-server` был жив, но stale были `aw-watcher-afk`, `aw-watcher-window`, `aw-worktime-sessions`. Действия: @@ -33,7 +33,7 @@ - через WinRM на `` проверены процессы, tasks и recovery-скрипты; - запущены: - `ActivityWatch Recovery` - - `ActivityWatch Launch [SHARKON2025_user5]` + - `ActivityWatch Launch [HOST-EXAMPLE_user5]` - отдельно перезапущен зависший `worktime-session-collector`. Итог: diff --git a/docs/DETMIR_UNIFIED_OPERATING_MODEL_RU.md b/docs/DETMIR_UNIFIED_OPERATING_MODEL_RU.md index fd09c5d..f318343 100644 --- a/docs/DETMIR_UNIFIED_OPERATING_MODEL_RU.md +++ b/docs/DETMIR_UNIFIED_OPERATING_MODEL_RU.md @@ -51,7 +51,7 @@ | `` | `pfSense`, firewall, VPN, ACL, OpenVPN export target | | `` | `Proxmox/DetMirAuto`, web gateway, Telegram bot, operator entrypoint | | `` | основной `AW-rus` server, health, worktime/reporting, `Hayabusa` server-side processing | -| `` | `SHARKON2025`, Windows/RDP host, collectors, worktime session path, EVTX export | +| `` | `HOST-EXAMPLE`, Windows/RDP host, collectors, worktime session path, EVTX export | Практический вывод: diff --git a/docs/INSTALL_FOR_EXPERT_RU.md b/docs/INSTALL_FOR_EXPERT_RU.md new file mode 100644 index 0000000..e8ca727 --- /dev/null +++ b/docs/INSTALL_FOR_EXPERT_RU.md @@ -0,0 +1,336 @@ +# Установка экземпляра для эксперта + +Документ описывает воспроизводимый путь: чистая VM -> установка -> проверка -> +ожидаемый результат. Он не использует адреса, домены и учетные данные +боевого стенда. + +## 1. Цель проверки + +Эксперт должен получить развернутый экземпляр DetMir/AWatch-rus, убедиться, +что ПО собирается из исходного кода, устанавливается на чистую Linux VM, +запускает базовые сервисы и проходит smoke-проверки без приватной +инфраструктуры правообладателя. + +## 2. Минимальный стенд + +Рекомендуемая чистая VM: + +- Debian 12 или Ubuntu Server 24.04 LTS; +- 2 vCPU; +- 4 GB RAM для минимальной проверки, 8 GB RAM для проверки Grafana/Prometheus; +- 30 GB свободного диска; +- доступ в интернет для установки пакетов и Rust crates; +- пользователь с правами `sudo`; +- корректные DNS, NTP и системное время. + +Опционально для полной проверки контура: + +- отдельная Windows VM или физический Windows host для collector toolkit; +- отдельный host или VM для Grafana/Prometheus, если они не ставятся на ту же + Linux VM; +- закрытая тестовая сеть с адресами, заданными в `ansible/inventory.ini`. + +## 3. Подготовка чистой VM + +Войти на VM под пользователем с `sudo` и установить базовые инструменты: + +```bash +sudo apt-get update +sudo apt-get install -y \ + ca-certificates curl git jq unzip tar xz-utils \ + build-essential pkg-config libssl-dev sqlite3 \ + python3 python3-venv python3-pip ansible +``` + +Проверить версии: + +```bash +git --version +python3 --version +ansible --version +``` + +Ожидаемый результат: + +- команды завершаются без ошибок; +- установлен Git, Python 3 и Ansible; +- VM имеет доступ к пакетным репозиториям. + +## 4. Установка Rust toolchain + +Установить стабильный Rust toolchain: + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +. "$HOME/.cargo/env" +rustup default stable +rustc --version +cargo --version +``` + +Ожидаемый результат: + +- `rustc --version` и `cargo --version` выводят стабильную версию; +- `$HOME/.cargo/bin` доступен в текущей shell-сессии. + +## 5. Получение исходного кода + +Склонировать репозиторий и перейти в рабочую директорию: + +```bash +git clone https://github.com/igor04091968/AWatch-rus.git +cd AWatch-rus +git rev-parse --short HEAD +``` + +Ожидаемый результат: + +- репозиторий склонирован; +- команда `git rev-parse --short HEAD` выводит commit id проверяемой версии. + +## 6. Проверка публичной гигиены поставки + +Перед установкой выполнить быстрый контроль отсутствия приватных маркеров в +публичных документах: + +```bash +PRIVATE_MARKERS_REGEX='|||' +git grep -n -E "$PRIVATE_MARKERS_REGEX" -- \ + README.md docs REGISTER_RU_SOFTWARE.md PRODUCT_DESCRIPTION_RU.md \ + SECURITY_OVERVIEW_RU.md adk-rust/RUNBOOK.md || true +``` + +Ожидаемый результат: + +- команда не должна находить реальные имена хостов, личные домены и личные + пути оператора в публичных документах; +- допустимы только нейтральные placeholders вроде ``, + ``, `HOST-EXAMPLE`. + +## 7. Подготовка локальной конфигурации + +Создать приватные файлы из примеров: + +```bash +cp private-config/deploy.env.example private-config/deploy.env +cp ansible/inventory.example.ini ansible/inventory.ini +``` + +Заполнить в `private-config/deploy.env` и `ansible/inventory.ini` только +тестовые значения: + +- адрес Linux VM; +- адрес Windows host, если проверяются Windows collectors; +- адрес Grafana/Prometheus, если они вынесены на отдельную VM; +- тестовые учетные данные; +- тестовые токены Telegram/Webhook, если проверяются уведомления. + +Ожидаемый результат: + +- приватная конфигурация существует локально; +- приватные файлы не попадают в Git благодаря `.gitignore`; +- в публичных файлах не появляются реальные пароли, IP и домены. + +## 8. Сборка Rust-компонентов + +Собрать workspace: + +```bash +cd adk-rust +cargo build --release --workspace +cd .. +``` + +Ожидаемый результат: + +- сборка завершается кодом `0`; +- release-бинарники появляются в `adk-rust/target/release/`; +- отсутствуют ошибки компиляции Rust-компонентов. + +## 9. Локальные тесты до установки + +Запустить базовые проверки исходников: + +```bash +cd adk-rust +cargo fmt --all -- --check +cargo test --workspace +cd .. +``` + +Проверить Ansible syntax: + +```bash +ansible-playbook --syntax-check -i ansible/inventory.ini ansible/deploy_aw_server.yml +``` + +Ожидаемый результат: + +- `cargo fmt` не сообщает diff; +- `cargo test --workspace` завершается успешно; +- Ansible syntax-check не находит ошибок YAML/playbook. + +## 10. Установка минимального серверного экземпляра + +Для чистой экспертной VM использовать inventory, где целевой Linux host +указывает на эту же VM или на отдельный тестовый сервер. + +Пример команды: + +```bash +ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_server.yml +``` + +Если playbook требует переменные окружения или группы hosts, задать их в +локальном inventory, не меняя публичные файлы репозитория. + +Ожидаемый результат: + +- playbook завершается без failed tasks; +- systemd units установлены на тестовый Linux host; +- ActivityWatch server и DetMir helper-компоненты доступны локально на + заданных портах; +- конфигурация не содержит боевых адресов правообладателя. + +## 11. Проверка сервисов после установки + +На тестовом Linux host выполнить: + +```bash +systemctl --failed --no-pager +systemctl status activitywatch-server --no-pager +``` + +Если установлены Rust helper-бинарники: + +```bash +detmir-check --json +detmir-status --json +``` + +Ожидаемый результат: + +- `systemctl --failed` не показывает критичных failed units DetMir/AWatch-rus; +- `activitywatch-server` находится в состоянии `active`; +- `detmir-check --json` возвращает машинно-читаемый статус без критичных + ошибок; +- `detmir-status --json` показывает агрегированный статус установленного + экземпляра. + +## 12. Проверка HTTP API + +Проверить ActivityWatch API: + +```bash +curl -fsS http://127.0.0.1:5600/api/0/info | jq . +curl -fsS http://127.0.0.1:5600/api/0/buckets/ | jq 'keys | length' +``` + +Ожидаемый результат: + +- `/api/0/info` возвращает JSON; +- `/api/0/buckets/` возвращает JSON-объект; +- ошибки `connection refused`, `403`, `500` отсутствуют. + +## 13. Проверка Grafana/Prometheus + +Если в экспертном стенде установлены Grafana/Prometheus: + +```bash +curl -fsS http://127.0.0.1:3000/api/health | jq . +curl -fsS http://127.0.0.1:9090/-/ready +``` + +Ожидаемый результат: + +- Grafana health API возвращает статус `ok` или `database: ok`; +- Prometheus ready endpoint возвращает успешный HTTP status; +- DetMir dashboards импортированы или доступны через documented provisioning + path. + +## 14. Проверка Windows collectors + +Этот шаг нужен только для полной проверки контура. + +На Windows host выполнить PowerShell deployment из `windows/` или Ansible +playbook для Windows collectors, используя тестовый domain/hostname: + +```powershell +Get-ScheduledTask | Where-Object TaskName -like 'ActivityWatch*' +``` + +На Linux server проверить появление buckets: + +```bash +curl -fsS http://127.0.0.1:5600/api/0/buckets/ | jq 'keys[]' | sort +``` + +Ожидаемый результат: + +- Windows tasks созданы с тестовыми именами host/user; +- ActivityWatch buckets получают события от Windows collectors; +- в bucket names нет приватных имен боевого стенда. + +## 15. Smoke-проверка портала оператора + +Если установлен portal runtime: + +```bash +curl -fsS http://127.0.0.1:8080/health +``` + +Ожидаемый результат: + +- health endpoint возвращает успешный HTTP status; +- карточки портала открываются по локальным адресам стенда; +- ссылки на Grafana/ActivityWatch/incident evidence используют значения из + конфигурации, а не захардкоженные адреса. + +## 16. Итоговый критерий приемки + +Экземпляр считается установленным корректно, если: + +- исходный код собирается командой `cargo build --release --workspace`; +- локальные Rust tests проходят; +- Ansible syntax-check проходит; +- серверный playbook завершается без failed tasks; +- ActivityWatch API отвечает JSON; +- `systemctl --failed` не показывает критичных failures; +- `detmir-check` и `detmir-status` отрабатывают; +- публичные документы не содержат приватных IP, доменов, hostnames и путей; +- third-party license inventory и release/SBOM checklist заполнены. + +## 17. Сбор диагностического пакета для эксперта + +После проверки сохранить артефакты: + +```bash +mkdir -p expert-check-output +git rev-parse HEAD > expert-check-output/commit.txt +systemctl --failed --no-pager > expert-check-output/systemd-failed.txt +curl -fsS http://127.0.0.1:5600/api/0/info > expert-check-output/aw-info.json +detmir-check --json > expert-check-output/detmir-check.json || true +detmir-status --json > expert-check-output/detmir-status.json || true +``` + +Ожидаемый результат: + +- в `expert-check-output/` есть commit id, systemd summary и JSON-проверки; +- пакет не содержит паролей, токенов и приватных доменов. + +## 18. Очистка тестового экземпляра + +Для удаления тестовой VM достаточно уничтожить саму VM. Если нужно очистить +только сервисы внутри VM, сначала сохранить diagnostic output, затем остановить +установленные units: + +```bash +sudo systemctl stop activitywatch-server || true +sudo systemctl list-units 'aw-*' 'detmir-*' --no-pager +``` + +Ожидаемый результат: + +- экспертский стенд можно воспроизвести заново из исходников и локальной + конфигурации; +- очистка не требует доступа к боевому стенду правообладателя. diff --git a/docs/SBOM_RELEASE_CHECKLIST_RU.md b/docs/SBOM_RELEASE_CHECKLIST_RU.md new file mode 100644 index 0000000..ade21eb --- /dev/null +++ b/docs/SBOM_RELEASE_CHECKLIST_RU.md @@ -0,0 +1,277 @@ +# SBOM и release checklist + +Документ фиксирует минимальный порядок подготовки публичного релиза +DetMir/AWatch-rus: исходники, сборка, артефакты, сторонние компоненты, +лицензии, публичная гигиена и экспертная проверка. + +## 1. Идентификация релиза + +Перед сборкой зафиксировать: + +- product name: `DetMir, программный комплекс AWatch-rus`; +- repository: `AWatch-rus`; +- release tag: `vX.Y.Z`; +- commit: вывод `git rev-parse HEAD`; +- дата сборки; +- ответственный правообладатель/maintainer; +- состав release assets. + +Команды: + +```bash +git status --short +git rev-parse HEAD +git tag --points-at HEAD +``` + +Критерий: + +- рабочее дерево не содержит незапланированных tracked-изменений; +- tag указывает на проверенный commit; +- release notes соответствуют фактическому составу поставки. + +## 2. Публичная гигиена репозитория + +Проверить отсутствие приватных идентификаторов в публичных документах: + +```bash +PRIVATE_MARKERS_REGEX='|||' +git grep -n -E "$PRIVATE_MARKERS_REGEX" -- \ + README.md docs REGISTER_RU_SOFTWARE.md PRODUCT_DESCRIPTION_RU.md \ + SECURITY_OVERVIEW_RU.md adk-rust/RUNBOOK.md || true +``` + +Проверить реальные IP в release-facing документах: + +```bash +git grep -n -E '10\\.10\\.10\\.|192\\.168\\.' -- \ + README.md docs REGISTER_RU_SOFTWARE.md PRODUCT_DESCRIPTION_RU.md \ + SECURITY_OVERVIEW_RU.md adk-rust/RUNBOOK.md || true +``` + +Критерий: + +- в документах нет приватных IP, доменов, usernames и live hostnames; +- допускаются только placeholders: ``, ``, + ``, `HOST-EXAMPLE`, `WINDOWS_USER_EXAMPLE`; +- приватные configs находятся только в ignored-файлах. + +## 3. Контроль секретов + +Проверить, что в репозиторий не попали secrets: + +```bash +git ls-files | grep -E '(^|/)secrets(/|$)|\\.env$|inventory\\.ini$' || true +git grep -n -E 'sk_[A-Za-z0-9]|pk_[A-Za-z0-9]|BEGIN OPENSSH PRIVATE KEY|password\\s*=' -- . || true +``` + +Рекомендуемый дополнительный контроль: + +```bash +gitleaks detect --source . --no-git --redact +``` + +Критерий: + +- директория `secrets/` не отслеживается Git; +- `.env`, `inventory.ini`, tokens, passwords и private keys отсутствуют в + tracked-файлах; +- если секрет ранее был опубликован, он ротируется вне этого checklist. + +## 4. Сборка Rust workspace + +Собрать Rust-компоненты: + +```bash +cd adk-rust +cargo fmt --all -- --check +cargo test --workspace +cargo clippy --workspace --all-targets -- -D warnings +cargo build --release --workspace +cd .. +``` + +Критерий: + +- formatting clean; +- tests green; +- clippy без warnings; +- release-бинарники собраны. + +## 5. Проверка Python-исключений + +Python в проекте допускается только как: + +- Telegram runtime, который решено оставить на Python; +- совместимые legacy fallbacks; +- installer/ops helpers, пока они не являются ядром продукта; +- тестовые и migration utilities. + +Проверка: + +```bash +git ls-files '*.py' +``` + +Критерий: + +- Python-файлы имеют понятную роль; +- ядро DetMir/AWatch-rus позиционируется как Rust-first; +- README/registry docs не обещают отсутствие Python там, где он еще остается. + +## 6. Third-party license inventory + +Основной license inventory: + +- [`../THIRD_PARTY_LICENSES_RU.md`](../THIRD_PARTY_LICENSES_RU.md) + +Проверить минимум: + +- ActivityWatch components: MPL-2.0 или лицензии конкретных upstream parts; +- Grafana: AGPL-3.0 или актуальная лицензия используемой версии; +- Prometheus: Apache-2.0; +- Hayabusa: лицензия upstream и правила распространения; +- Ansible: GPL-3.0-or-later; +- Rust crates: по `cargo metadata`, `cargo about`, `cargo deny`; +- Python dependencies: по `pip-licenses` или lock-файлам; +- JavaScript/Node dependencies для Playwright/UI helpers: по `npm ls` и + package metadata. + +Критерий: + +- для каждого крупного компонента указан upstream, license, роль и риск; +- copyleft-компоненты не скрыты; +- release notes не противоречат лицензиям. + +## 7. SBOM + +Сформировать машинные перечни зависимостей. + +Rust: + +```bash +cd adk-rust +cargo metadata --format-version 1 > ../sbom-cargo-metadata.json +cargo tree --workspace > ../sbom-cargo-tree.txt +cd .. +``` + +Python, если используется виртуальное окружение: + +```bash +python3 -m pip freeze > sbom-python-freeze.txt +``` + +Node/Playwright, если используется frontend smoke tooling: + +```bash +npm ls --all --json > sbom-npm-tree.json +``` + +OS packages на эталонной VM: + +```bash +dpkg-query -W -f='${Package}\\t${Version}\\n' > sbom-debian-packages.tsv +``` + +Критерий: + +- SBOM artifacts приложены к release assets или сохранены в build archive; +- SBOM не содержит секретов; +- SBOM соответствует проверяемому commit/tag. + +## 8. Release assets + +Собрать install-kit и бинарные артефакты только из проверенного commit: + +```bash +scripts/rebuild_install_kit.sh +scripts/validate_install_kit.sh +``` + +Критерий: + +- dated zip/tar.gz не лежат в корне tracked-репозитория; +- архивы публикуются как GitHub Release assets; +- checksum каждого asset зафиксирован. + +Пример фиксации checksum: + +```bash +sha256sum dist/* install-kit-awindows-*.zip install-kit-awindows-*.tar.gz \ + > SHA256SUMS +``` + +## 9. Документы для реестра российского ПО + +Проверить наличие и актуальность: + +- `REGISTER_RU_SOFTWARE.md`; +- `PRODUCT_DESCRIPTION_RU.md`; +- `THIRD_PARTY_LICENSES_RU.md`; +- `docs/INSTALL_FOR_EXPERT_RU.md`; +- `docs/ARCHITECTURE_RU.md`; +- `docs/ADMIN_GUIDE_RU.md`; +- `docs/OPERATOR_GUIDE_RU.md`; +- `docs/OWNERSHIP_RU.md`; +- `docs/REGISTRY_CHECKLIST_RU.md`. + +Критерий: + +- документы согласованы по названию продукта; +- не заявляются DLP/SIEM/EDR/XDR/сертифицированная СЗИ как основной класс; +- позиционирование: операционный контроль, технический аудит, автоматизация + эксплуатации ИТ-инфраструктуры. + +## 10. Экспертная установка + +Проверить релиз на чистой VM по: + +- [`INSTALL_FOR_EXPERT_RU.md`](INSTALL_FOR_EXPERT_RU.md) + +Критерий: + +- чистая VM проходит путь установка -> сборка -> проверка; +- ActivityWatch API отвечает; +- базовые DetMir checks работают; +- diagnostic bundle собран; +- инструкция воспроизводима без доступа к личному стенду разработчика. + +## 11. GitHub metadata + +Проверить публичную страницу репозитория: + +- description заполнен; +- website/homepage заполнен; +- topics заполнены; +- license отображается; +- releases содержат бинарные/install-kit assets; +- root README не содержит приватных адресов и личных путей. + +Критерий: + +- проект выглядит как поставляемое ПО, а не как личный стенд; +- About/Topics/Website заполнены; +- license detected на GitHub. + +## 12. Финальный gate перед публикацией + +Команды: + +```bash +git diff --check +git status --short +PRIVATE_MARKERS_REGEX='|||' +git grep -n -E "$PRIVATE_MARKERS_REGEX" -- \ + README.md docs REGISTER_RU_SOFTWARE.md PRODUCT_DESCRIPTION_RU.md \ + SECURITY_OVERVIEW_RU.md adk-rust/RUNBOOK.md || true +``` + +Критерий публикации: + +- diff не содержит whitespace errors; +- tracked changes входят в один понятный commit; +- release-facing docs обезличены; +- install-kit artifacts вынесены в Release assets; +- SBOM/license checklist приложен или воспроизводим; +- tag создан только после прохождения проверок. diff --git a/docs/dlp-content-analysis-runtime-status-2026-05-13.md b/docs/dlp-content-analysis-runtime-status-2026-05-13.md index 5785b67..900915e 100644 --- a/docs/dlp-content-analysis-runtime-status-2026-05-13.md +++ b/docs/dlp-content-analysis-runtime-status-2026-05-13.md @@ -10,7 +10,7 @@ This document records the production-verified state of advanced content analysis - `contentAnalysis.regexPack` - `contentAnalysis.ocrEnabled` - `ioc.*` -- Historical incidents in `aw-dlp-incidents_SHARKON2025` already contain enriched fields: +- Historical incidents in `aw-dlp-incidents_HOST-EXAMPLE` already contain enriched fields: - `dictionaryMatches` - `regexMatches` - `ocrRequested` diff --git a/docs/dlp-runtime-chain-status-2026-05-13.md b/docs/dlp-runtime-chain-status-2026-05-13.md index dba8c7d..b384d4b 100644 --- a/docs/dlp-runtime-chain-status-2026-05-13.md +++ b/docs/dlp-runtime-chain-status-2026-05-13.md @@ -27,8 +27,8 @@ Verified on ``: - recent journal runs are clean - current runtime result is `sent=0` / `delivered=0` because no new incidents were generated since the last seen bucket event - `endpoint -> incident ingest` - - `aw-dlp-endpoint-signals_SHARKON2025` fresh - - `aw-dlp-incidents_SHARKON2025` exists and contains valid historical incidents + - `aw-dlp-endpoint-signals_HOST-EXAMPLE` fresh + - `aw-dlp-incidents_HOST-EXAMPLE` exists and contains valid historical incidents - `health/admin` - `/usr/local/bin/dlp-health-check --json` = `ok=true` - `/usr/local/bin/dlp-admin-cli.py health check` = policy/cases/aw OK diff --git a/docs/wiki/Windows-Collector-Suite.md b/docs/wiki/Windows-Collector-Suite.md index 37d3254..53d3ee3 100644 --- a/docs/wiki/Windows-Collector-Suite.md +++ b/docs/wiki/Windows-Collector-Suite.md @@ -42,13 +42,13 @@ aw_windows_builtin_administrator_name: "Администратор" Назначение: явно фиксировать локализованное имя встроенной учетной записи Administrator с SID `*-500`. -Для текущего Windows host `SHARKON2025` task name должен строиться как: +Для текущего Windows host `HOST-EXAMPLE` task name должен строиться как: ```text -ActivityWatch Launch [SHARKON2025_Администратор] +ActivityWatch Launch [HOST-EXAMPLE_Администратор] ``` -Если task по `SHARKON2025_Administrator` не найден, recovery/deploy path обязан пробовать кириллическое имя `Администратор`. Это зафиксировано через: +Если task по `HOST-EXAMPLE_Administrator` не найден, recovery/deploy path обязан пробовать кириллическое имя `Администратор`. Это зафиксировано через: - default vars в `ansible/deploy_aw_windows.yml`; - `ansible/group_vars/aw_windows.yml`; @@ -60,7 +60,7 @@ ActivityWatch Launch [SHARKON2025_Администратор] `ActivityWatch.Windows.Common.psm1` усилил recovery path: -- `Get-ActivityWatchBuiltInAdministratorName` сначала смотрит env override, затем SID-500 lookup, затем host-specific fallback `SHARKON2025 -> Администратор`; +- `Get-ActivityWatchBuiltInAdministratorName` сначала смотрит env override, затем SID-500 lookup, затем host-specific fallback `HOST-EXAMPLE -> Администратор`; - `Normalize-ActivityWatchUsers` стабилизирован для pipeline/list cases; - удаление scheduled tasks стало устойчивее к частично удаленным task definitions; - recovery task может ориентироваться на live interactive session и запускаться в interactive logon context, когда это безопаснее для watcher'ов. diff --git a/docs/windows-deploy-startup-model.md b/docs/windows-deploy-startup-model.md index f4bd300..11ccbe8 100644 --- a/docs/windows-deploy-startup-model.md +++ b/docs/windows-deploy-startup-model.md @@ -4,7 +4,7 @@ ### 1. Multi-user RDP host -Use this model on `SHARKON2025`-style hosts with multiple user sessions. +Use this model on `HOST-EXAMPLE`-style hosts with multiple user sessions. - `AWatchRusCollectorGuard` service: - runs under `LocalSystem` diff --git a/docs/windows/deployment.md b/docs/windows/deployment.md index 7a1f64b..c9be17c 100755 --- a/docs/windows/deployment.md +++ b/docs/windows/deployment.md @@ -191,7 +191,7 @@ Ansible playbook `ansible/deploy_aw_windows.yml` выполняет этот mig .\windows\deploy-domain-users.ps1 ` -ServerHost ` -ServerPort 5600 ` - -Domain SHARKON2025 ` + -Domain HOST-EXAMPLE ` -Users user2,user3,user4,user5 ` -InstallRoot 'C:\Program Files\AWatch-rus\bin' ` -StateRoot 'C:\ProgramData\AWatch-rus' ` @@ -205,7 +205,7 @@ Single-user pilot в таком же стиле: .\windows\deploy-single-user.ps1 ` -ServerHost ` -ServerPort 5600 ` - -TargetUser 'SHARKON2025\user1' ` + -TargetUser 'HOST-EXAMPLE\user1' ` -InstallRoot 'C:\Program Files\AWatch-rus\bin' ` -StateRoot 'C:\ProgramData\AWatch-rus' ` -CustomRulesPath C:\Program Files\AWatch-rus\windows\web-category-rules.example.json ` diff --git a/docs/windows/ensemble.md b/docs/windows/ensemble.md index eac4d9d..9d34465 100644 --- a/docs/windows/ensemble.md +++ b/docs/windows/ensemble.md @@ -20,7 +20,7 @@ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process C:\Program Files\AWatch-rus\windows\deploy-ensemble.ps1 ` -ServerHost ` -ServerPort 5600 ` - -Domain SHARKON2025 ` + -Domain HOST-EXAMPLE ` -Users user1,user2,user3,user4,user5 ` -InstallRoot 'C:\Program Files\AWatch-rus\bin' ` -StateRoot 'C:\ProgramData\AWatch-rus' ` diff --git a/docs/windows/validation.md b/docs/windows/validation.md index 72ae9e5..30a17c5 100755 --- a/docs/windows/validation.md +++ b/docs/windows/validation.md @@ -43,7 +43,7 @@ Get-ScheduledTask -TaskName 'ActivityWatch*' | Точечная проверка: ```powershell -Get-ScheduledTask | Where-Object TaskName -eq 'ActivityWatch Launch [SHARKON2025_user1]' +Get-ScheduledTask | Where-Object TaskName -eq 'ActivityWatch Launch [HOST-EXAMPLE_user1]' Get-ScheduledTask | Where-Object TaskName -eq 'ActivityWatch Recovery' ``` diff --git a/docs/worktime_aql_detmir.md b/docs/worktime_aql_detmir.md index f3f84d2..df94117 100644 --- a/docs/worktime_aql_detmir.md +++ b/docs/worktime_aql_detmir.md @@ -13,8 +13,8 @@ Подходит для расчета рабочего времени в толстых клиентах (1С, документы, админка). ```javascript -events = flood(query_bucket("aw-watcher-window_SHARKON2025")); -not_afk = flood(query_bucket("aw-watcher-afk_SHARKON2025")); +events = flood(query_bucket("aw-watcher-window_HOST-EXAMPLE")); +not_afk = flood(query_bucket("aw-watcher-afk_HOST-EXAMPLE")); not_afk = filter_keyvals(not_afk, "status", ["not-afk"]); events = filter_period_intersect(events, not_afk); @@ -41,8 +41,8 @@ RETURN = sort_by_duration(work); `rootDomain=proxmox-webui`, `categoryGroup=work`, `category=Администрирование`. ```javascript -web = flood(query_bucket("aw-detmir-web-category_SHARKON2025")); -not_afk = flood(query_bucket("aw-watcher-afk_SHARKON2025")); +web = flood(query_bucket("aw-detmir-web-category_HOST-EXAMPLE")); +not_afk = flood(query_bucket("aw-watcher-afk_HOST-EXAMPLE")); not_afk = filter_keyvals(not_afk, "status", ["not-afk"]); web = filter_period_intersect(web, not_afk); @@ -55,8 +55,8 @@ RETURN = sort_by_duration(web); ## Sanity-check: «куда уходит время» ```javascript -events = flood(query_bucket("aw-watcher-window_SHARKON2025")); -not_afk = flood(query_bucket("aw-watcher-afk_SHARKON2025")); +events = flood(query_bucket("aw-watcher-window_HOST-EXAMPLE")); +not_afk = flood(query_bucket("aw-watcher-afk_HOST-EXAMPLE")); not_afk = filter_keyvals(not_afk, "status", ["not-afk"]); events = filter_period_intersect(events, not_afk); @@ -68,7 +68,7 @@ RETURN = sort_by_duration(events); ## Замечания -- Для других хостов замените суффикс `_SHARKON2025` на нужный hostname. +- Для других хостов замените суффикс `_HOST-EXAMPLE` на нужный hostname. - Если web-поток пустой, рабочее время в браузере корректно посчитать по доменам не получится. Тогда либо: - чинить/запускать browser collector; - либо временно считать браузер в `window` как «Интернет/Браузер» без разделения на work/personal. @@ -82,7 +82,7 @@ RETURN = sort_by_duration(events); «кто и когда вообще был в активной удалённой сессии». ```javascript -sessions = flood(query_bucket("aw-worktime-sessions_SHARKON2025")); +sessions = flood(query_bucket("aw-worktime-sessions_HOST-EXAMPLE")); sessions = filter_keyvals(sessions, "active", [true]); sessions = merge_events_by_keys(sessions, ["username", "sessionName", "state"]); RETURN = sort_by_duration(sessions); diff --git a/grafana-1c/.env.example b/grafana-1c/.env.example index e13d3aa..6a35b37 100755 --- a/grafana-1c/.env.example +++ b/grafana-1c/.env.example @@ -5,8 +5,8 @@ GRAFANA_PORT=3000 PROMETHEUS_PORT=9090 SQL_EXPORTER_PORT=9399 AW_EXPORTER_PORT=9398 -AW_SERVER_HOST=10.10.10.13 +AW_SERVER_HOST=192.0.2.13 AW_SERVER_PORT=5600 AW_SERVER_SCHEME=http AW_SCRAPE_INTERVAL_SECONDS=30 -ONEC_DSN=postgres://onec_reader:change_me@10.10.10.20:5432/onec_db?sslmode=disable +ONEC_DSN=postgres://onec_reader:change_me@192.0.2.20:5432/onec_db?sslmode=disable diff --git a/grafana-1c/docker-compose.yml b/grafana-1c/docker-compose.yml index 357fdc9..3df2ec8 100755 --- a/grafana-1c/docker-compose.yml +++ b/grafana-1c/docker-compose.yml @@ -6,7 +6,7 @@ services: container_name: awrus-aw-exporter restart: unless-stopped environment: - - AW_SERVER_HOST=${AW_SERVER_HOST:-10.10.10.13} + - AW_SERVER_HOST=${AW_SERVER_HOST:-192.0.2.13} - AW_SERVER_PORT=${AW_SERVER_PORT:-5600} - AW_SERVER_SCHEME=${AW_SERVER_SCHEME:-http} - EXPORTER_PORT=9398 diff --git a/grafana-1c/sql-exporter/collectors/aw_activitywatch.py b/grafana-1c/sql-exporter/collectors/aw_activitywatch.py index a3392cc..ee6e812 100755 --- a/grafana-1c/sql-exporter/collectors/aw_activitywatch.py +++ b/grafana-1c/sql-exporter/collectors/aw_activitywatch.py @@ -16,7 +16,7 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Configuration -AW_SERVER_HOST = os.getenv("AW_SERVER_HOST", "10.10.10.13") +AW_SERVER_HOST = os.getenv("AW_SERVER_HOST", "192.0.2.13") AW_SERVER_PORT = int(os.getenv("AW_SERVER_PORT", "5600")) AW_SERVER_SCHEME = os.getenv("AW_SERVER_SCHEME", "http") AW_API_BASE = os.getenv( diff --git a/grafana/detmir-aw-main-dashboard.json b/grafana/detmir-aw-main-dashboard.json index 6090c6e..d660015 100644 --- a/grafana/detmir-aw-main-dashboard.json +++ b/grafana/detmir-aw-main-dashboard.json @@ -362,8 +362,8 @@ { "current": { "selected": false, - "text": "SHARKON2025", - "value": "SHARKON2025" + "text": "HOST-EXAMPLE", + "value": "HOST-EXAMPLE" }, "datasource": { "type": "influxdb", diff --git a/grafana/detmir-rdp-user-activity-dashboard.json b/grafana/detmir-rdp-user-activity-dashboard.json index c681397..c290aee 100644 --- a/grafana/detmir-rdp-user-activity-dashboard.json +++ b/grafana/detmir-rdp-user-activity-dashboard.json @@ -724,8 +724,8 @@ { "current": { "selected": false, - "text": "SHARKON2025", - "value": "SHARKON2025" + "text": "HOST-EXAMPLE", + "value": "HOST-EXAMPLE" }, "datasource": { "type": "influxdb", diff --git a/install-kit-awindows-20260427-211240/MANIFEST.txt b/install-kit-awindows-20260427-211240/MANIFEST.txt deleted file mode 100644 index 67d02d4..0000000 --- a/install-kit-awindows-20260427-211240/MANIFEST.txt +++ /dev/null @@ -1,59 +0,0 @@ -856d2467afb4b413ae7833ba55340db01bd807f10d5200752b95cc113758c54e install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt -556a9f7524b61309b00b641495b6daa212115411aa5a6fc82d1537658ac0e719 install-kit-awindows-20260427-211240/ansible/README.md -412bb766bbf0791c3593f38daa771d5d0aa58cc1f2d3c9010fcd4588d0fe87df install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml -ac091618a6793a9be9b52eeb78267068df16d4f57e0051b9d868813c4a641f35 install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml -cd09574fd0d55bf62d9d7df757bb206b5184ba9ffdcc200e755700e50cfa41f3 install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml -4b6f658adb98d548eedd10a0d44ff224b4d46a663d8c11e038f18597503f6c27 install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml -dbaa55e66c65bf7e4e943c8070e53d002d8a794ffcc0593d7a8a07a5f99aa306 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 -9e4939d2d951870cacab6e42c5bf0ccd03350089dc0d32b9a681948ff96754d2 install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml -7189b5205bd25313db54e5be027b0d066199e6ae34ad74be2095a1691adaf5e3 install-kit-awindows-20260427-211240/ansible/install_full_stack.yml -1abe542724e617c285deaef76514d49c83c148ddbee90bc99b800e9049b70444 install-kit-awindows-20260427-211240/ansible/inventory.example.ini -3adda7422ffbbb76be50858f2f1ed6073b6bf9ee0b57283c41d0fabd3e7c7f9e install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml -234932632092226526173b9ef906de2cce961bc95be0f37c878e48d9779e1de7 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml -a74a49371e889dc3ea404534a939f32f2dac940d8902d20770590951ab67d532 install-kit-awindows-20260427-211240/ansible/tasks/provision_ct_and_deploy_aw.yml -ba16fe9e267194459a6082045a387acb828a1d39a98e66b401ece5069ea62e64 install-kit-awindows-20260427-211240/aw-server/activitywatch-server.service -fea5734c99b516b01bfe8ebbef59972d0ef553c09d7691790b08472ecfb9602d install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh -fabcdbb933b1d3b605c5ac1234af4e10495c09f18bd50931bf4a92081fcb5023 install-kit-awindows-20260427-211240/aw-server/aw-browser-smoke.service -e59d58d4cc6b2b0dceef5b1da14ca0a1b24a202ce576ee43028f44ffe43c5ef4 install-kit-awindows-20260427-211240/aw-server/aw-browser-smoke.timer -ba88cc284d047f521427ac038c266624a6fe8493ce3e79bc27c172a2e70ac54a install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json -509759461ce0918a2dc839832812cf5f4c77b1cea1e6ffdcab0146e02598df79 install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js -699f697143f6be588c698bad9a14a45c736fe74f5e4b0e0ea0f48b569c7df396 install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.service -8ae02b42f4bc33d95268624bae79c2101298b7ec6644553a1e81f7841dda74d8 install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.timer -7580282be0a3bf057e42ca6f733a64bf0d2f51d096e02a57cde9125064ac04d1 install-kit-awindows-20260427-211240/aw-server/aw-server.env.example -18d038e67b926eca04bec725ed5e6de34b93f8330c3b30a0b07d52eab414bd03 install-kit-awindows-20260427-211240/aw-server/aw-slo-monitor.service -257cdc18f088e8848b1bee69e7f8b99aa860a0f600eebd74365a40ed94aa5ecb install-kit-awindows-20260427-211240/aw-server/aw-slo-monitor.timer -98c0bed353bbda0fa7a69df23f3b008cb0e8e70cdff6cc63330d4caf79fd3280 install-kit-awindows-20260427-211240/aw-server/aw-sw-cleanup.js -0e555853f1cb498e63b70d305ce5facfaa626a2ceee6f7edb854cf37c4371f65 install-kit-awindows-20260427-211240/aw-server/aw-worktime-api.service -afbb4be301b4940ed9b7671be3441a48d53bee968d8870749442f81c6d066650 install-kit-awindows-20260427-211240/aw-server/aw-worktime-panel.js -7e37dd8bbfdbbe661defb9ce69284db8ad339a49b34f8c507060d0dbc75b7aa8 install-kit-awindows-20260427-211240/aw-server/aw-worktime-prewarm.service -0cd38bae7ad1b4718d7decc8ef3ac932939a6e5f8c25e9e27d1d0ecf8cc328b9 install-kit-awindows-20260427-211240/aw-server/aw-worktime-prewarm.timer -6de9b272ad732ad174c32398ab8baf5cc8c83be47efe0d6df46c3190b4010680 install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh -aedffecfa24834968742cb2477faef80bf794345275a9679ac12c5a1f609acc2 install-kit-awindows-20260427-211240/aw-server/settings/classes-worktime.json -47c50054515506b72af2d6bca0ae959ca57861803e528991c230611e6a8893d6 install-kit-awindows-20260427-211240/aw-server/settings/views-default.json -2e728f4632e6564355a580ea1718d41a660dc35595ac38d0ac7fe0dd308dae6a install-kit-awindows-20260427-211240/scripts/aw-webui-browser-smoke.mjs -76a24d19440676d447fac16e4a663f34c3f31d62166a6c326b5bd3754beac0eb install-kit-awindows-20260427-211240/scripts/aw-webui-browser-smoke.sh -6770275fac17607770653a522a64cf5ab31a7a11993b68e7e80a2858c3b8930e install-kit-awindows-20260427-211240/scripts/check_install_kit_vs_repo.sh -4e5bc6ce977e9f5b9e87c393061d5ecac0f90d5e1314e3325093692e5d19965e install-kit-awindows-20260427-211240/scripts/quality-gate.sh -db1a9b0ccd21aec78d8e2aa08dfa28368e5ba6209c57361e6824e85c1af9f6ca install-kit-awindows-20260427-211240/scripts/rebuild_install_kit.sh -38ad2e112796c2fa9bdc1cc2403c679bb896f90ae2b51d6ccc9a9ec103b4dea6 install-kit-awindows-20260427-211240/scripts/validate_install_kit.sh -0b49e6db51d5abcecaeee0b85f186efd4f10d3360cf04e8aea2e4c7d4465dbda install-kit-awindows-20260427-211240/scripts/verify_innosetup_installer.sh -e856d4b411b61dfc11e0a59b59026eff10972a47e7d8b28f6dfe367a2a5e192d install-kit-awindows-20260427-211240/windows/AWatchRusCollectorGuardService.cs -33aa34b89246d6c079ef9afe2f5cd153bd9d5946b69a175ff6fd678c77f61da5 install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1 -6d5a7dd92619bbf972e17e455a7cda65b53c790f6f0ede47e1196246bfe8b1cf install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 -f4405a959a52e21185234463887a4fc09ea28e48d2412ac336f9715b46be9cb9 install-kit-awindows-20260427-211240/windows/aw-collector-guard.ps1 -81e95c7e4b7336a2e1f0caed3ea99f6cdb9696b99380dc2148667b5fba577f68 install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 -51eb13f689494c279ec321bbb68dfcb9b8ff81f22f11502b72c16895b65e0b65 install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 -5bacbd48fdfc8d0d4479ba1af54cbbfe6057cc654409b385f487962576f4f1db install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 -6ab4c496220bb46f064cc927a64ea853526a2c814182a32f39b432be6b32fe89 install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 -73f3328cde22798dafa645cdd78d648ccf6425d6577432ae8c95d24daa17dc94 install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 -b97dfbd94201c83f3edd87c7940b058bf85ac64794468b44ab3200dcb1bff3c3 install-kit-awindows-20260427-211240/windows/dlp-policy.example.json -0df53518a43bb1f0af7fdff5b44ba859e797090dd08680b239571d2e8a478d47 install-kit-awindows-20260427-211240/windows/dlp-policy.native-cross-os.example.json -863727465497b474d13d2270d443ff96ccb6076f90a5ce3eb270bdf8088e02dc install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1 -2c0e7a96f32d546ce3ff8da2059259e6e5141c97cefc76bccae4c87c502f00cf install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 -ea68b869182587b7be41bf1891401bdf93dda3b9af6028afa0e30d245c77b0f4 install-kit-awindows-20260427-211240/windows/install-collector-guard-service.ps1 -5ef21a25d5e2da4eeaef17126e60f96f195f90f9dc17a776f8629334b904d096 install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 -d50228354f4c1f15b8ea9ccfef184a3a2e5cc832259ddbdc7afea4c083d03075 install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 -731098681d89b9af6f3872abd586ac3b1faba2d7f9340211e503f52ad0243b3f install-kit-awindows-20260427-211240/windows/web-category-rules.example.json -1b7c337967236474484e781dc8ac37543509b051513f01e1a3145369262f5389 install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 diff --git a/install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt b/install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt deleted file mode 100644 index bae7e6f..0000000 --- a/install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt +++ /dev/null @@ -1,14 +0,0 @@ -ActivityWatch DetMir Windows Install Kit - -Includes: -- windows/* (deploy scripts, collectors, common module, configs/examples) -- ansible/* (Windows and AW server playbooks, examples, inventory, tasks) -- aw-server/* (server installer, health orchestrator, RU patch loader, host groups, default settings) -- scripts/* (install-kit rebuild/validation, quality gates, browser/web smoke checks) - -Source: -- Local project snapshot at build time. - -Customer-specific deployment configs, inventories, passwords, tokens, domains, -IP addresses and runtime snapshots are intentionally excluded from this public -install-kit. diff --git a/install-kit-awindows-20260427-211240/ansible/README.md b/install-kit-awindows-20260427-211240/ansible/README.md deleted file mode 100644 index 796c48f..0000000 --- a/install-kit-awindows-20260427-211240/ansible/README.md +++ /dev/null @@ -1,289 +0,0 @@ -# Ansible ensemble for AWatch-rus - -Эта директория содержит Ansible-ensemble для полного развёртывания AWatch-rus: - -- деплой на уже существующий Debian host/CT; -- полный цикл с нуля в Proxmox: создание CT + bootstrap + установка ActivityWatch + RU patch; -- централизованное развёртывание Windows/RDP collector'ов по WinRM; -- развёртывание внешнего pfSense poller'а на Debian/Ubuntu utility VM. - -## Файлы - -- `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/deploy_grafana_dashboards.yml` — импорт version-controlled Grafana dashboard'ов через HTTP API. -- `ansible/deploy_tsj_guardian_bot_proxmox.yml` — развёртывание TSJ Guardian Telegram Bot на Proxmox host. -- `ansible/install_full_stack.yml` — полный установочный playbook (оркестратор всех этапов). -- `ansible/inventory.example.ini` — шаблон inventory. -- `ansible/group_vars/*.example.yml` — шаблоны переменных. - -## Быстрый запуск - -1. Скопируйте шаблоны: - - `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 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 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]`); -- `deploy_grafana_dashboards.yml` (группа `[grafana]`). - -Пустые группы в `inventory.ini` безопасны: соответствующий play будет пропущен. - -## Полный запуск с нуля в Proxmox - -1. Подготовьте inventory и vars: - - `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 ansible -ansible-playbook -i inventory.ini provision_proxmox_ct_and_deploy_aw.yml -``` - -## Массовый запуск (матрица CT) - -1. Подготовьте матрицу: - - `cp ansible/group_vars/proxmox-matrix.example.yml ansible/group_vars/proxmox-matrix.yml` -2. Заполните `proxmox-matrix.yml`. -3. Запустите: - -```bash -cd ansible -ansible-playbook -i inventory.ini provision_proxmox_ct_matrix_and_deploy_aw.yml -``` - -## Windows/RDP rollout (WinRM) - -Важно: - -- `WinRM` здесь остаётся транспортом для `Ansible deploy` и `validation`; -- для интерактивной PowerShell-работы из Linux/Codex по DetMir используйте project MCP-over-SSH путь, а не `WSMan`; -- каноника лежит в `docs/DETMIR_POWERSHELL_MCP_REMOTE_RU.md` и `scripts/install_detmir_powershell_mcp.sh`. - -1. Подготовьте inventory и vars: - - `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 ansible -AW_WINRM_PASSWORD='...' bash ./run_deploy_aw_windows.sh -``` - -`run_deploy_aw_windows.sh` автоматически: -- очищает proxy env (`http_proxy/https_proxy/...`), чтобы WinRM не уходил в локальный прокси; -- включает OpenSSL legacy provider, если на хосте отключён `MD4` (нужно для NTLM в pywinrm). -- перезапускает `ansible-playbook` при временных WinRM/NTLM сбоях (по умолчанию 5 попыток, пауза 30 сек). - -Параметры retry: -- `AW_DEPLOY_RETRIES` (по умолчанию `5`); -- `AW_DEPLOY_RETRY_DELAY_SEC` (по умолчанию `30`). - -Playbook: - -- выгружает полный `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` и managed `ActivityWatch Launch *` задачи; -- включает (`Enable-ScheduledTask`) `ActivityWatch Recovery` и managed `ActivityWatch Launch *` задачи перед запуском (иначе WebUI может показывать `Active time: 0s`); -- оставляет `ActivityWatch Recovery` включённым даже при активном `AWatchRusCollectorGuard`: guard является основным контроллером, recovery остаётся fallback/bootstrap path; -- выполняет API smoke-check bucket `aw-watcher-afk_` и ожидает свежие события; -- выполняет API smoke-check bucket `aw-watcher-window_` и ожидает свежие события (по умолчанию включено); -- запускает `validate-deployment.ps1`; -- забирает JSON-отчёт в локальную директорию (`/tmp/aw-rus-validation-` по умолчанию). -- настраивает scheduled task `ActivityWatch Hayabusa Upload` с периодом и lookback по vars. - -Дополнительные флаги: - -- `aw_windows_afk_enabled: false` — не запускать `aw-watcher-afk`; -- `aw_windows_window_enabled: false` — не запускать `aw-watcher-window`; -- `aw_windows_incident_capture_enabled: false` — отключить блок incidentCapture; -- `aw_windows_incident_screenshot_enabled: false` — не делать скриншот при DLP-инциденте; -- `aw_windows_incident_artifacts_root: 'C:\...\incident-artifacts'` — переопределить путь артефактов; -- `aw_windows_deploy_root: 'C:\Program Files\AWatch-rus'` — каталог toolkit, совпадает с InnoSetup `{app}`; -- `aw_windows_install_root: 'C:\Program Files\AWatch-rus\bin'` — каталог бинарников, совпадает с InnoSetup `AwDefaultInstallRoot`; -- `aw_windows_state_root: 'C:\ProgramData\AWatch-rus'` — каталог состояния/отчётов, совпадает с InnoSetup `AwDefaultStateRoot`; -- `aw_windows_validation_remote_path: '{{ aw_windows_state_root }}\aw_validate_ansible.json'` — отчёт Ansible-валидации хранится рядом с `ensemble-report-*.json`; -- `aw_windows_migration_enabled: true` — включить guard миграции текущего production из `ActivityWatch-Phase2` в единый `AWatch-rus`; -- `aw_windows_legacy_install_root` / `aw_windows_legacy_state_root` — старые production paths, откуда выполняется перенос; -- `aw_windows_migration_report_remote_path` — JSON-отчёт о миграции на Windows-хосте; -- `aw_windows_package_version`, `aw_windows_package_url`, `aw_windows_package_zip_path` — версия и источник Windows-пакета ActivityWatch; -- `aw_windows_api_smoke_check_bucket: ""` — автоматически использовать `aw-watcher-afk_`; -- `aw_windows_api_smoke_check_window_enabled: true` — включить дополнительный smoke-check `aw-watcher-window_`; -- `aw_windows_api_smoke_check_window_bucket: ""` — переопределить bucket для window smoke-check; -- `aw_windows_api_smoke_check_min_events: 1` — минимум событий, ожидаемых в smoke-check; -- `aw_windows_fail_on_validation_error: true` — завершать playbook ошибкой, если `validate-deployment.ps1` возвращает `overallOk=false`; -- `aw_windows_skip_hardening: true` — пропустить `hardening-recovery.ps1` внутри ensemble-скрипта. -- `aw_windows_hayabusa_auto_upload_enabled: true` — включить авто-upload EVTX на AW-server; -- `aw_windows_hayabusa_auto_upload_interval_hours: 6` — период scheduled task; -- `aw_windows_hayabusa_auto_upload_hours_back: 6` — lookback для каждого запуска; -- `aw_windows_hayabusa_auto_upload_mode: "incident"` — mode для server-side processing; -- `aw_windows_hayabusa_auto_upload_task_name: "ActivityWatch Hayabusa Upload"` — имя scheduled task. - -## Server-side Hayabusa auto-case и Telegram alerting - -На стороне `deploy_aw_server.yml` теперь есть server-side контур: - -- `aw-hayabusa-drop.path` -- `aw-hayabusa-drop.service` -- `aw-hayabusa-autoprocess` -- `aw-hayabusa-case-alert` - -Что делает контур: - -- автоматически подхватывает `zip` из `/opt/activitywatch/aw-rus-ops/drop`; -- запускает `aw-hayabusa`; -- считает severity/score по `timeline.jsonl`; -- создаёт или обновляет case; -- пишет bounded metadata в `forensics.hayabusa`; -- отправляет Telegram alert. - -Основные vars: - -- `aw_hayabusa_auto_case_enabled: true` -- `aw_hayabusa_auto_case_min_severity: "medium"` -- `aw_hayabusa_telegram_enabled: true` -- `aw_hayabusa_telegram_min_severity: "high"` -- `aw_hayabusa_telegram_bot_token` -- `aw_hayabusa_telegram_chat_ids` - -## Развёртывание pfSense poller - -1. Подготовьте vars: - - `cp ansible/group_vars/pfsense-poller.example.yml ansible/group_vars/pfsense-poller.yml` -2. Добавьте inventory group `[aw_pfsense_pollers]`. -3. Запустите: - -```bash -cd ansible -ansible-playbook -i inventory.ini deploy_aw_pfsense_poller.yml -``` - -Playbook: - -- ставит `python3`; -- копирует `pfsense-aw-poller.py`; -- пишет `/etc/aw-pfsense/poller.json`; -- поднимает `aw-pfsense-poller.service`. - -## Импорт Grafana dashboard'ов - -1. Подготовьте inventory и vars: - - `cp ansible/inventory.example.ini ansible/inventory.ini` - - `cp ansible/group_vars/grafana.example.yml ansible/group_vars/grafana.yml` -2. Укажите в inventory группу `[grafana]` и переменную `grafana_url`. -3. Экспортируйте пароль Grafana API: - -```bash -export GRAFANA_ADMIN_PASSWORD='...' -``` - -4. Запустите: - -```bash -cd ansible -ansible-playbook -i inventory.ini deploy_grafana_dashboards.yml -``` - -Playbook: - -- проверяет `GET /api/health`; -- создает или актуализирует folder `AWatch-rus` в Grafana; -- импортирует dashboard JSON из каталога `grafana/`; -- верифицирует доступность dashboard'ов по `uid` через Grafana API. - -По умолчанию импортируются: - -- `DetMir: Работа пользователей в RDP` -- `DetMir: DLP и ИБ обзор` -- `DetMir: ИБ сводка для руководства` -- `AW-rus: DLP обзор` - -Подробная документация: `docs/GRAFANA_DASHBOARDS_RU.md` - -## Развёртывание TSJ Guardian Bot на Proxmox - -1. Подготовьте vars: - - `cp ansible/group_vars/proxmox-bot.example.yml ansible/group_vars/proxmox-bot.yml` -2. Заполните минимум: - - `telegram_bot_token` - - `telegram_allowed_chat_ids` - - `tsj_bot_source_local_path` -3. Убедитесь, что в inventory есть группа `[proxmox]`. - Для текущего контура AW-Rus bot ожидает Proxmox host ``. - Рабочая модель для этого контура: `igor` + `sudo`, а не обязательный `root` login. -4. При необходимости задайте recovery-команды для AW-Rus: - - `tsj_bot_aw_rus_worktime_heal_cmd` - - `tsj_bot_aw_rus_dlp_heal_cmd` -5. Запустите: - -```bash -cd ansible -ansible-playbook -i inventory.ini deploy_tsj_guardian_bot_proxmox.yml -``` - -После актуального production hardening: - -- bot различает `worktime idle` и реальную деградацию; -- bot поддерживает отдельный `AW_RUS_DLP_HEAL_CMD`; -- redeploy не должен терять runtime env-ключи, связанные с proxy, FS checks и AI escalation. - -## Результат - -- Установлен ActivityWatch Server. -- Создан systemd-unit `activitywatch-server.service`. -- Установлен RU Web UI patch. -- Для 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`. -- Для полного сценария CT создаётся автоматически через `pct create`. -- На Windows/RDP host развёрнуты AFK/window watchers, browser domain collector, DLP endpoint collector и worktime session collector. -- Проверочный JSON-отчёт Windows playbook должен иметь `overallOk=true`. - -## Prod rollout одной командой - -Для ручного запуска с dry-run и логированием используйте: - -```bash -bash scripts/prod_rollout.sh -``` - -Скрипт попросит `AW_SSH_PASSWORD` и `AW_WINRM_PASSWORD` интерактивно (ввод скрыт) и сложит логи в `.rollout-logs/`. diff --git a/install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml b/install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml deleted file mode 100644 index 34aa29d..0000000 --- a/install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml +++ /dev/null @@ -1,66 +0,0 @@ ---- -- name: Развернуть pfSense ActivityWatch poller - hosts: aw_pfsense_pollers - become: true - gather_facts: true - - vars: - aw_pfsense_install_root: "/opt/aw-pfsense" - aw_pfsense_config_dir: "/etc/aw-pfsense" - aw_pfsense_service_name: "aw-pfsense-poller.service" - - tasks: - - name: Установить обязательные пакеты - ansible.builtin.apt: - name: - - python3 - state: present - update_cache: true - - - name: Создать каталоги - ansible.builtin.file: - path: "{{ item }}" - state: directory - mode: "0755" - loop: - - "{{ aw_pfsense_install_root }}" - - "{{ aw_pfsense_config_dir }}" - - - 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: Установить 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: - - Перезагрузить systemd - - - name: Записать конфигурацию pfSense poller - ansible.builtin.copy: - dest: "{{ aw_pfsense_config_dir }}/poller.json" - mode: "0600" - content: "{{ aw_pfsense_poller_config | to_nice_json }}" - notify: - - Перезапустить pfSense poller - - - name: Включить и запустить pfSense poller - ansible.builtin.systemd: - name: "{{ aw_pfsense_service_name }}" - enabled: true - state: restarted - daemon_reload: true - - handlers: - - name: Перезагрузить systemd - ansible.builtin.systemd: - daemon_reload: true - - - name: Перезапустить pfSense poller - ansible.builtin.systemd: - name: "{{ aw_pfsense_service_name }}" - state: restarted diff --git a/install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml b/install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml deleted file mode 100644 index 6a81578..0000000 --- a/install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml +++ /dev/null @@ -1,2569 +0,0 @@ ---- -- name: Развернуть сервер AWatch-rus - hosts: aw_server - become: true - gather_facts: true - - vars: - aw_release_root: "/opt/activitywatch/releases" - aw_release_dir: "{{ aw_release_root }}/{{ aw_server_version }}" - aw_archive_path: "/tmp/activitywatch-{{ aw_server_version }}.zip" - aw_bootstrap_dir: "/tmp/aw-rus-bootstrap" - aw_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_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 }}" - aw_rust_release_dir: "{{ (lookup('env', 'CARGO_TARGET_DIR') | default(aw_repo_root + '/adk-rust/target', true)) + '/release' }}" - - tasks: - - name: Установить базовые пакеты - ansible.builtin.apt: - name: - - curl - - python3-venv - - python3-pip - - rsync - - tesseract-ocr - - tesseract-ocr-rus - - unzip - - jq - state: present - update_cache: true - - - name: Установить пакеты для browser smoke проверки - ansible.builtin.apt: - name: - - chromium - - nodejs - - node-playwright - state: present - when: aw_browser_smoke_enabled | default(true) | bool - - - name: Создать системную группу сервиса - ansible.builtin.group: - name: "{{ aw_server_group }}" - system: true - state: present - - - name: Создать системную учётную запись сервиса - ansible.builtin.user: - name: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - home: "{{ aw_server_data_dir }}" - shell: /usr/sbin/nologin - system: true - create_home: false - - - name: Создать обязательные каталоги - 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_data_dir }}/slo" - - "{{ aw_server_data_dir }}/browser-smoke" - - "{{ aw_rus_health_state_dir }}" - - "{{ aw_rus_health_validation_dir }}" - - "{{ aw_server_log_dir }}" - - /etc/activitywatch - - /usr/local/lib/aw-rus-browser-smoke - - "{{ aw_bootstrap_dir }}" - - - name: Настроить каталоги ActivityWatch с владельцем сервиса - ansible.builtin.file: - path: "{{ item }}" - state: directory - owner: "{{ aw_server_user }}" - 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_data_dir }}/slo" - - "{{ aw_server_data_dir }}/browser-smoke" - - "{{ aw_rus_health_state_dir }}" - - "{{ aw_rus_health_validation_dir }}" - - "{{ aw_server_log_dir }}" - - - name: Проверить существующие health state files - ansible.builtin.stat: - path: "{{ item }}" - loop: - - "{{ aw_rus_health_state_dir }}/aw-rus-health.json" - - "{{ aw_rus_health_state_dir }}/aw-rus-health.txt" - - "{{ aw_rus_health_state_dir }}/dlp-health-check-counters.json" - register: aw_rus_health_state_files - - - name: Нормализовать владельца существующих health state files - ansible.builtin.file: - path: "{{ item.stat.path }}" - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0644" - loop: "{{ aw_rus_health_state_files.results }}" - when: item.stat.exists | default(false) - - - name: Удалить устаревшие Python entrypoints после Rust migration - ansible.builtin.file: - path: "{{ item }}" - state: absent - loop: - - /opt/activitywatch/aw-rus-ops/health-check.sh - - /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa-autoprocess.py - - /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa-case-alert.py - - /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa-from-windows.py - - /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa-link-case.py - - /opt/activitywatch/dlp-case-management/.venv - - /opt/activitywatch/dlp-case-management/case_rules.py - - /opt/activitywatch/dlp-case-management/case_schema.py - - /opt/activitywatch/dlp-case-management/case_service.py - - /opt/activitywatch/dlp-case-management/case_storage.py - - /opt/activitywatch/dlp-case-management/evidence_chain.py - - /opt/activitywatch/dlp-compliance/compliance_scheduler.py - - /opt/activitywatch/dlp-compliance/report_generator.py - - /opt/activitywatch/dlp-compliance/.venv - - /opt/activitywatch/dlp-integrations/cef_exporter.py - - /opt/activitywatch/dlp-integrations/syslog_forwarder.py - - /opt/activitywatch/dlp-integrations/webhook_sender.py - - /opt/activitywatch/dlp-integrations/.venv - - /opt/activitywatch/dlp-policy-engine/.venv - - /opt/activitywatch/dlp-policy-engine/policy_distributor.py - - /opt/activitywatch/dlp-policy-engine/policy_schema.py - - /opt/activitywatch/dlp-policy-engine/policy_service.py - - /opt/activitywatch/dlp-policy-engine/policy_storage.py - - /opt/activitywatch/scripts/aggregate_dlp_events.py - - /usr/local/bin/aw-dlp-influx-exporter.py - - /usr/local/bin/aw-extract-ioc-from-sigma.py - - /usr/local/bin/aw-rus-healthd.py - - /usr/local/bin/aw-slo-monitor.py - - /usr/local/bin/aw-worktime-afk-bridge.py - - /usr/local/bin/aw-worktime-api.py - - /usr/local/bin/aw-worktime-influx-exporter.py - - /usr/local/bin/aw-worktime-ui-bridge.py - - /usr/local/bin/dlp-admin-cli.py - - /usr/local/bin/merge_aw_server_dbs.py - - - name: Установить prune script для локального state - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-prune-local-state.sh" - dest: /usr/local/bin/aw-prune-local-state.sh - owner: root - group: root - mode: "0755" - - - name: Положить legacy prune script в server-side ops bundle - ansible.builtin.copy: - src: "{{ aw_repo_root }}/scripts/legacy/aw-prune-local-state.sh" - dest: /opt/activitywatch/aw-rus-ops/aw-prune-local-state.sh - owner: root - group: root - mode: "0755" - - - name: Проверить локальный Rust prune local state - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/aw-prune-local-state" - delegate_to: localhost - register: aw_prune_local_state_rust_binary - become: false - - - name: Установить Rust prune local state - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/aw-prune-local-state" - dest: /usr/local/bin/aw-prune-local-state-rust - owner: root - group: root - mode: "0755" - when: aw_prune_local_state_rust_binary.stat.exists | default(false) - - - name: Ограничить рост journald на aw-server - ansible.builtin.copy: - dest: /etc/systemd/journald.conf.d/aw-rus-retention.conf - owner: root - group: root - mode: "0644" - content: | - [Journal] - SystemMaxUse={{ aw_server_journal_system_max_use }} - RuntimeMaxUse={{ aw_server_journal_runtime_max_use }} - SystemKeepFree={{ aw_server_journal_system_keep_free }} - register: aw_journald_dropin - - - name: Установить systemd service prune локального state - ansible.builtin.copy: - dest: /etc/systemd/system/aw-prune-local-state.service - owner: root - group: root - mode: "0644" - content: | - [Unit] - Description=Prune ActivityWatch local backups and temp state - - [Service] - Type=oneshot - Environment=AW_DATA_DIR={{ aw_server_data_dir }} - Environment=AW_BACKUP_RETENTION_DAYS={{ aw_server_backup_retention_days }} - Environment=AW_BACKUP_KEEP_LAST_DB={{ aw_server_backup_keep_last_db }} - Environment=AW_BACKUP_KEEP_LAST_JSON={{ aw_server_backup_keep_last_json }} - ExecStart=/usr/local/bin/aw-prune-local-state.sh - - - name: Установить systemd timer prune локального state - ansible.builtin.copy: - dest: /etc/systemd/system/aw-prune-local-state.timer - owner: root - group: root - mode: "0644" - content: | - [Unit] - Description=Daily prune of ActivityWatch local backups and temp state - - [Timer] - OnCalendar=*-*-* 04:40:00 - Persistent=true - - [Install] - WantedBy=timers.target - - - name: Перечитать systemd после retention unit/drop-in - ansible.builtin.systemd: - daemon_reload: true - - - name: Включить и запустить timer prune локального state - ansible.builtin.systemd: - name: aw-prune-local-state.timer - enabled: true - state: started - - - name: Применить journald retention без простоя - ansible.builtin.command: - argv: - - systemctl - - restart - - systemd-journald - when: aw_journald_dropin.changed - failed_when: false - - - name: Сжать существующий journald до нового лимита - ansible.builtin.command: - argv: - - journalctl - - --vacuum-size={{ aw_server_journal_system_max_use }} - changed_when: true - failed_when: false - - - 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: Установить релиз 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: Распаковать релиз ActivityWatch - ansible.builtin.unarchive: - src: "{{ aw_archive_path }}" - dest: "{{ aw_release_dir }}" - remote_src: true - extra_opts: ["-o"] - - - name: Удалить временный архив ActivityWatch после распаковки - ansible.builtin.file: - path: "{{ aw_archive_path }}" - state: absent - - - name: Найти распакованный каталог ActivityWatch - ansible.builtin.find: - paths: "{{ aw_release_dir }}" - recurse: true - file_type: directory - patterns: "activitywatch*" - register: aw_release_find - - - 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: Найти index.html WebUI - ansible.builtin.find: - paths: "{{ aw_release_dir }}" - recurse: true - file_type: file - patterns: - - index.html - register: aw_webui_index_find - - - 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: - 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: (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: Применить 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-worktime-panel.js", dest: "{{ aw_server_webui_dir }}/js/aw-worktime-panel.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/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-worktime-panel.js", dest: "/root/bootstrap/aw-worktime-panel.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" } - - - 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: Создать server-side ops bundle для ранних health helpers - ansible.builtin.file: - path: /opt/activitywatch/aw-rus-ops - state: directory - owner: root - group: root - mode: "0755" - - - name: Установить wrapper aw-health-check до Influx проверок - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-health-check-wrapper.sh" - dest: /usr/local/bin/aw-health-check - owner: root - group: root - mode: "0755" - - - name: Проверить локальный Rust aw-health-check до Influx проверок - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/aw-health-check" - delegate_to: localhost - register: aw_health_check_rust_binary_early - become: false - - - name: Установить Rust aw-health-check до Influx проверок - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/aw-health-check" - dest: /usr/local/bin/aw-health-check-rust - owner: root - group: root - mode: "0755" - when: aw_health_check_rust_binary_early.stat.exists | default(false) - - - name: Требовать Rust aw-health-check до Influx проверок - ansible.builtin.assert: - that: - - aw_health_check_rust_binary_early.stat.exists | default(false) - fail_msg: "Rust aw-health-check artifact is required at {{ aw_rust_release_dir }}/aw-health-check" - - - name: Проверить локальный Rust aw-db-health до Influx проверок - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/aw-db-health" - delegate_to: localhost - register: aw_db_health_rust_binary_early - become: false - - - name: Установить Rust aw-db-health до Influx проверок - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/aw-db-health" - dest: /usr/local/bin/aw-db-health - owner: root - group: root - mode: "0755" - when: aw_db_health_rust_binary_early.stat.exists | default(false) - - - name: Проверить локальный Rust aw-ensure-reliability до Influx проверок - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/aw-ensure-reliability" - delegate_to: localhost - register: aw_ensure_reliability_rust_binary_early - become: false - - - name: Установить Rust aw-ensure-reliability до Influx проверок - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/aw-ensure-reliability" - dest: /usr/local/bin/aw-ensure-reliability - owner: root - group: root - mode: "0755" - when: aw_ensure_reliability_rust_binary_early.stat.exists | default(false) - - - name: Проверить локальный Rust aw-db-maintenance до Influx проверок - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/aw-db-maintenance" - delegate_to: localhost - register: aw_db_maintenance_rust_binary_early - become: false - - - name: Установить Rust aw-db-maintenance до Influx проверок - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/aw-db-maintenance" - dest: /usr/local/bin/aw-db-maintenance - owner: root - group: root - mode: "0755" - when: aw_db_maintenance_rust_binary_early.stat.exists | default(false) - - - name: Установить aw-db-maintenance service до Influx проверок - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-db-maintenance.service" - dest: /etc/systemd/system/aw-db-maintenance.service - owner: root - group: root - mode: "0644" - when: aw_db_maintenance_rust_binary_early.stat.exists | default(false) - - - name: Установить aw-db-maintenance timer до Influx проверок - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-db-maintenance.timer" - dest: /etc/systemd/system/aw-db-maintenance.timer - owner: root - group: root - mode: "0644" - when: aw_db_maintenance_rust_binary_early.stat.exists | default(false) - - - name: Включить weekly aw-db-maintenance timer до Influx проверок - ansible.builtin.systemd: - name: aw-db-maintenance.timer - enabled: true - state: started - daemon_reload: true - when: aw_db_maintenance_rust_binary_early.stat.exists | default(false) - - - name: Прочитать текущий aw-server.env для сохранения Influx token - ansible.builtin.slurp: - path: /etc/activitywatch/aw-server.env - register: aw_existing_server_env_raw - failed_when: false - no_log: true - - - name: Подготовить effective Influx tokens без вывода секретов - ansible.builtin.set_fact: - aw_existing_server_env_text: "{{ aw_existing_server_env_raw.content | default('') | b64decode }}" - aw_existing_worktime_influx_token: "{{ (aw_existing_server_env_raw.content | default('') | b64decode | regex_search('(?m)^AW_WORKTIME_INFLUX_TOKEN=.*$') | default('', true) | regex_replace('^AW_WORKTIME_INFLUX_TOKEN=', '')) }}" - aw_existing_dlp_influx_token: "{{ (aw_existing_server_env_raw.content | default('') | b64decode | regex_search('(?m)^AW_DLP_INFLUX_TOKEN=.*$') | default('', true) | regex_replace('^AW_DLP_INFLUX_TOKEN=', '')) }}" - no_log: true - - - name: Выбрать effective Influx tokens - ansible.builtin.set_fact: - aw_effective_worktime_influx_token: "{{ (aw_worktime_influx_token | default('') | string) if ((aw_worktime_influx_token | default('') | string | length) > 0) else aw_existing_worktime_influx_token }}" - aw_effective_dlp_influx_token: "{{ (aw_dlp_influx_token | default('') | string) if ((aw_dlp_influx_token | default('') | string | length) > 0) else aw_existing_dlp_influx_token }}" - no_log: true - - - name: Проверить Influx token для AW worktime exporter - ansible.builtin.assert: - that: - - aw_effective_worktime_influx_token is defined - - aw_effective_worktime_influx_token | length > 0 - fail_msg: "aw_worktime_influx_enabled=true, но token пуст и в локальном env, и в текущем /etc/activitywatch/aw-server.env. Exporter будет падать и Grafana не получит worktime-ряды." - when: aw_worktime_influx_enabled | default(false) | bool - - - name: Проверить Influx token для AW DLP exporter - ansible.builtin.assert: - that: - - aw_effective_dlp_influx_token is defined - - aw_effective_dlp_influx_token | length > 0 - fail_msg: "aw_dlp_influx_enabled=true, но token пуст и в локальном env, и в текущем /etc/activitywatch/aw-server.env. Exporter будет падать и Grafana не получит DLP-ряды." - when: aw_dlp_influx_enabled | default(false) | bool - - - 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 }} - AW_WORKTIME_REPORT_BASE={{ aw_worktime_report_base }} - AW_WORKTIME_TZ={{ aw_worktime_timezone }} - AW_DLP_IOC_DIR={{ aw_dlp_ioc_workdir }}/output - AW_DLP_POLICY_ENGINE_BIND_HOST={{ aw_dlp_policy_engine_bind_host }} - AW_DLP_POLICY_ENGINE_PORT={{ aw_dlp_policy_engine_port }} - AW_DLP_POLICY_ENGINE_DB_PATH={{ aw_dlp_policy_engine_db_path }} - AW_DLP_CASE_BIND_HOST={{ aw_dlp_case_bind_host }} - AW_DLP_CASE_PORT={{ aw_dlp_case_port }} - AW_DLP_CASE_DB_PATH={{ aw_dlp_case_db_path }} - AW_DLP_COMPLIANCE_REPORT_DIR={{ aw_dlp_compliance_report_dir }} - AW_DLP_COMPLIANCE_TEMPLATE={{ aw_dlp_compliance_template_path }} - AW_SERVER_URL=http://127.0.0.1:5600 - XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share - XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config - AW_WORKTIME_INFLUX_ENABLED={{ 'true' if (aw_worktime_influx_enabled | default(false) | bool) else 'false' }} - AW_WORKTIME_INFLUX_URL={{ aw_worktime_influx_url | default('') }} - AW_WORKTIME_INFLUX_ORG={{ aw_worktime_influx_org | default('proxmox') }} - AW_WORKTIME_INFLUX_BUCKET={{ aw_worktime_influx_bucket | default('aw_metrics') }} - AW_WORKTIME_INFLUX_HOSTS={{ aw_worktime_influx_hosts | default('SHARKON2025') }} - AW_WORKTIME_INFLUX_DAYS={{ aw_worktime_influx_days | default('today,yesterday') }} - AW_WORKTIME_INFLUX_TOKEN={{ aw_effective_worktime_influx_token | default('') }} - AW_DLP_INFLUX_ENABLED={{ 'true' if (aw_dlp_influx_enabled | default(false) | bool) else 'false' }} - AW_DLP_INFLUX_URL={{ aw_dlp_influx_url | default('') }} - AW_DLP_INFLUX_ORG={{ aw_dlp_influx_org | default('proxmox') }} - AW_DLP_INFLUX_BUCKET={{ aw_dlp_influx_bucket | default('aw_metrics') }} - AW_DLP_INFLUX_HOSTS={{ aw_dlp_influx_hosts | default('SHARKON2025') }} - AW_DLP_INFLUX_LOOKBACK_DAYS={{ aw_dlp_influx_lookback_days | default(30) }} - AW_DLP_INFLUX_EVENT_LIMIT={{ aw_dlp_influx_event_limit | default(2000) }} - AW_DLP_INFLUX_TOKEN={{ aw_effective_dlp_influx_token | default('') }} - AW_DLP_AW_API_BASE=http://127.0.0.1:5600/api/0 - AW_DLP_CASE_API_BASE=http://127.0.0.1:5602/api/0/dlp/cases - AW_EXPECT_START_OF_DAY={{ aw_worktime_start_of_day | default('') }} - AW_EXPECT_ALWAYS_ACTIVE_PATTERN={{ aw_server_always_active_pattern | default('') }} - AW_EXPECT_LANDINGPAGE={{ aw_server_landingpage | default('') }} - AW_HEALTH_STRICT_FILEOPS={{ aw_health_strict_fileops | default(0) }} - AW_MONITORED_WINDOWS_HOST={{ aw_monitored_windows_host }} - AW_MONITORED_WINDOWS_HOSTNAME={{ aw_monitored_windows_hostname }} - AW_RUS_HEALTH_WORKTIME_API={{ aw_rus_health_worktime_api_base | default('http://127.0.0.1:5610') }} - AW_RUS_HEALTH_STATE_DIR={{ aw_rus_health_state_dir }} - AW_RUS_HEALTH_VALIDATION_DIR={{ aw_rus_health_validation_dir }} - AW_RUS_HEALTH_SESSION_EVENTS_MAX_AGE_SECONDS={{ aw_rus_health_session_events_max_age_seconds | default(86400) }} - AW_RUS_HEALTH_GUARD_MAX_AGE_SECONDS={{ aw_rus_health_guard_max_age_seconds | default(300) }} - AW_RUS_HEALTH_GUARD_REQUIRED={{ 1 if (aw_rus_health_guard_required | default(true) | bool) else 0 }} - AW_RUS_SLO_STATE_DIR={{ aw_server_data_dir }}/slo - AW_RUS_SLO_AW_BASE=http://127.0.0.1:5600 - AW_RUS_SLO_WORKTIME_BASE={{ aw_rus_health_worktime_api_base | default('http://127.0.0.1:5610') }} - AW_RUS_SLO_TARGET_PERCENT={{ aw_rus_slo_target_percent | default('99.97') }} - AW_BROWSER_SMOKE_AW_BASE=http://127.0.0.1:5600 - AW_BROWSER_SMOKE_WORKTIME_BASE={{ aw_rus_health_worktime_api_base | default('http://127.0.0.1:5610') }} - AW_BROWSER_SMOKE_HOST={{ aw_monitored_windows_hostname }} - AW_BROWSER_SMOKE_OUTPUT_DIR={{ aw_server_data_dir }}/browser-smoke - AW_BROWSER_SMOKE_ENGINE={{ aw_browser_smoke_engine | default('chromium-cli') }} - AW_BROWSER_SMOKE_TIMEOUT_MS={{ aw_browser_smoke_timeout_ms | default(20000) }} - AW_BROWSER_SMOKE_RENDER_TIMEOUT_MS={{ aw_browser_smoke_render_timeout_ms | default(15000) }} - AW_HAYABUSA_AUTO_CASE_ENABLED={{ 'true' if (aw_hayabusa_auto_case_enabled | default(true) | bool) else 'false' }} - AW_HAYABUSA_AUTO_CASE_MIN_SEVERITY={{ aw_hayabusa_auto_case_min_severity | default('medium') }} - AW_HAYABUSA_TELEGRAM_ENABLED={{ 'true' if (aw_hayabusa_telegram_enabled | default(false) | bool) else 'false' }} - AW_HAYABUSA_TELEGRAM_MIN_SEVERITY={{ aw_hayabusa_telegram_min_severity | default('high') }} - AW_HAYABUSA_TELEGRAM_BOT_TOKEN={{ aw_hayabusa_telegram_bot_token | default('') }} - AW_HAYABUSA_TELEGRAM_CHAT_IDS={{ aw_hayabusa_telegram_chat_ids | default('') }} - - - name: Создать каталог DLP policy engine - ansible.builtin.file: - path: /opt/activitywatch/dlp-policy-engine - state: directory - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0755" - when: aw_dlp_policy_engine_enabled | default(false) | bool - - - name: Установить systemd unit DLP policy engine - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/dlp-policy-engine/dlp-policy-engine.service" - dest: /etc/systemd/system/aw-dlp-policy-engine.service - owner: root - group: root - mode: "0644" - when: aw_dlp_policy_engine_enabled | default(false) | bool - - - name: Проверить локальный Rust DLP policy engine - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/dlp-policy-engine" - delegate_to: localhost - register: aw_dlp_policy_engine_rust_binary - become: false - when: aw_dlp_policy_engine_enabled | default(false) | bool - - - name: Требовать Rust DLP policy engine artifact - ansible.builtin.assert: - that: - - aw_dlp_policy_engine_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/dlp-policy-engine" - when: aw_dlp_policy_engine_enabled | default(false) | bool - - - name: Установить Rust DLP policy engine - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/dlp-policy-engine" - dest: /usr/local/bin/aw-dlp-policy-engine-rust - owner: root - group: root - mode: "0755" - when: - - aw_dlp_policy_engine_enabled | default(false) | bool - - aw_dlp_policy_engine_rust_binary.stat.exists | default(false) - - - name: Создать каталог DLP content analysis - ansible.builtin.file: - path: /opt/activitywatch/dlp-content-analysis - state: directory - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0755" - when: aw_dlp_content_analysis_enabled | default(true) | bool - - - name: Скопировать файлы DLP content analysis - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/dlp-content-analysis/" - dest: /opt/activitywatch/dlp-content-analysis/ - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0644" - when: aw_dlp_content_analysis_enabled | default(true) | bool - - - name: Установить wrapper запуска DLP content analysis через virtualenv - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/dlp-content-analysis/aw-dlp-content-analyzer.sh" - dest: /usr/local/bin/aw-dlp-content-analyzer - owner: root - group: root - mode: "0755" - when: aw_dlp_content_analysis_enabled | default(true) | bool - - - name: Проверить локальный Rust DLP content analyzer - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/dlp-content-analyzer" - delegate_to: localhost - register: dlp_content_analyzer_rust_binary - become: false - when: aw_dlp_content_analysis_enabled | default(true) | bool - - - name: Установить Rust DLP content analyzer - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/dlp-content-analyzer" - dest: /usr/local/bin/aw-dlp-content-analyzer-rust - owner: root - group: root - mode: "0755" - when: - - aw_dlp_content_analysis_enabled | default(true) | bool - - dlp_content_analyzer_rust_binary.stat.exists | default(false) - - - name: Создать virtualenv DLP content analysis - ansible.builtin.command: - cmd: python3 -m venv /opt/activitywatch/dlp-content-analysis/.venv - args: - creates: /opt/activitywatch/dlp-content-analysis/.venv/bin/python - when: aw_dlp_content_analysis_enabled | default(true) | bool - - - name: Установить зависимости DLP content analysis - ansible.builtin.pip: - requirements: /opt/activitywatch/dlp-content-analysis/requirements.txt - virtualenv: /opt/activitywatch/dlp-content-analysis/.venv - when: aw_dlp_content_analysis_enabled | default(true) | bool - - - name: Создать каталог DLP integrations - ansible.builtin.file: - path: /opt/activitywatch/dlp-integrations - state: directory - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0755" - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Скопировать файлы DLP integrations - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/dlp-integrations/{{ item }}" - dest: "/opt/activitywatch/dlp-integrations/{{ item }}" - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0644" - loop: - - cef-config.yaml - - syslog-forwarder-config.yaml - - webhook-config.yaml - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Создать state каталог DLP integrations - ansible.builtin.file: - path: /var/lib/activitywatch/dlp-integrations - state: directory - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0755" - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Установить systemd unit CEF exporter - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/dlp-integrations/cef-exporter.service" - dest: /etc/systemd/system/aw-dlp-cef-exporter.service - owner: root - group: root - mode: "0644" - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Установить systemd timer CEF exporter - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/dlp-integrations/cef-exporter.timer" - dest: /etc/systemd/system/aw-dlp-cef-exporter.timer - owner: root - group: root - mode: "0644" - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Проверить локальный Rust CEF exporter - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/dlp-cef-exporter" - delegate_to: localhost - register: dlp_cef_exporter_rust_binary - become: false - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Требовать Rust CEF exporter artifact - ansible.builtin.assert: - that: - - dlp_cef_exporter_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/dlp-cef-exporter" - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Установить Rust CEF exporter - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/dlp-cef-exporter" - dest: /usr/local/bin/dlp-cef-exporter-rust - owner: root - group: root - mode: "0755" - when: - - aw_dlp_integrations_enabled | default(true) | bool - - dlp_cef_exporter_rust_binary.stat.exists | default(false) - - - name: Установить systemd unit syslog forwarder - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/dlp-integrations/syslog-forwarder.service" - dest: /etc/systemd/system/aw-dlp-syslog-forwarder.service - owner: root - group: root - mode: "0644" - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Установить systemd timer syslog forwarder - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/dlp-integrations/syslog-forwarder.timer" - dest: /etc/systemd/system/aw-dlp-syslog-forwarder.timer - owner: root - group: root - mode: "0644" - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Проверить локальный Rust syslog forwarder - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/dlp-syslog-forwarder" - delegate_to: localhost - register: dlp_syslog_forwarder_rust_binary - become: false - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Требовать Rust syslog forwarder artifact - ansible.builtin.assert: - that: - - dlp_syslog_forwarder_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/dlp-syslog-forwarder" - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Установить Rust syslog forwarder - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/dlp-syslog-forwarder" - dest: /usr/local/bin/dlp-syslog-forwarder-rust - owner: root - group: root - mode: "0755" - when: - - aw_dlp_integrations_enabled | default(true) | bool - - dlp_syslog_forwarder_rust_binary.stat.exists | default(false) - - - name: Установить systemd unit webhook sender - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/dlp-integrations/webhook-sender.service" - dest: /etc/systemd/system/aw-dlp-webhook-sender.service - owner: root - group: root - mode: "0644" - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Установить systemd timer webhook sender - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/dlp-integrations/webhook-sender.timer" - dest: /etc/systemd/system/aw-dlp-webhook-sender.timer - owner: root - group: root - mode: "0644" - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Проверить локальный Rust webhook sender - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/dlp-webhook-sender" - delegate_to: localhost - register: dlp_webhook_sender_rust_binary - become: false - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Требовать Rust webhook sender artifact - ansible.builtin.assert: - that: - - dlp_webhook_sender_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/dlp-webhook-sender" - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Установить Rust webhook sender - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/dlp-webhook-sender" - dest: /usr/local/bin/dlp-webhook-sender-rust - owner: root - group: root - mode: "0755" - when: - - aw_dlp_integrations_enabled | default(true) | bool - - dlp_webhook_sender_rust_binary.stat.exists | default(false) - - - name: Создать каталог DLP case management - ansible.builtin.file: - path: /opt/activitywatch/dlp-case-management - state: directory - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0755" - when: aw_dlp_case_management_enabled | default(true) | bool - - - name: Установить systemd unit DLP case management - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/dlp-case-management/case-service.service" - dest: /etc/systemd/system/aw-dlp-case-management.service - owner: root - group: root - mode: "0644" - when: aw_dlp_case_management_enabled | default(true) | bool - - - name: Проверить локальный Rust DLP case management - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/dlp-case-management" - delegate_to: localhost - register: aw_dlp_case_management_rust_binary - become: false - when: aw_dlp_case_management_enabled | default(true) | bool - - - name: Требовать Rust DLP case management artifact - ansible.builtin.assert: - that: - - aw_dlp_case_management_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/dlp-case-management" - when: aw_dlp_case_management_enabled | default(true) | bool - - - name: Установить Rust DLP case management - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/dlp-case-management" - dest: /usr/local/bin/aw-dlp-case-management-rust - owner: root - group: root - mode: "0755" - when: - - aw_dlp_case_management_enabled | default(true) | bool - - aw_dlp_case_management_rust_binary.stat.exists | default(false) - - - name: Создать каталоги DLP compliance - ansible.builtin.file: - path: "{{ item }}" - state: directory - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0755" - loop: - - /opt/activitywatch/dlp-compliance - - /opt/activitywatch/dlp-compliance/templates - - "{{ aw_dlp_compliance_report_dir }}" - when: aw_dlp_compliance_enabled | default(true) | bool - - - name: Скопировать файлы DLP compliance - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/dlp-compliance/{{ item.src }}" - dest: "{{ item.dest }}" - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "{{ item.mode }}" - loop: - - { src: "templates/152-fz-report.html", dest: "/opt/activitywatch/dlp-compliance/templates/152-fz-report.html", mode: "0644" } - - { src: "templates/pci-dss-report.html", dest: "/opt/activitywatch/dlp-compliance/templates/pci-dss-report.html", mode: "0644" } - - { src: "report-scheduler.service", dest: "/etc/systemd/system/aw-dlp-report-scheduler.service", mode: "0644" } - - { src: "report-scheduler.timer", dest: "/etc/systemd/system/aw-dlp-report-scheduler.timer", mode: "0644" } - when: aw_dlp_compliance_enabled | default(true) | bool - - - name: Проверить локальный Rust DLP compliance - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/dlp-compliance" - delegate_to: localhost - register: aw_dlp_compliance_rust_binary - become: false - when: aw_dlp_compliance_enabled | default(true) | bool - - - name: Требовать Rust DLP compliance artifact - ansible.builtin.assert: - that: - - aw_dlp_compliance_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/dlp-compliance" - when: aw_dlp_compliance_enabled | default(true) | bool - - - name: Установить Rust DLP compliance - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/dlp-compliance" - dest: /usr/local/bin/aw-dlp-compliance-rust - owner: root - group: root - mode: "0755" - when: - - aw_dlp_compliance_enabled | default(true) | bool - - aw_dlp_compliance_rust_binary.stat.exists | default(false) - - - name: Проверить локальный Rust dlp-admin-cli - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/dlp-admin-cli" - delegate_to: localhost - register: aw_dlp_admin_cli_rust_binary - - - name: Требовать Rust dlp-admin-cli artifact - ansible.builtin.assert: - that: - - aw_dlp_admin_cli_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/dlp-admin-cli" - - - name: Установить Rust dlp-admin-cli - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/dlp-admin-cli" - dest: /usr/local/bin/dlp-admin-cli - owner: root - group: root - mode: "0755" - when: aw_dlp_admin_cli_rust_binary.stat.exists - - - name: Проверить локальный Rust AW worktime API - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/worktime-api" - delegate_to: localhost - register: aw_worktime_api_rust_binary - become: false - - - name: Требовать Rust AW worktime API artifact - ansible.builtin.assert: - that: - - aw_worktime_api_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/worktime-api" - - - name: Установить Rust AW worktime API - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/worktime-api" - dest: /usr/local/bin/aw-worktime-api-rust - owner: root - group: root - mode: "0755" - when: aw_worktime_api_rust_binary.stat.exists | default(false) - - - name: Установить systemd unit AW worktime API - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.service" - dest: /etc/systemd/system/aw-worktime-api.service - owner: root - group: root - mode: "0644" - - - name: Проверить локальный Rust AW worktime UI bridge - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/worktime-ui-bridge" - delegate_to: localhost - register: aw_worktime_ui_bridge_rust_binary - become: false - - - name: Требовать Rust AW worktime UI bridge artifact - ansible.builtin.assert: - that: - - aw_worktime_ui_bridge_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/worktime-ui-bridge" - - - name: Установить Rust AW worktime UI bridge - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/worktime-ui-bridge" - dest: /usr/local/bin/aw-worktime-ui-bridge-rust - owner: root - group: root - mode: "0755" - when: aw_worktime_ui_bridge_rust_binary.stat.exists | default(false) - - - name: Проверить локальный Rust AW worktime autoheal - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/worktime-autoheal" - delegate_to: localhost - register: aw_worktime_autoheal_rust_binary - become: false - - - name: Требовать Rust AW worktime autoheal artifact - ansible.builtin.assert: - that: - - aw_worktime_autoheal_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/worktime-autoheal" - - - name: Установить Rust AW worktime autoheal - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/worktime-autoheal" - dest: /usr/local/bin/aw-worktime-autoheal-rust - owner: root - group: root - mode: "0755" - when: aw_worktime_autoheal_rust_binary.stat.exists | default(false) - - - name: Проверить локальный Rust AW worktime prewarm - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/worktime-prewarm" - delegate_to: localhost - register: aw_worktime_prewarm_rust_binary - become: false - - - name: Требовать Rust AW worktime prewarm artifact - ansible.builtin.assert: - that: - - aw_worktime_prewarm_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/worktime-prewarm" - - - name: Установить Rust AW worktime prewarm - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/worktime-prewarm" - dest: /usr/local/bin/aw-worktime-prewarm-rust - owner: root - group: root - mode: "0755" - when: aw_worktime_prewarm_rust_binary.stat.exists | default(false) - - - name: Создать server-side ops bundle для AW ops helpers - ansible.builtin.file: - path: /opt/activitywatch/aw-rus-ops - state: directory - owner: root - group: root - mode: "0755" - - - name: Установить скрипт aw-health-check - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-health-check-wrapper.sh" - dest: /usr/local/bin/aw-health-check - owner: root - group: root - mode: "0755" - - - name: Проверить локальный Rust aw-health-check - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/aw-health-check" - delegate_to: localhost - register: aw_health_check_rust_binary - become: false - - - name: Установить Rust aw-health-check - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/aw-health-check" - dest: /usr/local/bin/aw-health-check-rust - owner: root - group: root - mode: "0755" - when: aw_health_check_rust_binary.stat.exists | default(false) - - - name: Требовать Rust aw-health-check artifact - ansible.builtin.assert: - that: - - aw_health_check_rust_binary.stat.exists | default(false) - fail_msg: "Rust aw-health-check artifact is required at {{ aw_rust_release_dir }}/aw-health-check" - - - name: Проверить локальный Rust aw-db-health - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/aw-db-health" - delegate_to: localhost - register: aw_db_health_rust_binary - become: false - - - name: Установить Rust aw-db-health - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/aw-db-health" - dest: /usr/local/bin/aw-db-health - owner: root - group: root - mode: "0755" - when: aw_db_health_rust_binary.stat.exists | default(false) - - - name: Установить скрипт check-aw-data - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/check-aw-data-wrapper.sh" - dest: /usr/local/bin/check-aw-data - owner: root - group: root - mode: "0755" - - - name: Положить legacy check-aw-data в server-side ops bundle - ansible.builtin.copy: - src: "{{ aw_repo_root }}/scripts/legacy/check-aw-data.sh" - dest: /opt/activitywatch/aw-rus-ops/check-aw-data.sh - owner: root - group: root - mode: "0755" - - - name: Проверить локальный Rust check-aw-data - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/check-aw-data" - delegate_to: localhost - register: check_aw_data_rust_binary - become: false - - - name: Установить Rust check-aw-data - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/check-aw-data" - dest: /usr/local/bin/check-aw-data-rust - owner: root - group: root - mode: "0755" - when: check_aw_data_rust_binary.stat.exists | default(false) - - - name: Проверить локальный Rust dlp-health-check - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/dlp-health-check" - delegate_to: localhost - register: dlp_health_check_rust_binary - become: false - - - name: Установить Rust dlp-health-check - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/dlp-health-check" - dest: /usr/local/bin/dlp-health-check - owner: root - group: root - mode: "0755" - when: dlp_health_check_rust_binary.stat.exists | default(false) - - - name: Проверить локальный Rust AW-RUS healthd - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/aw-rus-healthd" - delegate_to: localhost - register: aw_rus_healthd_rust_binary - become: false - - - name: Проверить локальный Rust AW-RUS SLO monitor - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/aw-slo-monitor" - delegate_to: localhost - register: aw_slo_monitor_rust_binary - become: false - - - name: Требовать Rust dlp-health-check artifact - ansible.builtin.assert: - that: - - dlp_health_check_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/dlp-health-check" - - - name: Требовать Rust AW-RUS healthd artifact - ansible.builtin.assert: - that: - - aw_rus_healthd_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/aw-rus-healthd" - - - name: Требовать Rust AW-RUS SLO monitor artifact - ansible.builtin.assert: - that: - - aw_slo_monitor_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/aw-slo-monitor" - - - name: Установить Rust AW-RUS healthd - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/aw-rus-healthd" - dest: /usr/local/bin/aw-rus-healthd-rust - owner: root - group: root - mode: "0755" - - - name: Установить Rust AW-RUS SLO monitor - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/aw-slo-monitor" - dest: /usr/local/bin/aw-slo-monitor-rust - owner: root - group: root - mode: "0755" - when: aw_slo_monitor_rust_binary.stat.exists | default(false) - - - name: Установить browser smoke скрипт - ansible.builtin.copy: - src: "{{ aw_repo_root }}/scripts/aw-webui-browser-smoke.mjs" - dest: /usr/local/lib/aw-rus-browser-smoke/aw-webui-browser-smoke.mjs - owner: root - group: root - mode: "0755" - when: aw_browser_smoke_enabled | default(true) | bool - - - name: Установить systemd unit AW worktime UI bridge - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.service" - dest: /etc/systemd/system/aw-worktime-ui-bridge.service - owner: root - group: root - mode: "0644" - - - name: Установить systemd timer AW worktime UI bridge - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.timer" - dest: /etc/systemd/system/aw-worktime-ui-bridge.timer - owner: root - group: root - mode: "0644" - - - name: Установить systemd unit AW worktime autoheal - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-autoheal.service" - dest: /etc/systemd/system/aw-worktime-autoheal.service - owner: root - group: root - mode: "0644" - - - name: Установить systemd timer AW worktime autoheal - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-autoheal.timer" - dest: /etc/systemd/system/aw-worktime-autoheal.timer - owner: root - group: root - mode: "0644" - - - name: Установить systemd unit AW worktime prewarm - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-prewarm.service" - dest: /etc/systemd/system/aw-worktime-prewarm.service - owner: root - group: root - mode: "0644" - - - name: Установить systemd timer AW worktime prewarm - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-prewarm.timer" - dest: /etc/systemd/system/aw-worktime-prewarm.timer - owner: root - group: root - mode: "0644" - - - name: Установить systemd unit AW-RUS healthd - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-rus-healthd.service" - dest: /etc/systemd/system/aw-rus-healthd.service - owner: root - group: root - mode: "0644" - - - name: Установить systemd timer AW-RUS healthd - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-rus-healthd.timer" - dest: /etc/systemd/system/aw-rus-healthd.timer - owner: root - group: root - mode: "0644" - - - name: Установить systemd unit AW-RUS SLO monitor - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-slo-monitor.service" - dest: /etc/systemd/system/aw-slo-monitor.service - owner: root - group: root - mode: "0644" - - - name: Установить systemd timer AW-RUS SLO monitor - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-slo-monitor.timer" - dest: /etc/systemd/system/aw-slo-monitor.timer - owner: root - group: root - mode: "0644" - - - name: Установить systemd unit AW-RUS browser smoke - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-browser-smoke.service" - dest: /etc/systemd/system/aw-browser-smoke.service - owner: root - group: root - mode: "0644" - when: aw_browser_smoke_enabled | default(true) | bool - - - name: Установить systemd timer AW-RUS browser smoke - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-browser-smoke.timer" - dest: /etc/systemd/system/aw-browser-smoke.timer - owner: root - group: root - mode: "0644" - when: aw_browser_smoke_enabled | default(true) | bool - - - name: Установить systemd unit AW worktime Influx exporter - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-influx-exporter.service" - dest: /etc/systemd/system/aw-worktime-influx-exporter.service - owner: root - group: root - mode: "0644" - when: aw_worktime_influx_enabled | default(false) | bool - - - name: Проверить локальный Rust AW worktime Influx exporter - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/worktime-influx-exporter" - delegate_to: localhost - register: aw_worktime_influx_exporter_rust_binary - become: false - when: aw_worktime_influx_enabled | default(false) | bool - - - name: Требовать Rust AW worktime Influx exporter artifact - ansible.builtin.assert: - that: - - aw_worktime_influx_exporter_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/worktime-influx-exporter" - when: aw_worktime_influx_enabled | default(false) | bool - - - name: Установить Rust AW worktime Influx exporter - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/worktime-influx-exporter" - dest: /usr/local/bin/aw-worktime-influx-exporter-rust - owner: root - group: root - mode: "0755" - when: - - aw_worktime_influx_enabled | default(false) | bool - - aw_worktime_influx_exporter_rust_binary.stat.exists | default(false) - - - name: Установить systemd timer AW worktime Influx exporter - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-influx-exporter.timer" - dest: /etc/systemd/system/aw-worktime-influx-exporter.timer - owner: root - group: root - mode: "0644" - when: aw_worktime_influx_enabled | default(false) | bool - - - name: Установить systemd unit AW DLP Influx exporter - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-dlp-influx-exporter.service" - dest: /etc/systemd/system/aw-dlp-influx-exporter.service - owner: root - group: root - mode: "0644" - when: aw_dlp_influx_enabled | default(false) | bool - - - name: Проверить локальный Rust AW DLP Influx exporter - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/dlp-influx-exporter" - delegate_to: localhost - register: aw_dlp_influx_exporter_rust_binary - become: false - when: aw_dlp_influx_enabled | default(false) | bool - - - name: Требовать Rust AW DLP Influx exporter artifact - ansible.builtin.assert: - that: - - aw_dlp_influx_exporter_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/dlp-influx-exporter" - when: aw_dlp_influx_enabled | default(false) | bool - - - name: Установить Rust AW DLP Influx exporter - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/dlp-influx-exporter" - dest: /usr/local/bin/aw-dlp-influx-exporter-rust - owner: root - group: root - mode: "0755" - when: - - aw_dlp_influx_enabled | default(false) | bool - - aw_dlp_influx_exporter_rust_binary.stat.exists | default(false) - - - name: Установить systemd timer AW DLP Influx exporter - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-dlp-influx-exporter.timer" - dest: /etc/systemd/system/aw-dlp-influx-exporter.timer - owner: root - group: root - mode: "0644" - when: aw_dlp_influx_enabled | default(false) | bool - - - name: Перезагрузить systemd после установки AW worktime API - ansible.builtin.systemd: - daemon_reload: true - - - name: Включить таймер AW-RUS healthd - ansible.builtin.systemd: - name: aw-rus-healthd.timer - enabled: true - state: started - - - name: Включить таймер AW-RUS SLO monitor - ansible.builtin.systemd: - name: aw-slo-monitor.timer - enabled: true - state: started - - - name: Включить таймер AW-RUS browser smoke - ansible.builtin.systemd: - name: aw-browser-smoke.timer - enabled: true - state: started - when: aw_browser_smoke_enabled | default(true) | bool - - - name: Включить и перезапустить DLP policy engine - ansible.builtin.systemd: - name: aw-dlp-policy-engine.service - enabled: true - state: restarted - when: aw_dlp_policy_engine_enabled | default(false) | bool - - - name: Включить и перезапустить timer CEF exporter - ansible.builtin.systemd: - name: aw-dlp-cef-exporter.timer - enabled: true - state: restarted - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Включить и перезапустить timer syslog forwarder - ansible.builtin.systemd: - name: aw-dlp-syslog-forwarder.timer - enabled: true - state: restarted - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Включить и перезапустить timer webhook sender - ansible.builtin.systemd: - name: aw-dlp-webhook-sender.timer - enabled: true - state: restarted - when: aw_dlp_integrations_enabled | default(true) | bool - - - name: Включить и перезапустить DLP case management - ansible.builtin.systemd: - name: aw-dlp-case-management.service - enabled: true - state: restarted - when: aw_dlp_case_management_enabled | default(true) | bool - - - name: Включить и перезапустить timer DLP compliance report - ansible.builtin.systemd: - name: aw-dlp-report-scheduler.timer - enabled: true - state: restarted - when: aw_dlp_compliance_enabled | default(true) | bool - - - name: Выполнить разовый прогон DLP compliance report - ansible.builtin.systemd: - name: aw-dlp-report-scheduler.service - state: started - failed_when: false - when: aw_dlp_compliance_enabled | default(true) | bool - - - name: Включить и перезапустить AW worktime API - ansible.builtin.systemd: - name: aw-worktime-api.service - enabled: true - state: restarted - - - name: Отключить legacy timer aw-worktime-afk-bridge (если есть) - ansible.builtin.systemd: - name: aw-worktime-afk-bridge.timer - enabled: false - state: stopped - failed_when: false - - - name: Включить и перезапустить AW worktime UI bridge timer - ansible.builtin.systemd: - name: aw-worktime-ui-bridge.timer - enabled: true - state: restarted - - - name: Выполнить разовый прогон AW worktime UI bridge - ansible.builtin.systemd: - name: aw-worktime-ui-bridge.service - state: started - failed_when: false - - - name: Включить и перезапустить AW worktime autoheal timer - ansible.builtin.systemd: - name: aw-worktime-autoheal.timer - enabled: true - state: restarted - - - name: Выполнить разовый прогон AW worktime autoheal - ansible.builtin.systemd: - name: aw-worktime-autoheal.service - state: started - failed_when: false - - - name: Включить и перезапустить AW worktime prewarm timer - ansible.builtin.systemd: - name: aw-worktime-prewarm.timer - enabled: true - state: restarted - - - name: Выполнить разовый прогон AW worktime prewarm - ansible.builtin.systemd: - name: aw-worktime-prewarm.service - state: started - failed_when: false - - - name: Включить и перезапустить AW worktime Influx exporter timer - ansible.builtin.systemd: - name: aw-worktime-influx-exporter.timer - enabled: true - state: restarted - when: aw_worktime_influx_enabled | default(false) | bool - - - name: Выполнить разовый прогон AW worktime Influx exporter - ansible.builtin.systemd: - name: aw-worktime-influx-exporter.service - state: started - when: aw_worktime_influx_enabled | default(false) | bool - - - name: Включить и перезапустить AW DLP Influx exporter timer - ansible.builtin.systemd: - name: aw-dlp-influx-exporter.timer - enabled: true - state: restarted - when: aw_dlp_influx_enabled | default(false) | bool - - - name: Выполнить разовый прогон AW DLP Influx exporter - ansible.builtin.systemd: - name: aw-dlp-influx-exporter.service - state: started - when: aw_dlp_influx_enabled | default(false) | bool - - - 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 - - (aw_webui_ru_index.stat.size | default(0) | int) > 0 - fail_msg: "Не найден index.html WebUI для применения RU patch." - - - name: Проверить, что index.html не опустел после применения RU patch - ansible.builtin.stat: - path: "{{ aw_server_webui_dir }}/index.html" - register: aw_webui_ru_index_after_patch - - - name: Подтвердить, что index.html остался непустым - ansible.builtin.assert: - that: - - aw_webui_ru_index_after_patch.stat.exists - - (aw_webui_ru_index_after_patch.stat.size | default(0) | int) > 0 - fail_msg: "index.html WebUI пуст после RU patch." - - - name: Удалить старые теги RU patch из index.html - ansible.builtin.replace: - path: "{{ aw_server_webui_dir }}/index.html" - regexp: ']+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*>' - replace: '' - - - name: Добавить cleanup script RU patch в index.html - ansible.builtin.replace: - path: "{{ aw_server_webui_dir }}/index.html" - regexp: '' - replace: '' - - - name: Добавить загрузчик RU patch перед закрытием body - ansible.builtin.replace: - path: "{{ aw_server_webui_dir }}/index.html" - regexp: '' - replace: '' - - - name: Check Rust merge AW DB binary artifact - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/merge-aw-server-dbs" - register: aw_merge_db_rust_binary - delegate_to: localhost - become: false - - - name: Требовать Rust merge AW DB artifact - ansible.builtin.assert: - that: - - aw_merge_db_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/merge-aw-server-dbs" - when: aw_legacy_db_merge_enabled | default(false) | bool - - - name: Install Rust merge AW DB binary - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/merge-aw-server-dbs" - dest: /usr/local/bin/merge-aw-server-dbs - owner: root - group: root - mode: "0755" - when: aw_merge_db_rust_binary.stat.exists | default(false) - - - 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_db_merge_enabled | default(false) | bool - - 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_db_merge_enabled | default(false) | bool - - 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_db_merge_enabled | default(false) | bool - - 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_db_merge_enabled | default(false) | bool - - aw_legacy_root_db.stat.exists | default(false) - - - name: Merge legacy root DB в target DB - ansible.builtin.command: - argv: - - /usr/local/bin/merge-aw-server-dbs - - --base - - /root/.local/share/activitywatch/aw-server-rust/sqlite.db - - --overlay - - "{{ aw_server_db_path }}" - - --output - - "{{ aw_server_db_path }}.merged" - register: aw_merge_result - failed_when: false - when: - - aw_legacy_db_merge_enabled | default(false) | bool - - aw_legacy_root_db.stat.exists | default(false) - - aw_target_db.stat.exists | default(false) - - - name: Показать результат merge legacy root DB - ansible.builtin.debug: - msg: "{{ aw_merge_result.stdout | default(aw_merge_result.stderr | default('merge not executed')) }}" - when: - - aw_legacy_db_merge_enabled | default(false) | bool - - 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_db_merge_enabled | default(false) | bool - - aw_legacy_root_db.stat.exists | default(false) - - aw_target_db.stat.exists | default(false) - - aw_merge_result is defined - - (aw_merge_result.rc | default(1) | int) == 0 - - - 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_db_merge_enabled | default(false) | bool - - 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/settings/" - method: GET - status_code: 200 - register: aw_settings_current - when: aw_apply_worktime_settings | default(false) | bool - - - 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: Проверить локальный Rust DLP aggregator - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/dlp-aggregator" - delegate_to: localhost - register: dlp_aggregator_rust_binary - become: false - - - name: Требовать Rust DLP aggregator artifact - ansible.builtin.assert: - that: - - dlp_aggregator_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/dlp-aggregator" - - - name: Установить Rust DLP aggregator - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/dlp-aggregator" - dest: /usr/local/bin/dlp-aggregator-rust - owner: root - group: root - mode: "0755" - when: dlp_aggregator_rust_binary.stat.exists | default(false) - - - name: Установить systemd unit для агрегатора - ansible.builtin.copy: - dest: /etc/systemd/system/activitywatch-dlp-aggregator.service - owner: root - group: root - mode: "0644" - 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/local/bin/dlp-aggregator-rust \ - --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] - OnCalendar=*:3/10:10 - AccuracySec=30s - RandomizedDelaySec=30s - Persistent=false - - [Install] - WantedBy=timers.target - - - name: Включить и запустить таймер агрегатора - ansible.builtin.systemd: - name: activitywatch-dlp-aggregator.timer - enabled: true - state: started - daemon_reload: true - - - name: Настроить IOC enrichment из Hayabusa Sigma - when: aw_dlp_ioc_enabled | default(false) | bool - block: - - name: Создать каталог IOC enrichment - ansible.builtin.file: - path: "{{ aw_dlp_ioc_workdir }}/{{ item }}" - state: directory - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0755" - loop: - - "" - - output - - tmp - - - name: Проверить локальный Rust extractor IOC из Sigma - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/extract-ioc-from-sigma" - delegate_to: localhost - register: aw_extract_ioc_from_sigma_rust_binary - become: false - - - name: Требовать Rust extractor IOC из Sigma - ansible.builtin.assert: - that: - - aw_extract_ioc_from_sigma_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/extract-ioc-from-sigma" - - - name: Установить Rust extractor IOC из Sigma - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/extract-ioc-from-sigma" - dest: /usr/local/bin/aw-extract-ioc-from-sigma - owner: root - group: root - mode: "0755" - - - name: Установить wrapper refresh IOC - ansible.builtin.copy: - dest: /usr/local/bin/aw-dlp-ioc-refresh.sh - owner: root - group: root - mode: "0755" - content: | - #!/usr/bin/env bash - set -euo pipefail - WORKDIR="{{ aw_dlp_ioc_workdir }}" - OUTDIR="${WORKDIR}/output" - TMPDIR="${WORKDIR}/tmp" - RULES_DIR="${TMPDIR}/hayabusa-rules" - ZIP_PATH="${TMPDIR}/hayabusa-rules.zip" - - mkdir -p "${OUTDIR}" "${TMPDIR}" - rm -rf "${RULES_DIR}" - curl -fsSL -o "${ZIP_PATH}" "{{ aw_dlp_ioc_rules_zip_url }}" - unzip -q -o "${ZIP_PATH}" -d "${TMPDIR}" - - EXTRACTED_DIR=$(find "${TMPDIR}" -maxdepth 1 -type d -name 'hayabusa-rules-*' | head -n1) - if [ -z "${EXTRACTED_DIR}" ]; then - echo "ERROR: hayabusa-rules archive extraction failed" >&2 - exit 1 - fi - mv "${EXTRACTED_DIR}" "${RULES_DIR}" - - /usr/local/bin/aw-extract-ioc-from-sigma \ - --rules-root "${RULES_DIR}" \ - --out-dir "${OUTDIR}" - - ln -sfn "${OUTDIR}" "${WORKDIR}/latest" - - - name: Установить systemd unit IOC refresh - ansible.builtin.copy: - dest: /etc/systemd/system/aw-dlp-ioc-refresh.service - owner: root - group: root - mode: "0644" - content: | - [Unit] - Description=Refresh DLP IOC blacklist from Hayabusa Sigma rules - After=network-online.target - Wants=network-online.target - - [Service] - Type=oneshot - ExecStart=/usr/local/bin/aw-dlp-ioc-refresh.sh - - - name: Установить systemd timer IOC refresh - ansible.builtin.copy: - dest: /etc/systemd/system/aw-dlp-ioc-refresh.timer - owner: root - group: root - mode: "0644" - content: | - [Unit] - Description=Run DLP IOC refresh from Hayabusa rules - - [Timer] - OnBootSec={{ aw_dlp_ioc_refresh_on_boot_sec }} - OnUnitActiveSec={{ aw_dlp_ioc_refresh_interval }} - Persistent=true - Unit=aw-dlp-ioc-refresh.service - - [Install] - WantedBy=timers.target - - - name: Включить и запустить IOC refresh timer - ansible.builtin.systemd: - name: aw-dlp-ioc-refresh.timer - enabled: true - state: started - daemon_reload: true - - - name: Выполнить принудительный refresh IOC - ansible.builtin.systemd: - name: aw-dlp-ioc-refresh.service - state: started - - - name: Проверить наличие IOC артефактов после refresh - ansible.builtin.stat: - path: "{{ aw_dlp_ioc_workdir }}/output/{{ item }}" - register: aw_dlp_ioc_artifacts - loop: - - ioc_blacklist.json - - ioc_blacklist.csv - - ioc_blacklist.sql - - - name: Assert по IOC артефактам - ansible.builtin.assert: - that: - - item.stat.exists - - (item.stat.size | int) > 100 - fail_msg: "Не сгенерирован IOC артефакт: {{ item.stat.path | default('unknown') }}" - loop: "{{ aw_dlp_ioc_artifacts.results }}" - - - 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: [200, 201] - when: aw_apply_worktime_settings | default(false) | bool - - - 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: [200, 201] - when: aw_apply_worktime_settings | default(false) | bool - - - name: Вычислить worktime durationDefault из aw_worktime_from/to - ansible.builtin.set_fact: - aw_worktime_duration_default_derived: >- - {{ - ( - ( - (((aw_worktime_to | default('17:00')).split(':')[0] | int) * 60 + ((aw_worktime_to | default('17:00')).split(':')[1] | int)) - - (((aw_worktime_from | default('00:00')).split(':')[0] | int) * 60 + ((aw_worktime_from | default('00:00')).split(':')[1] | int)) - ) * 60 - ) - }} - when: aw_apply_worktime_settings | default(false) | bool - - - name: Нормализовать durationDefault для ночных смен - ansible.builtin.set_fact: - aw_worktime_duration_default_effective: >- - {{ - (aw_worktime_duration_default_derived | int) - if (aw_worktime_duration_default_derived | int) > 0 - else ((aw_worktime_duration_default_derived | int) + 86400) - }} - when: aw_apply_worktime_settings | default(false) | bool - - - name: Проверить корректность durationDefault - ansible.builtin.assert: - that: - - aw_worktime_duration_default_effective | int > 0 - - aw_worktime_duration_default_effective | int <= 86400 - fail_msg: "Некорректный интервал рабочего времени: {{ aw_worktime_from }}..{{ aw_worktime_to }}" - when: aw_apply_worktime_settings | default(false) | bool - - - 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 }}\"" - headers: - Content-Type: application/json - status_code: [200, 201] - when: aw_apply_worktime_settings | default(false) | bool - - - 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 | 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 - - - name: Обновить IOC blacklist из hayabusa (если включено) - when: - - aw_hayabusa_ioc_refresh_enabled | default(false) | bool - - aw_hayabusa_rules_root is defined - - aw_hayabusa_ioc_output_dir is defined - block: - - name: Создать выходную директорию для IOC - ansible.builtin.file: - path: "{{ aw_hayabusa_ioc_output_dir }}" - state: directory - mode: "0755" - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - - - name: Обновить IOC blacklist из hayabusa правил - ansible.builtin.shell: - cmd: | - set -euo pipefail - {{ aw_repo_root }}/scripts/build_dlp_ioc_from_hayabusa.sh \ - "{{ aw_hayabusa_rules_root }}" \ - "{{ aw_hayabusa_ioc_output_dir }}" - args: - chdir: "{{ aw_repo_root }}" - register: aw_hayabusa_ioc_refresh_result - changed_when: "'IOC artifacts generated' in aw_hayabusa_ioc_refresh_result.stdout" - - - name: Показать результат обновления IOC - ansible.builtin.debug: - msg: "{{ aw_hayabusa_ioc_refresh_result.stdout }}" - when: aw_hayabusa_ioc_refresh_result.stdout is defined - - - name: Установить server-side Hayabusa runner - when: aw_hayabusa_runner_enabled | default(false) | bool - block: - - name: Создать каталоги Hayabusa - ansible.builtin.file: - path: "{{ item }}" - state: directory - owner: root - group: root - mode: "0755" - loop: - - "{{ aw_hayabusa_root }}" - - "{{ aw_hayabusa_root }}/releases" - - "{{ aw_hayabusa_release_dir }}" - - "{{ aw_hayabusa_reports_dir }}" - - "{{ aw_hayabusa_state_dir }}" - - "{{ aw_hayabusa_inbox_dir }}" - - "{{ aw_hayabusa_archive_dir }}" - - "{{ aw_hayabusa_incoming_dir }}" - - "{{ aw_hayabusa_staging_dir }}" - - "{{ aw_hayabusa_archive_packages_dir }}" - - "{{ aw_hayabusa_archive_extracted_dir }}" - - "{{ aw_hayabusa_logs_dir }}" - - - name: Скачать архив Hayabusa - ansible.builtin.get_url: - url: "{{ aw_hayabusa_download_url }}" - dest: "{{ aw_hayabusa_archive_path }}" - mode: "0644" - - - name: Распаковать pinned Hayabusa release - ansible.builtin.unarchive: - src: "{{ aw_hayabusa_archive_path }}" - dest: "{{ aw_hayabusa_release_dir }}" - remote_src: true - creates: "{{ aw_hayabusa_release_dir }}/{{ aw_hayabusa_binary_name }}" - - - name: Удалить временный архив Hayabusa после распаковки - ansible.builtin.file: - path: "{{ aw_hayabusa_archive_path }}" - state: absent - - - name: Нормализовать права release Hayabusa - ansible.builtin.file: - path: "{{ aw_hayabusa_release_dir }}" - state: directory - owner: root - group: root - mode: "0755" - recurse: true - - - name: Сделать бинарь Hayabusa исполняемым - ansible.builtin.file: - path: "{{ aw_hayabusa_release_dir }}/{{ aw_hayabusa_binary_name }}" - owner: root - group: root - mode: "0755" - state: file - - - name: Создать canonical symlink на текущий релиз Hayabusa - ansible.builtin.file: - src: "{{ aw_hayabusa_release_dir }}" - dest: "{{ aw_hayabusa_current_link }}" - state: link - force: true - - - name: Создать canonical symlink на бинарь Hayabusa - ansible.builtin.file: - src: "{{ aw_hayabusa_binary_name }}" - dest: "{{ aw_hayabusa_release_dir }}/hayabusa" - state: link - force: true - - - name: Установить wrapper aw-hayabusa - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa.sh" - dest: /usr/local/bin/aw-hayabusa - owner: root - group: root - mode: "0755" - - - name: Создать server-side ops bundle для Hayabusa - ansible.builtin.file: - path: "{{ item }}" - state: directory - owner: root - group: root - mode: "0755" - loop: - - /opt/activitywatch/aw-rus-ops - - /opt/activitywatch/aw-rus-ops/hayabusa - - /opt/activitywatch/aw-rus-ops/ansible - - /opt/activitywatch/aw-rus-ops/drop - - - name: Положить исходный wrapper в server-side ops bundle - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa.sh" - dest: /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa.sh - owner: root - group: root - mode: "0755" - - - name: Проверить локальный Rust helper link-case для Hayabusa - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/aw-hayabusa-link-case-rust" - delegate_to: localhost - register: aw_hayabusa_link_case_rust_binary - become: false - - - name: Установить Rust helper link-case для Hayabusa - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/aw-hayabusa-link-case-rust" - dest: /usr/local/bin/aw-hayabusa-link-case-rust - owner: root - group: root - mode: "0755" - when: aw_hayabusa_link_case_rust_binary.stat.exists | default(false) - - - name: Требовать Rust helper link-case для Hayabusa - ansible.builtin.assert: - that: - - aw_hayabusa_link_case_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/aw-hayabusa-link-case-rust" - - - name: Установить helper link-case для Hayabusa - ansible.builtin.copy: - content: | - #!/usr/bin/env bash - set -euo pipefail - exec /usr/local/bin/aw-hayabusa-link-case-rust "$@" - dest: /usr/local/bin/aw-hayabusa-link-case - owner: root - group: root - mode: "0755" - - - name: Проверить локальный Rust helper from-windows для Hayabusa - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/aw-hayabusa-from-windows-rust" - delegate_to: localhost - register: aw_hayabusa_from_windows_rust_binary - become: false - - - name: Установить Rust helper from-windows для Hayabusa - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/aw-hayabusa-from-windows-rust" - dest: /usr/local/bin/aw-hayabusa-from-windows-rust - owner: root - group: root - mode: "0755" - when: aw_hayabusa_from_windows_rust_binary.stat.exists | default(false) - - - name: Требовать Rust helper from-windows для Hayabusa - ansible.builtin.assert: - that: - - aw_hayabusa_from_windows_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/aw-hayabusa-from-windows-rust" - - - name: Установить helper from-windows для Hayabusa - ansible.builtin.copy: - content: | - #!/usr/bin/env bash - set -euo pipefail - exec /usr/local/bin/aw-hayabusa-from-windows-rust "$@" - dest: /usr/local/bin/aw-hayabusa-from-windows - owner: root - group: root - mode: "0755" - - - name: Положить README Hayabusa в server-side ops bundle - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/hayabusa/README.md" - dest: /opt/activitywatch/aw-rus-ops/hayabusa/README.md - owner: root - group: root - mode: "0644" - - - name: Проверить локальный Rust helper autoprocess для Hayabusa - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/aw-hayabusa-autoprocess-rust" - delegate_to: localhost - register: aw_hayabusa_autoprocess_rust_binary - become: false - - - name: Установить Rust helper autoprocess для Hayabusa - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/aw-hayabusa-autoprocess-rust" - dest: /usr/local/bin/aw-hayabusa-autoprocess-rust - owner: root - group: root - mode: "0755" - when: aw_hayabusa_autoprocess_rust_binary.stat.exists | default(false) - - - name: Требовать Rust helper autoprocess для Hayabusa - ansible.builtin.assert: - that: - - aw_hayabusa_autoprocess_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/aw-hayabusa-autoprocess-rust" - - - name: Установить helper autoprocess для Hayabusa - ansible.builtin.copy: - content: | - #!/usr/bin/env bash - set -euo pipefail - exec /usr/local/bin/aw-hayabusa-autoprocess-rust "$@" - dest: /usr/local/bin/aw-hayabusa-autoprocess - owner: root - group: root - mode: "0755" - - - name: Проверить локальный Rust helper case-alert для Hayabusa - ansible.builtin.stat: - path: "{{ aw_rust_release_dir }}/aw-hayabusa-case-alert-rust" - delegate_to: localhost - register: aw_hayabusa_case_alert_rust_binary - become: false - - - name: Установить Rust helper case-alert для Hayabusa - ansible.builtin.copy: - src: "{{ aw_rust_release_dir }}/aw-hayabusa-case-alert-rust" - dest: /usr/local/bin/aw-hayabusa-case-alert-rust - owner: root - group: root - mode: "0755" - when: aw_hayabusa_case_alert_rust_binary.stat.exists | default(false) - - - name: Требовать Rust helper case-alert для Hayabusa - ansible.builtin.assert: - that: - - aw_hayabusa_case_alert_rust_binary.stat.exists | default(false) - fail_msg: "Missing Rust artifact: {{ aw_rust_release_dir }}/aw-hayabusa-case-alert-rust" - - - name: Установить helper case-alert для Hayabusa - ansible.builtin.copy: - content: | - #!/usr/bin/env bash - set -euo pipefail - exec /usr/local/bin/aw-hayabusa-case-alert-rust "$@" - dest: /usr/local/bin/aw-hayabusa-case-alert - owner: root - group: root - mode: "0755" - - - name: Установить systemd unit aw-hayabusa-drop.service - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-hayabusa-drop.service" - dest: /etc/systemd/system/aw-hayabusa-drop.service - owner: root - group: root - mode: "0644" - notify: Перезагрузить systemd - - - name: Установить systemd unit aw-hayabusa-drop.path - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-hayabusa-drop.path" - dest: /etc/systemd/system/aw-hayabusa-drop.path - owner: root - group: root - mode: "0644" - notify: Перезагрузить systemd - - - name: Включить и запустить aw-hayabusa-drop.path - ansible.builtin.systemd: - name: aw-hayabusa-drop.path - enabled: true - state: started - daemon_reload: true - - - name: Проверить server-side runner через doctor - ansible.builtin.command: - cmd: /usr/local/bin/aw-hayabusa doctor - changed_when: false - - - name: Проверить загрузку profiles Hayabusa - ansible.builtin.shell: /usr/local/bin/aw-hayabusa profiles > /tmp/aw-hayabusa-profiles.txt - args: - executable: /bin/bash - changed_when: false - - - name: Проверить наличие standard profile у Hayabusa - ansible.builtin.shell: "grep -q 'standard:' /tmp/aw-hayabusa-profiles.txt" - args: - executable: /bin/bash - changed_when: false - - - name: Post-deploy health gate (aw-rus-healthd) - when: - - not ansible_check_mode - - aw_server_post_deploy_health_check_enabled | default(true) | bool - block: - - name: Запустить /usr/local/bin/aw-rus-healthd-rust --json - ansible.builtin.command: - cmd: /usr/local/bin/aw-rus-healthd-rust --json - register: aw_post_deploy_health - changed_when: false - failed_when: false - - - name: Показать результат aw-rus-healthd - ansible.builtin.debug: - msg: "{{ aw_post_deploy_health.stdout }}" - - handlers: - - name: Перезагрузить systemd - ansible.builtin.systemd: - daemon_reload: true - - - name: Перезапустить activitywatch - ansible.builtin.systemd: - name: activitywatch-server.service - state: restarted diff --git a/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml b/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml deleted file mode 100644 index 4a1734c..0000000 --- a/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml +++ /dev/null @@ -1,559 +0,0 @@ ---- -- name: Развернуть Windows/RDP collector'ы AWatch-rus - hosts: aw_windows - gather_facts: false - - vars: - ansible_winrm_operation_timeout_sec: 120 - ansible_winrm_read_timeout_sec: 180 - aw_windows_repo_root: "{{ playbook_dir | dirname }}" - aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus" - aw_windows_server_scheme: "http" - 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_builtin_administrator_name: "Администратор" - 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_policy_mode: "server" - aw_windows_policy_refresh_seconds: 300 - aw_windows_policy_engine_enabled: true - aw_windows_policy_engine_port: 5601 - aw_windows_policy_engine_scheme: "http" - aw_windows_hayabusa_auto_upload_enabled: true - aw_windows_hayabusa_auto_upload_interval_hours: 6 - aw_windows_hayabusa_auto_upload_hours_back: 6 - aw_windows_hayabusa_auto_upload_mode: "incident" - aw_windows_hayabusa_auto_upload_task_name: "ActivityWatch Hayabusa Upload" - aw_windows_file_1c_auto_upload_enabled: true - aw_windows_file_1c_auto_upload_interval_hours: 6 - aw_windows_file_1c_auto_upload_task_name: "ActivityWatch File1C Upload" - aw_windows_file_1c_target_user: "igor" - aw_windows_file_1c_registry_workbook_path: "E:\\USER1\\СПИСОК ПРЕДПРИЯТИЙ И ИХ РАСПРЕДЕЛЕНИЕ.xlsx" - aw_windows_afk_enabled_default: true - aw_windows_window_enabled_default: 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_forensics_root: "{{ aw_windows_state_root }}\\forensics\\evtx-exports" - aw_windows_logon_marker_enabled: true - aw_windows_process_events_enabled: false - 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-{{ lookup('env','USER') | default('ansible', true) }}" - aw_windows_launch_task_pattern: "ActivityWatch Launch *" - aw_windows_recovery_task_name: "ActivityWatch Recovery" - aw_windows_collector_guard_enabled: true - aw_windows_collector_guard_mode: "enforce" - aw_windows_collector_guard_service_name: "AWatchRusCollectorGuard" - aw_windows_collector_guard_loop_seconds: 60 - 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_api_smoke_check_window_enabled_default: true - aw_windows_api_smoke_check_window_bucket: "" - aw_windows_api_smoke_check_min_events: 1 - 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: Вычислить inventory host AW server по умолчанию - ansible.builtin.set_fact: - aw_server_inventory_host_effective: "{{ (groups['aw_server'] | default([]) | first) | default('', true) }}" - - - name: Вычислить inventory host analytics node по умолчанию - ansible.builtin.set_fact: - aw_analytics_inventory_host_effective: "{{ (groups['proxmox'] | default([]) | first) | default('', true) }}" - - - name: Вычислить effective host для AW server - ansible.builtin.set_fact: - aw_windows_server_host_effective: >- - {{ - aw_windows_server_host - | default( - ( - hostvars[aw_server_inventory_host_effective].ansible_host - | default(aw_server_inventory_host_effective, true) - ) - if (aw_server_inventory_host_effective | length) > 0 - else '', - true - ) - }} - - - name: Вычислить effective каталог health validation на AW server - ansible.builtin.set_fact: - aw_windows_health_validation_dir_effective: >- - {{ - ( - hostvars[aw_server_inventory_host_effective].aw_rus_health_validation_dir - | default('/var/lib/activitywatch/health/windows-validation', true) - ) - if (aw_server_inventory_host_effective | length) > 0 - else '' - }} - - - name: Вычислить effective host для policy engine - ansible.builtin.set_fact: - aw_windows_policy_engine_host_effective: >- - {{ - aw_windows_policy_engine_host - | default(aw_windows_server_host_effective, true) - }} - - - name: Вычислить effective host для file-1C analytics - ansible.builtin.set_fact: - aw_windows_file_1c_target_host_effective: >- - {{ - aw_windows_file_1c_target_host - | default( - ( - hostvars[aw_analytics_inventory_host_effective].ansible_host - | default(aw_analytics_inventory_host_effective, true) - ) - if (aw_analytics_inventory_host_effective | length) > 0 - else '', - true - ) - }} - - - name: Проверить обязательные переменные - ansible.builtin.assert: - that: - - aw_windows_server_host_effective | length > 0 - - aw_windows_server_port is defined - - aw_windows_server_scheme is defined - - aw_windows_domain is defined - - aw_windows_builtin_administrator_name is defined - - aw_windows_builtin_administrator_name | length > 0 - - aw_windows_users_effective | length > 0 - - aw_windows_install_root is defined - - aw_windows_state_root is defined - - (not (aw_windows_file_1c_auto_upload_enabled | bool)) or (aw_windows_file_1c_target_host_effective | length > 0) - fail_msg: "Не заданы обязательные переменные Windows-развёртывания." - - - name: Нормализовать effective флаги collector'ов и smoke-check - ansible.builtin.set_fact: - aw_windows_afk_enabled_effective: "{{ (aw_windows_afk_enabled | default(aw_windows_afk_enabled_default)) | bool }}" - aw_windows_window_enabled_effective: "{{ (aw_windows_window_enabled | default(aw_windows_window_enabled_default)) | bool }}" - aw_windows_api_smoke_check_window_enabled_effective: "{{ (aw_windows_api_smoke_check_window_enabled | default(aw_windows_api_smoke_check_window_enabled_default)) | bool }}" - - - 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 - - dlp-policy-client.ps1 - - email-outbound-collector.ps1 - - file-operations-collector.ps1 - - worktime-session-collector.ps1 - - export-evtx-for-hayabusa.ps1 - - export-upload-hayabusa-to-aw-server.ps1 - - export-upload-file-1c-telemetry.ps1 - - sync-dlp-evidence-artifacts.ps1 - - migrate-awatch-rus-paths.ps1 - - deploy-domain-users.ps1 - - deploy-ensemble.ps1 - - hardening-recovery.ps1 - - AWatchRusCollectorGuardService.cs - - aw-collector-guard.ps1 - - install-collector-guard-service.ps1 - - rebuild-worktime-tasks.ps1 - - audit-cryptopro.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' - $env:AWATCH_RUS_BUILTIN_ADMINISTRATOR_NAME = "{{ aw_windows_builtin_administrator_name }}" - $params = @{ - ServerScheme = "{{ aw_windows_server_scheme }}" - ServerHost = "{{ aw_windows_server_host_effective }}" - 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_effective | bool) else '$false' }} - WindowEnabled = {{ '$true' if (aw_windows_window_enabled_effective | 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 }}" - EvtxExportRoot = "{{ aw_windows_forensics_root }}" - EvtxRetentionDays = {{ aw_windows_evtx_retention_days | int }} - LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }} - ProcessEventsEnabled = {{ '$true' if (aw_windows_process_events_enabled | bool) else '$false' }} - PolicyMode = "{{ aw_windows_policy_mode }}" - PolicyEngineEnabled = {{ '$true' if (aw_windows_policy_engine_enabled | bool) else '$false' }} - PolicyEngineHost = "{{ aw_windows_policy_engine_host_effective }}" - PolicyEnginePort = {{ aw_windows_policy_engine_port }} - PolicyEngineScheme = "{{ aw_windows_policy_engine_scheme }}" - PolicyRefreshSeconds = {{ aw_windows_policy_refresh_seconds }} - HayabusaAutoUploadEnabled = {{ '$true' if (aw_windows_hayabusa_auto_upload_enabled | bool) else '$false' }} - HayabusaAutoUploadIntervalHours = {{ aw_windows_hayabusa_auto_upload_interval_hours | int }} - HayabusaAutoUploadHoursBack = {{ aw_windows_hayabusa_auto_upload_hours_back | int }} - HayabusaAutoUploadMode = "{{ aw_windows_hayabusa_auto_upload_mode }}" - HayabusaAutoUploadTaskName = "{{ aw_windows_hayabusa_auto_upload_task_name }}" - File1CAutoUploadEnabled = {{ '$true' if (aw_windows_file_1c_auto_upload_enabled | bool) else '$false' }} - File1CAutoUploadIntervalHours = {{ aw_windows_file_1c_auto_upload_interval_hours | int }} - File1CAutoUploadTaskName = "{{ aw_windows_file_1c_auto_upload_task_name }}" - File1CTargetHost = "{{ aw_windows_file_1c_target_host_effective }}" - File1CTargetUser = "{{ aw_windows_file_1c_target_user }}" - File1CRegistryWorkbookPath = "{{ aw_windows_file_1c_registry_workbook_path }}" - 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_evtx_channels | default([]) | length) > 0 %} - $params.EvtxChannels = @( - {% for channel in aw_windows_evtx_channels %} - "{{ channel }}"{% if not loop.last %},{% endif %} - {% endfor %} - ) - {% endif %} - {% if (aw_windows_hostname_override | default('') | string | length) > 0 %} - $params.AwHostname = "{{ aw_windows_hostname_override }}" - {% endif %} - {% if aw_windows_skip_hardening | bool %} - $params.SkipHardening = $true - {% endif %} - {% if aw_windows_integration_test_enabled | bool %} - $params.IntegrationTestEnabled = $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' - - function Get-CollectorKey { - param([string]$CommandLine) - if (-not $CommandLine) { return $null } - $cl = $CommandLine.ToLowerInvariant() - if ($cl -like '*browser-domains-native-collector.ps1*') { return 'browser' } - if ($cl -like '*file-operations-collector.ps1*') { return 'fileops' } - if ($cl -like '*dlp-endpoint-signals-collector.ps1*') { return 'endpoint' } - if ($cl -like '*email-outbound-collector.ps1*') { return 'email' } - if ($cl -like '*worktime-session-collector.ps1*') { return 'worktime' } - return $null - } - - $collectorProcs = Get-CimInstance Win32_Process | - Where-Object { $_.Name -eq 'powershell.exe' -and $_.CommandLine } | - ForEach-Object { - $key = Get-CollectorKey -CommandLine $_.CommandLine - if ($key) { - $groupKey = if ($key -eq 'worktime') { 'worktime::global' } else { '{0}::{1}' -f $key, ([int]$_.SessionId) } - [pscustomobject]@{ - ProcessId = [int]$_.ProcessId - SessionId = [int]$_.SessionId - CreationDate = $_.CreationDate - CollectorKey = $key - GroupKey = $groupKey - } - } - } | - Where-Object { $_ -ne $null } - - # Keep only one process per collector scope: worktime collector is global, others stay per-session. - foreach ($group in ($collectorProcs | Group-Object GroupKey)) { - $ordered = @($group.Group | Sort-Object CreationDate -Descending) - if ($ordered.Count -le 1) { continue } - foreach ($dup in $ordered | Select-Object -Skip 1) { - Stop-Process -Id $dup.ProcessId -Force -ErrorAction SilentlyContinue - } - } - - # Force managed collectors/watchers to reload the freshly deployed scripts. - Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - ( - ($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and - $_.CommandLine -match 'C:\\ProgramData\\AWatch-rus\\' -and - $_.CommandLine -match '(collector|launch-watchers|recovery-loop)\.ps1' - ) -or - ($_.Name -ieq 'aw-watcher-afk.exe') -or - ($_.Name -ieq 'aw-watcher-window.exe') - } | - ForEach-Object { - Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue - } - - Start-Sleep -Seconds 2 - - $modulePath = "{{ aw_windows_deploy_root }}\windows\ActivityWatch.Windows.Common.psm1" - Import-Module $modulePath -Force - - # Ensure tasks are enabled (some environments keep them disabled, causing "0s" in WebUI). - try { - Enable-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}" -ErrorAction SilentlyContinue | Out-Null - } catch {} - - $config = Get-Content -Raw -LiteralPath "{{ aw_windows_state_root }}\deployment-config.json" | ConvertFrom-Json - $configPaths = Get-ActivityWatchRecoveryConfigPaths -PrimaryConfigPath "{{ aw_windows_state_root }}\deployment-config.json" - $taskDefs = @(Get-ActivityWatchRecoveryTaskDefinitions -ConfigPaths $configPaths) - $sessionRecords = @(Get-ActivityWatchSessionRecords) - - foreach ($taskDef in @($taskDefs)) { - try { Enable-ScheduledTask -TaskName ([string]$taskDef.taskName) -ErrorAction SilentlyContinue | Out-Null } catch {} - } - - $recoveryTask = Get-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}" -ErrorAction SilentlyContinue - if ($recoveryTask -and $recoveryTask.State -notin @('Running', 'Queued')) { - Start-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}" - } - foreach ($taskDef in @($taskDefs)) { - if (-not (Test-ActivityWatchUserHasManagedSession -UserId ([string]$taskDef.userId) -SessionRecords $sessionRecords -IncludeLive -IncludeDisconnected)) { - continue - } - $launchTask = Get-ScheduledTask -TaskName ([string]$taskDef.taskName) -ErrorAction SilentlyContinue - if ($launchTask -and $launchTask.State -notin @('Running', 'Queued')) { - Start-ScheduledTask -TaskName ([string]$taskDef.taskName) -ErrorAction SilentlyContinue - } - } - - - name: Установить session-aware AWatch-rus Collector Guard service - when: aw_windows_collector_guard_enabled | bool - ansible.windows.win_powershell: - script: | - $ErrorActionPreference = 'Stop' - $guardParams = @{ - ConfigPath = "{{ aw_windows_state_root }}\deployment-config.json" - Mode = "{{ aw_windows_collector_guard_mode }}" - ServiceName = "{{ aw_windows_collector_guard_service_name }}" - LoopSeconds = {{ aw_windows_collector_guard_loop_seconds | int }} - } - & "{{ aw_windows_deploy_root }}\windows\install-collector-guard-service.ps1" @guardParams - - - 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 worktime smoke-check bucket - when: - - aw_windows_api_smoke_check_enabled | bool - - aw_windows_hostname_result.stdout is defined - 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-worktime-sessions_' ~ (aw_windows_hostname_result.stdout | trim) - }} - - - name: Вычислить AW Window smoke-check bucket - when: - - aw_windows_api_smoke_check_enabled | bool - - aw_windows_api_smoke_check_window_enabled_effective | bool - - aw_windows_window_enabled_effective | bool - - aw_windows_hostname_result.stdout is defined - ansible.builtin.set_fact: - aw_windows_api_smoke_check_window_bucket_effective: >- - {{ - aw_windows_api_smoke_check_window_bucket - if (aw_windows_api_smoke_check_window_bucket | default('') | string | length) > 0 - else 'aw-watcher-window_' ~ (aw_windows_hostname_result.stdout | trim) - }} - - - name: Выполнить AW API smoke-check (worktime bucket должен получать события) - when: - - aw_windows_api_smoke_check_enabled | bool - ansible.builtin.uri: - url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host_effective }}:{{ 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 - return_content: true - register: aw_windows_api_smoke_result - failed_when: false - until: >- - (aw_windows_api_smoke_result.status | default(0)) == 200 - and ((aw_windows_api_smoke_result.json | default([])) | length) >= aw_windows_api_smoke_check_min_events - and ( - (aw_windows_api_smoke_result.json | default([])) - | selectattr('data.source', 'equalto', 'worktime-session-collector') - | list - | length - ) > 0 - retries: 12 - delay: 5 - ignore_errors: "{{ (not aw_windows_fail_on_validation_error | bool) }}" - delegate_to: localhost - changed_when: false - - - name: Выполнить AW API smoke-check (Window bucket должен получать события) - when: - - aw_windows_api_smoke_check_enabled | bool - - aw_windows_api_smoke_check_window_enabled_effective | bool - - aw_windows_window_enabled_effective | bool - ansible.builtin.uri: - url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host_effective }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_window_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}" - method: GET - status_code: 200 - return_content: true - register: aw_windows_api_smoke_window_result - failed_when: false - until: >- - (aw_windows_api_smoke_window_result.status | default(0)) == 200 - and ((aw_windows_api_smoke_window_result.json | default([])) | length) >= aw_windows_api_smoke_check_min_events - retries: 12 - delay: 5 - ignore_errors: "{{ (not aw_windows_fail_on_validation_error | bool) }}" - delegate_to: localhost - changed_when: false - - - 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 12 | 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: Убедиться, что каталог Windows validation существует на AW server - when: - - aw_server_inventory_host_effective | length > 0 - - aw_windows_health_validation_dir_effective | length > 0 - ansible.builtin.file: - path: "{{ aw_windows_health_validation_dir_effective }}" - state: directory - mode: "0755" - delegate_to: "{{ aw_server_inventory_host_effective }}" - - - name: Опубликовать validation report на AW server для aw-rus-healthd - when: - - aw_server_inventory_host_effective | length > 0 - - aw_windows_health_validation_dir_effective | length > 0 - ansible.builtin.copy: - src: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json" - dest: "{{ aw_windows_health_validation_dir_effective }}/{{ inventory_hostname }}-aw_validate_ansible.json" - mode: "0644" - delegate_to: "{{ aw_server_inventory_host_effective }}" - - - name: Проверить статус валидации - ansible.builtin.shell: | - python3 - <<'PY' - import json, sys - with open('{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json', 'r', encoding='utf-8-sig') as f: - data = json.load(f) - if not data.get('overallOk', False): - failed = ", ".join(data.get("summary", {}).get("failedSections", [])) or "unknown" - print(f"Validation failed for {{ inventory_hostname }}: {failed}") - sys.exit(1) - PY - delegate_to: localhost - when: aw_windows_fail_on_validation_error | bool diff --git a/install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml b/install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml deleted file mode 100644 index 7897f05..0000000 --- a/install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml +++ /dev/null @@ -1,86 +0,0 @@ -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_server_inventory_host: "{{ (groups['aw_server'] | default([]) | first) | default('aw-server', true) }}" -aw_server_public_host: "{{ (hostvars[aw_server_inventory_host].ansible_host | default(aw_server_inventory_host, true)) if (aw_server_inventory_host | length) > 0 else 'aw-server' }}" -aw_worktime_report_base: "http://{{ aw_server_public_host }}:5610" -aw_worktime_timezone: "Europe/Moscow" -aw_worktime_influx_enabled: false -aw_worktime_influx_url: "http://:8086" -aw_worktime_influx_org: "proxmox" -aw_worktime_influx_bucket: "aw_metrics" -aw_worktime_influx_hosts: "SHARKON2025" -aw_worktime_influx_days: "today,yesterday" -aw_worktime_influx_token: "" -aw_dlp_influx_enabled: false -aw_dlp_influx_url: "http://:8086" -aw_dlp_influx_org: "proxmox" -aw_dlp_influx_bucket: "aw_metrics" -aw_dlp_influx_hosts: "SHARKON2025" -aw_dlp_influx_lookback_days: 30 -aw_dlp_influx_event_limit: 2000 -aw_dlp_influx_token: "" -aw_monitored_windows_host: "" -aw_monitored_windows_hostname: "SHARKON2025" -aw_rus_health_worktime_api_base: "http://127.0.0.1:5610" -aw_rus_health_state_dir: "{{ aw_server_data_dir }}/health" -aw_rus_health_validation_dir: "{{ aw_rus_health_state_dir }}/windows-validation" -aw_browser_smoke_enabled: true -aw_browser_smoke_engine: "chromium-cli" -aw_legacy_db_merge_enabled: false -aw_browser_smoke_timeout_ms: 20000 -aw_browser_smoke_render_timeout_ms: 15000 -aw_hayabusa_auto_case_enabled: true -aw_hayabusa_auto_case_min_severity: "medium" -aw_hayabusa_telegram_enabled: true -aw_hayabusa_telegram_min_severity: "high" -aw_hayabusa_telegram_bot_token: "" -aw_hayabusa_telegram_chat_ids: "" - -aw_repo_root: "{{ playbook_dir | dirname }}" - -# Применить базовые категории и views для рабочего времени через AW settings API. -# При прод-обновлениях это нужно оставлять включённым, иначе UI остаётся без views/classes. -aw_apply_worktime_settings: true - -# Дополнительные origin для aw-server-rust CORS. -# Обязательно включите тот origin, с которого реально открывается Web UI. -aw_server_cors_origins: - - "http://127.0.0.1:5600" - - "http://localhost:5600" - - "http://{{ aw_server_public_host }}:5600" - - "http://aw-server:5600" - -# Опциональные значения периода рабочего времени в Web UI. -# startOfDay задаёт границу дня и стартовое время окна отчёта. -# durationDefault задаёт диапазон по умолчанию в секундах. -# -# Рекомендуется явно задать рабочий интервал и дать playbook вычислить duration. -aw_worktime_from: "00: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/" -aw_health_strict_fileops: 0 - -aw_dlp_policy_engine_enabled: true -aw_dlp_policy_engine_bind_host: "0.0.0.0" -aw_dlp_policy_engine_port: 5601 -aw_dlp_policy_engine_db_path: "{{ aw_server_data_dir }}/dlp-policy-engine.sqlite" -aw_dlp_content_analysis_enabled: true -aw_dlp_integrations_enabled: true -aw_dlp_case_management_enabled: true -aw_dlp_case_bind_host: "0.0.0.0" -aw_dlp_case_port: 5602 -aw_dlp_case_db_path: "/opt/activitywatch/dlp-case-management/cases.db" -aw_dlp_compliance_enabled: true -aw_dlp_compliance_report_dir: "/opt/activitywatch/dlp-compliance/reports" -aw_dlp_compliance_template_path: "/opt/activitywatch/dlp-compliance/templates/152-fz-report.html" -aw_server_post_deploy_health_check_enabled: true diff --git a/install-kit-awindows-20260427-211240/ansible/group_vars/pfsense-poller.example.yml b/install-kit-awindows-20260427-211240/ansible/group_vars/pfsense-poller.example.yml deleted file mode 100644 index 37357f9..0000000 --- a/install-kit-awindows-20260427-211240/ansible/group_vars/pfsense-poller.example.yml +++ /dev/null @@ -1,30 +0,0 @@ -aw_pfsense_poller_config: - poll_interval_seconds: 60 - aw: - server_host: "" - server_port: 5600 - hostname: "PFSENSE-EDGE01" - pulse_time_seconds: 120 - timeout_seconds: 15 - pfsense: - name: "pfSense Edge 01" - host: "" - scheme: "https" - verify_tls: false - timeout_seconds: 15 - auth: - api_key: "replace-me" - api_secret: "replace-me" - endpoints: - - name: "system-status" - path: "/api/v2/status/system" - bucket_prefix: "aw-pfsense-health" - bucket_type: "aw.pfsense.health" - - name: "interfaces" - path: "/api/v2/interfaces" - bucket_prefix: "aw-pfsense-interfaces" - bucket_type: "aw.pfsense.interfaces" - - name: "gateways" - path: "/api/v2/status/gateways" - bucket_prefix: "aw-pfsense-gateways" - bucket_type: "aw.pfsense.gateways" diff --git a/install-kit-awindows-20260427-211240/ansible/group_vars/proxmox-matrix.example.yml b/install-kit-awindows-20260427-211240/ansible/group_vars/proxmox-matrix.example.yml deleted file mode 100644 index ffe4b88..0000000 --- a/install-kit-awindows-20260427-211240/ansible/group_vars/proxmox-matrix.example.yml +++ /dev/null @@ -1,38 +0,0 @@ -proxmox_ct_matrix: - - id: "203" - hostname: "activitywatch-user1" - storage: "local-lvm" - template: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst" - rootfs_size: "8G" - cores: "2" - memory: "2048" - swap: "512" - bridge: "vmbr10" - ip: "10.20.30.13/24" - gw: "10.20.30.1" - vlan: "" - nameserver: "1.1.1.1 8.8.8.8" - searchdomain: "example.internal" - password: "CHANGE_ME" - unprivileged: "1" - onboot: "1" - features: "nesting=1,keyctl=1" - - - id: "204" - hostname: "activitywatch-user2" - storage: "local-lvm" - template: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst" - rootfs_size: "8G" - cores: "2" - memory: "2048" - swap: "512" - bridge: "vmbr10" - ip: "10.20.30.14/24" - gw: "10.20.30.1" - vlan: "" - nameserver: "1.1.1.1 8.8.8.8" - searchdomain: "example.internal" - password: "CHANGE_ME" - unprivileged: "1" - onboot: "1" - features: "nesting=1,keyctl=1" diff --git a/install-kit-awindows-20260427-211240/ansible/group_vars/proxmox.example.yml b/install-kit-awindows-20260427-211240/ansible/group_vars/proxmox.example.yml deleted file mode 100644 index 07e6eda..0000000 --- a/install-kit-awindows-20260427-211240/ansible/group_vars/proxmox.example.yml +++ /dev/null @@ -1,18 +0,0 @@ -proxmox_ct_id: "203" -proxmox_ct_hostname: "activitywatch-server" -proxmox_ct_storage: "local-lvm" -proxmox_ct_template: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst" -proxmox_ct_rootfs_size: "8G" -proxmox_ct_cores: "2" -proxmox_ct_memory: "2048" -proxmox_ct_swap: "512" -proxmox_ct_bridge: "vmbr10" -proxmox_ct_ip: "10.20.30.13/24" -proxmox_ct_gw: "10.20.30.1" -proxmox_ct_vlan: "" -proxmox_ct_nameserver: "1.1.1.1 8.8.8.8" -proxmox_ct_searchdomain: "example.internal" -proxmox_ct_password: "CHANGE_ME" -proxmox_ct_unprivileged: "1" -proxmox_ct_onboot: "1" -proxmox_ct_features: "nesting=1,keyctl=1" diff --git a/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml b/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml deleted file mode 100644 index 9f6ad99..0000000 --- a/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml +++ /dev/null @@ -1,80 +0,0 @@ -aw_windows_repo_root: "{{ playbook_dir | dirname }}" -aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus" -aw_windows_server_scheme: "http" -# Leave empty to derive from the first host in [aw_server] inventory. -aw_windows_server_host: "" -aw_windows_server_port: 5600 -aw_windows_hayabusa_auto_upload_enabled: false -aw_windows_hayabusa_auto_upload_interval_hours: 6 -aw_windows_hayabusa_auto_upload_hours_back: 6 -aw_windows_hayabusa_auto_upload_mode: "incident" -aw_windows_hayabusa_auto_upload_task_name: "ActivityWatch Hayabusa Upload" -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" -# Localized name of the built-in local Administrator account (SID ending in -500). -# On the current Russian Windows host this must stay "Администратор"; -# do not replace it with "Administrator" unless the target OS account is actually named that way. -aw_windows_builtin_administrator_name: "Администратор" -aw_windows_users: - - Администратор - - user1 - - user2 - - user3 - - user4 - - user5 -aw_windows_extra_users: [] -# Например: -# aw_windows_extra_users: -# - Администратор - -# Единые Windows/RDP пути: те же, что использует InnoSetup. -aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin" -aw_windows_state_root: "C:\\ProgramData\\AWatch-rus" -aw_windows_hostname_override: "" # Например: SHARKON2025 -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_forensics_root: "{{ aw_windows_state_root }}\\forensics\\evtx-exports" -aw_windows_evtx_retention_days: 14 -aw_windows_evtx_channels: - - Security - - System - - Application - - Microsoft-Windows-PowerShell/Operational - - Microsoft-Windows-TerminalServices-LocalSessionManager/Operational - - Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational -aw_windows_logon_marker_enabled: true -aw_windows_process_events_enabled: false -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-{{ lookup('env','USER') | default('ansible', true) }}" -aw_windows_fail_on_validation_error: true - -# Collector Guard supervises collectors, but ActivityWatch Recovery must stay enabled -# as a fallback and as the launch-task bootstrap path for managed RDP sessions. -aw_windows_collector_guard_enabled: true -aw_windows_collector_guard_mode: "enforce" -aw_windows_collector_guard_service_name: "AWatchRusCollectorGuard" -aw_windows_collector_guard_loop_seconds: 60 - -# Безопасная миграция текущего прода со старых путей в единый профиль AWatch-rus. -aw_windows_migration_enabled: true -aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2" -aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2" -aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json" - -# По умолчанию AFK bucket вычисляется как aw-watcher-afk_. -# Задайте явное значение только если watcher пишет в нестандартный bucket. -aw_windows_api_smoke_check_enabled: true -aw_windows_api_smoke_check_bucket: "" -aw_windows_api_smoke_check_limit: 10 diff --git a/install-kit-awindows-20260427-211240/ansible/install_full_stack.yml b/install-kit-awindows-20260427-211240/ansible/install_full_stack.yml deleted file mode 100644 index f2694c9..0000000 --- a/install-kit-awindows-20260427-211240/ansible/install_full_stack.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -# Полный установщик 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] -# 5) импорт Grafana dashboard'ов на [grafana] -# -# Примечания: -# - Заполняйте только нужные группы 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 -- import_playbook: deploy_grafana_dashboards.yml diff --git a/install-kit-awindows-20260427-211240/ansible/inventory.example.ini b/install-kit-awindows-20260427-211240/ansible/inventory.example.ini deleted file mode 100644 index 87aac03..0000000 --- a/install-kit-awindows-20260427-211240/ansible/inventory.example.ini +++ /dev/null @@ -1,17 +0,0 @@ -[proxmox] -pve-main ansible_host=192.168.10.2 ansible_user=root ansible_port=22 - -[aw_server] -aw-ct ansible_host=10.20.30.13 ansible_user=root ansible_port=22 - -[aw_windows] -# Примечание: в русифицированных Windows часто нужен "Администратор", а не "Administrator". -win-node1 ansible_host= 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 - -[grafana] -# Для API-import playbook достаточно указать grafana_url. -# SSH-подключение не требуется: playbook работает через HTTP API с control host. -grafana-main grafana_url=http://10.20.30.11:3000 diff --git a/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml b/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml deleted file mode 100644 index c6318db..0000000 --- a/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -- name: Deprecated Proxmox CT bootstrap path - hosts: proxmox - gather_facts: false - - tasks: - - name: Stop deprecated Python-era CT bootstrap - ansible.builtin.fail: - msg: >- - ansible/provision_proxmox_ct_and_deploy_aw.yml is deprecated because - its bootstrap path copied removed Python service entrypoints. Use the - existing DetMir AW server and ansible/deploy_aw_server.yml for the - Rust application deploy. Rebuild this CT bootstrap path separately - before using it for new infrastructure. diff --git a/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml b/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml deleted file mode 100644 index 3819d24..0000000 --- a/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -- name: Deprecated Proxmox CT matrix bootstrap path - hosts: proxmox - gather_facts: false - - tasks: - - name: Stop deprecated Python-era CT matrix bootstrap - ansible.builtin.fail: - msg: >- - ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml is deprecated - because its bootstrap path copied removed Python service entrypoints. - Use the existing DetMir AW server and ansible/deploy_aw_server.yml for - the Rust application deploy. Rebuild this CT bootstrap path separately - before using it for new infrastructure. diff --git a/install-kit-awindows-20260427-211240/ansible/tasks/provision_ct_and_deploy_aw.yml b/install-kit-awindows-20260427-211240/ansible/tasks/provision_ct_and_deploy_aw.yml deleted file mode 100644 index a2610fd..0000000 --- a/install-kit-awindows-20260427-211240/ansible/tasks/provision_ct_and_deploy_aw.yml +++ /dev/null @@ -1,230 +0,0 @@ ---- -- name: Проверить обязательные переменные CT - ansible.builtin.assert: - that: - - ct_id is defined - - ct_hostname is defined - - ct_storage is defined - - ct_template is defined - - ct_rootfs_size is defined - - ct_cores is defined - - ct_memory is defined - - ct_swap is defined - - ct_bridge is defined - - ct_ip is defined - - ct_gw is defined - - ct_password is defined - - ct_unprivileged is defined - - ct_onboot is defined - - ct_features is defined - - aw_repo_root is defined - - aw_server_version is defined - - aw_server_download_url is defined - - aw_server_bind_host is defined - - aw_server_port is defined - - aw_server_webui_dir is defined - - aw_server_data_dir is defined - - aw_server_log_dir is defined - - aw_server_user is defined - - aw_server_group is defined - fail_msg: "Не заданы обязательные переменные для создания CT и развёртывания." - -- 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: Проверить, существует ли CT - ansible.builtin.command: - argv: - - pct - - status - - "{{ ct_id }}" - register: ct_status_check - failed_when: false - changed_when: false - -- name: Создать CT, если он отсутствует - ansible.builtin.command: - argv: - - pct - - create - - "{{ ct_id }}" - - "{{ ct_template }}" - - --hostname - - "{{ ct_hostname }}" - - --cores - - "{{ ct_cores }}" - - --memory - - "{{ ct_memory }}" - - --swap - - "{{ ct_swap }}" - - --rootfs - - "{{ ct_storage }}:{{ ct_rootfs_size }}" - - --password - - "{{ ct_password }}" - - --unprivileged - - "{{ ct_unprivileged }}" - - --onboot - - "{{ ct_onboot }}" - - --features - - "{{ ct_features }}" - - --net0 - - "{{ ct_net0 }}" - - --nameserver - - "{{ ct_nameserver | default('') }}" - - --searchdomain - - "{{ ct_searchdomain | default('') }}" - - --ostype - - debian - when: ct_status_check.rc != 0 - no_log: true - -- name: Проверить текущее состояние CT - ansible.builtin.command: - argv: - - pct - - status - - "{{ ct_id }}" - register: ct_runtime_status - changed_when: false - -- name: Запустить CT, если он остановлен - ansible.builtin.command: - argv: - - pct - - start - - "{{ ct_id }}" - when: "'stopped' in ct_runtime_status.stdout" - -- name: Создать bootstrap каталог на Proxmox host - ansible.builtin.file: - path: "{{ item }}" - state: directory - mode: "0700" - loop: - - "{{ proxmox_bootstrap_dir }}" - - "{{ proxmox_bootstrap_dir }}/settings" - -- 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: Установить базовые зависимости ОС внутри CT - ansible.builtin.command: - argv: - - pct - - exec - - "{{ ct_id }}" - - -- - - bash - - -lc - - | - set -euo pipefail - export DEBIAN_FRONTEND=noninteractive - apt-get update - apt-get install -y curl ca-certificates bash unzip xz-utils jq rsync openssh-server 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: Передать bootstrap файлы внутрь CT - ansible.builtin.command: - argv: - - pct - - push - - "{{ ct_id }}" - - "{{ proxmox_bootstrap_dir }}/{{ item }}" - - "/root/bootstrap/{{ item }}" - loop: "{{ aw_bootstrap_files }}" - -- name: Записать AW server env во временный каталог Proxmox host - ansible.builtin.copy: - dest: "{{ proxmox_bootstrap_dir }}/aw-server.env" - mode: "0600" - content: | - AW_SERVER_VERSION={{ aw_server_version }} - AW_SERVER_DOWNLOAD_URL={{ aw_server_download_url }} - AW_SERVER_BIND_HOST={{ aw_server_bind_host }} - AW_SERVER_PORT={{ aw_server_port }} - AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }} - AW_SERVER_DATA_DIR={{ aw_server_data_dir }} - AW_SERVER_LOG_DIR={{ aw_server_log_dir }} - AW_SERVER_USER={{ aw_server_user }} - AW_SERVER_GROUP={{ aw_server_group }} - AW_SERVER_URL=http://127.0.0.1:{{ aw_server_port }} - AW_WORKTIME_REPORT_BASE={{ aw_worktime_report_base }} - AW_WORKTIME_TZ={{ aw_worktime_timezone }} - no_log: true - -- name: Передать AW server env внутрь CT - ansible.builtin.command: - argv: - - pct - - push - - "{{ ct_id }}" - - "{{ proxmox_bootstrap_dir }}/aw-server.env" - - /etc/activitywatch/aw-server.env - no_log: true - -- name: Настроить права env файла внутри CT - ansible.builtin.command: - argv: - - pct - - exec - - "{{ ct_id }}" - - -- - - chmod - - "0600" - - /etc/activitywatch/aw-server.env - -- name: Установить сервер и применить RU patch внутри CT - ansible.builtin.command: - argv: - - pct - - exec - - "{{ ct_id }}" - - -- - - bash - - -lc - - | - set -euo pipefail - chmod +x /root/bootstrap/install_aw_server.sh /root/bootstrap/apply_webui_ru_patch.sh - bash /root/bootstrap/install_aw_server.sh - bash /root/bootstrap/apply_webui_ru_patch.sh - systemctl restart activitywatch-server.service - -- name: Проверить AW API изнутри CT - ansible.builtin.command: - argv: - - pct - - exec - - "{{ ct_id }}" - - -- - - bash - - -lc - - "curl -fsS http://127.0.0.1:{{ aw_server_port }}/api/0/info >/dev/null" - -- name: Проверить hooks RU patch в index.html - ansible.builtin.command: - argv: - - pct - - exec - - "{{ ct_id }}" - - -- - - bash - - -lc - - "grep -q 'ru-patch-v5.js' {{ aw_server_webui_dir }}/index.html && grep -q 'sw-cleanup.js' {{ aw_server_webui_dir }}/index.html" - -- name: Показать итоговый endpoint - ansible.builtin.debug: - msg: - - "CT {{ ct_id }} создан и настроен." - - "Endpoint ActivityWatch: http://{{ ct_ip | regex_replace('/[0-9]+$', '') }}:{{ aw_server_port }}" diff --git a/install-kit-awindows-20260427-211240/aw-server/activitywatch-server.service b/install-kit-awindows-20260427-211240/aw-server/activitywatch-server.service deleted file mode 100755 index 89d31e3..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/activitywatch-server.service +++ /dev/null @@ -1,24 +0,0 @@ -[Unit] -Description=ActivityWatch Server (Rust) -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -EnvironmentFile=/etc/activitywatch/aw-server.env -User=__AW_SERVER_USER__ -Group=__AW_SERVER_GROUP__ -WorkingDirectory=__AW_SERVER_DATA_DIR__ -ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT" --dbpath "$AW_SERVER_DB_PATH" --webpath "$AW_SERVER_WEBUI_DIR"' -Restart=on-failure -RestartSec=5s -StateDirectory=activitywatch -LogsDirectory=activitywatch -NoNewPrivileges=true -PrivateTmp=true -ProtectSystem=full -ProtectHome=read-only -LimitNOFILE=65535 - -[Install] -WantedBy=multi-user.target diff --git a/install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh b/install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh deleted file mode 100755 index 64e8826..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh +++ /dev/null @@ -1,222 +0,0 @@ -#!/bin/bash -set -euo pipefail - -ENV_FILE="/etc/activitywatch/aw-server.env" -if [[ ! -f "$ENV_FILE" ]]; then - echo "missing env file: $ENV_FILE" >&2 - exit 1 -fi - -source "$ENV_FILE" - -WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-${AW_WEBUI_DIR:-/opt/activitywatch/webui-ru}}" -SERVER_PUBLIC_HOST="${AW_SERVER_PUBLIC_HOST:-${AW_SERVER_HOST:-$(hostname -f 2>/dev/null || hostname)}}" -REPORT_BASE="${AW_WORKTIME_REPORT_BASE:-http://${SERVER_PUBLIC_HOST}:5610}" -CASE_PORT="${AW_DLP_CASE_PORT:-5602}" -CASE_BASE="${AW_DLP_CASE_PUBLIC_BASE:-}" -PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js" -SW_CLEANUP_SRC="/root/bootstrap/aw-sw-cleanup.js" -WORKTIME_PANEL_SRC="/root/bootstrap/aw-worktime-panel.js" -HOST_GROUPS_SRC="/root/bootstrap/aw-host-groups.json" -INDEX_HTML="$WEBUI_DIR/index.html" -SERVICE_WORKER="$WEBUI_DIR/service-worker.js" -TS=$(date +%Y%m%d%H%M%S) -PATCH_TARGET="$WEBUI_DIR/js/ru-patch-v5.js" -SW_TARGET="$WEBUI_DIR/js/sw-cleanup.js" -WORKTIME_PANEL_TARGET="$WEBUI_DIR/js/aw-worktime-panel.js" -HOST_GROUPS_TARGET="$WEBUI_DIR/js/aw-host-groups.json" -TRENDS_NEEDLE='this.activityStore.query_category_time_by_period(r)' -TRENDS_REPLACEMENT='this.activityStore.ensure_loaded(r)' -TIMESPIRAL_NEEDLE='start:new Date("2022-08-08")' -TIMESPIRAL_REPLACEMENT='start:new Date(Date.now()-12*36e5)' -CATEGORY_HELPER_NEEDLE='hostname:t.hostnameChoices[0]' -CATEGORY_HELPER_REPLACEMENT='hostname:t.hostnameChoices.filter((function(t){return"unknown"!==t&&"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; } -[[ -f "$WORKTIME_PANEL_SRC" ]] || { echo "missing $WORKTIME_PANEL_SRC" >&2; exit 1; } -[[ -f "$HOST_GROUPS_SRC" ]] || { echo "missing $HOST_GROUPS_SRC" >&2; exit 1; } -[[ -f "$INDEX_HTML" ]] || { echo "missing $INDEX_HTML" >&2; exit 1; } - -if [[ ! -s "$INDEX_HTML" ]]; then - latest_nonempty_backup="$(find "$WEBUI_DIR" -maxdepth 1 -type f -name 'index.html.bak.*' -size +0c | sort | tail -n 1 || true)" - if [[ -n "$latest_nonempty_backup" ]]; then - cp "$latest_nonempty_backup" "$INDEX_HTML" - echo "restored empty index.html from backup: $latest_nonempty_backup" - else - echo "index.html is empty and no non-empty backup exists: $INDEX_HTML" >&2 - exit 1 - fi -fi - -install -d "$WEBUI_DIR/js" -install -m 0644 "$PATCH_JS_SRC" "$PATCH_TARGET" -install -m 0644 "$SW_CLEANUP_SRC" "$SW_TARGET" -install -m 0644 "$WORKTIME_PANEL_SRC" "$WORKTIME_PANEL_TARGET" -install -m 0644 "$HOST_GROUPS_SRC" "$HOST_GROUPS_TARGET" -cp "$INDEX_HTML" "$INDEX_HTML.bak.$TS" - -patch_hash="$(sha1sum "$PATCH_TARGET" | awk '{print substr($1,1,12)}')" -sw_hash="$(sha1sum "$SW_TARGET" | awk '{print substr($1,1,12)}')" -worktime_panel_hash="$(sha1sum "$WORKTIME_PANEL_TARGET" | awk '{print substr($1,1,12)}')" - -if [[ -z "$CASE_BASE" ]]; then - CASE_BASE="$(python3 - "$REPORT_BASE" "$CASE_PORT" <<'PY' -from urllib.parse import urlsplit, urlunsplit -import os -import socket -import sys - -report_base = sys.argv[1] -case_port = sys.argv[2] -parts = urlsplit(report_base) -hostname = ( - parts.hostname - or os.environ.get("AW_SERVER_PUBLIC_HOST") - or os.environ.get("AW_SERVER_HOST") - or socket.getfqdn() -) -scheme = parts.scheme or "http" -print(urlunsplit((scheme, f"{hostname}:{case_port}", "", "", ""))) -PY -)" -fi - -python3 - "$WORKTIME_PANEL_TARGET" "$REPORT_BASE" <<'PY' -from pathlib import Path -import sys - -path = Path(sys.argv[1]) -report_base = sys.argv[2] -text = path.read_text() -text = text.replace("__AW_WORKTIME_REPORT_BASE__", report_base) -path.write_text(text) -PY - -python3 - "$INDEX_HTML" "$sw_hash" "$patch_hash" "$worktime_panel_hash" "$REPORT_BASE" "$CASE_BASE" <<'PY' -from pathlib import Path -import re -import sys -from urllib.parse import urlsplit - -path = Path(sys.argv[1]) -sw_hash = sys.argv[2] -patch_hash = sys.argv[3] -panel_hash = sys.argv[4] -report_base = sys.argv[5] -case_base = sys.argv[6] -content = path.read_text() - -content = re.sub( - r']+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js|aw-worktime-panel\.js)[^>]*>', - '', - content, -) -content = re.sub(r";\s*frame-src 'self' [^\";>]*", "", content) -content = re.sub(r";\s*connect-src 'self' [^\";>]*", "", content) -report_origin = urlsplit(report_base) -case_origin = urlsplit(case_base) -connect_targets = " ".join( - [ - f"{report_origin.scheme}://{report_origin.netloc}", - f"{case_origin.scheme}://{case_origin.netloc}", - ] -) -content = re.sub( - r"script-src 'self' 'unsafe-eval'(?:;\s*connect-src 'self' [^\";>]*)?(?:;\s*frame-src 'self' [^\";>]*)?", - f"script-src 'self' 'unsafe-eval'; connect-src 'self' {connect_targets}; frame-src 'self' {report_base}", - content, - count=1, -) -content = content.replace( - "", - ( - f'' - f'' - ), - 1, -) -content = content.replace( - "", - f'', - 1, -) -if 'id="aw-report-links"' not in content: - content = content.replace( - "", - '', - 1, - ) -path.write_text(content) -PY -cp "$SW_CLEANUP_SRC" "$SERVICE_WORKER" - -if [[ ! -s "$INDEX_HTML" ]]; then - echo "index.html became empty after RU patch: $INDEX_HTML" >&2 - exit 1 -fi - -trends_chunk="$(grep -Rsl "$TRENDS_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)" -if [[ -n "$trends_chunk" ]]; then - cp "$trends_chunk" "$trends_chunk.bak.$TS" - python3 - "$trends_chunk" "$TRENDS_NEEDLE" "$TRENDS_REPLACEMENT" <<'PY' -from pathlib import Path -import sys - -path = Path(sys.argv[1]) -old = sys.argv[2] -new = sys.argv[3] -content = path.read_text() -if old in content: - path.write_text(content.replace(old, new, 1)) - print(f"Trends hotfix applied to {path}") -else: - print(f"Trends hotfix already present in {path}") -PY -else - echo "Trends hotfix skipped: chunk not found" -fi - -timespiral_chunk="$(grep -Rsl "$TIMESPIRAL_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)" -if [[ -n "$timespiral_chunk" ]]; then - cp "$timespiral_chunk" "$timespiral_chunk.bak.$TS" - python3 - "$timespiral_chunk" "$TIMESPIRAL_NEEDLE" "$TIMESPIRAL_REPLACEMENT" <<'PY' -from pathlib import Path -import sys - -path = Path(sys.argv[1]) -old = sys.argv[2] -new = sys.argv[3] -content = path.read_text() -if old in content: - path.write_text(content.replace(old, new, 1)) - print(f"Timespiral hotfix applied to {path}") -else: - print(f"Timespiral hotfix already present in {path}") -PY -else - echo "Timespiral hotfix skipped: chunk not found" -fi - -category_helper_chunk="$(grep -Rsl "$CATEGORY_HELPER_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)" -if [[ -n "$category_helper_chunk" ]]; then - cp "$category_helper_chunk" "$category_helper_chunk.bak.$TS" - python3 - "$category_helper_chunk" "$CATEGORY_HELPER_NEEDLE" "$CATEGORY_HELPER_REPLACEMENT" <<'PY' -from pathlib import Path -import sys - -path = Path(sys.argv[1]) -old = sys.argv[2] -new = sys.argv[3] -content = path.read_text() -if old in content: - path.write_text(content.replace(old, new, 1)) - print(f"Category helper host hotfix applied to {path}") -else: - print(f"Category helper host hotfix already present in {path}") -PY -else - echo "Category helper host hotfix skipped: chunk not found" -fi - -echo "RU patch applied to $WEBUI_DIR (ru-patch-v5.js?v=$patch_hash)" diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-browser-smoke.service b/install-kit-awindows-20260427-211240/aw-server/aw-browser-smoke.service deleted file mode 100644 index 41b74e9..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-browser-smoke.service +++ /dev/null @@ -1,28 +0,0 @@ -[Unit] -Description=AW-RUS browser-visible WebUI smoke check -After=network-online.target activitywatch-server.service aw-worktime-api.service -Wants=network-online.target activitywatch-server.service aw-worktime-api.service - -[Service] -Type=oneshot -EnvironmentFile=/etc/activitywatch/aw-server.env -Environment=NODE_PATH=/usr/share/nodejs -Environment=AW_BROWSER_SMOKE_ENGINE=chromium-cli -Environment=HOME=/var/lib/activitywatch/browser-smoke -Environment=XDG_CONFIG_HOME=/var/lib/activitywatch/browser-smoke/.config -Environment=XDG_CACHE_HOME=/var/lib/activitywatch/browser-smoke/.cache -WorkingDirectory=/var/lib/activitywatch/browser-smoke -ExecStart=/usr/bin/node /usr/local/lib/aw-rus-browser-smoke/aw-webui-browser-smoke.mjs -TimeoutStartSec=180 -User=activitywatch -Group=activitywatch -Nice=15 -CPUQuota=25% -NoNewPrivileges=true -PrivateTmp=true -ProtectSystem=strict -ProtectHome=true -ReadWritePaths=/var/lib/activitywatch/browser-smoke -StandardOutput=journal -StandardError=journal -SyslogIdentifier=aw-browser-smoke diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-browser-smoke.timer b/install-kit-awindows-20260427-211240/aw-server/aw-browser-smoke.timer deleted file mode 100644 index d956c91..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-browser-smoke.timer +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Run AW-RUS browser-visible WebUI smoke check hourly - -[Timer] -OnCalendar=hourly -AccuracySec=5min -RandomizedDelaySec=10min -Persistent=false -Unit=aw-browser-smoke.service - -[Install] -WantedBy=timers.target diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json b/install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json deleted file mode 100644 index b7e5fff..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "groups": [ - { - "id": "pve-detmir", - "name": "pve-detmir", - "description": "Выделенный клиент DetMir в разделе Активность.", - "patterns": [ - "^pve-detmir$" - ], - "links": [ - { "label": "Активность", "type": "activity", "view": "pve_audit" }, - { "label": "Web-admin аудит", "type": "bucket", "bucket_prefix": "aw-pve-webadmin-events_" }, - { "label": "PVE tasks", "type": "bucket", "bucket_prefix": "aw-pve-task-events_" }, - { "label": "SSH сессии", "type": "bucket", "bucket_prefix": "aw-ssh-sessions_" }, - { "label": "Команды shell", "type": "bucket", "bucket_prefix": "aw-console-commands_" }, - { "label": "Web категории", "type": "bucket", "bucket_prefix": "aw-detmir-web-category_" }, - { "label": "Все бакеты", "type": "buckets" } - ] - }, - { - "id": "windows-rdp", - "name": "Windows RDP", - "description": "Пользовательские Windows/RDP хосты с активностью, DLP и рабочим временем.", - "patterns": [ - "^(SHARKON|WIN|RDP|TERM|TS-|WS-)" - ], - "links": [ - { "label": "Активность", "type": "activity" }, - { "label": "DLP", "type": "bucket", "bucket_prefix": "aw-dlp-endpoint-signals_" } - ] - }, - { - "id": "linux-remote", - "name": "Linux remote workers", - "description": "Linux-хосты удалённых сотрудников: GUI активность, SSH/console и browser admin UI.", - "patterns": [ - "^(LINUX-WS|LINUX-DESKTOP|LX-|DESKTOP-|ADMIN-|WORKSTATION-|DEVBOX-)" - ], - "links": [ - { "label": "Активность", "type": "activity" }, - { "label": "SSH сессии", "type": "bucket", "bucket_prefix": "aw-ssh-sessions_" }, - { "label": "Команды shell", "type": "bucket", "bucket_prefix": "aw-console-commands_" }, - { "label": "Web категории", "type": "bucket", "bucket_prefix": "aw-detmir-web-category_" }, - { "label": "Все бакеты", "type": "buckets" } - ] - }, - { - "id": "virtual-infra", - "name": "Virtual servers + Proxmox", - "description": "Инфраструктурные VM и серверы Proxmox, Debian и Ubuntu.", - "patterns": [ - "^(PVE|PROXMOX|DEBIAN|UBUNTU|LINUX|VM-|SRV-|INFRA-)" - ], - "links": [ - { "label": "Все бакеты", "type": "buckets" } - ] - } - ], - "ungrouped_name": "Прочие хосты" -} diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js b/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js deleted file mode 100755 index 6b546fd..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js +++ /dev/null @@ -1,2220 +0,0 @@ -(function () { - if (window.__awRuPatchBootstrapped) { - return; - } - window.__awRuPatchBootstrapped = true; - window.__awRuPatchVersion = "template-v14-dlp-route-lite"; - document.documentElement.setAttribute("data-aw-ru-patch", "template-v14-dlp-route-lite"); - - const exact = new Map([ - ["ActivityWatch", "АктивВотч"], - ["Home", "Главная"], - ["Activity", "Активность"], - ["Developer settings", "Настройки разработчика"], - ["Timeline", "Таймлайн"], - ["Trends", "Тренды"], - ["Report", "Отчеты"], - ["Settings", "Настройки"], - ["Search", "Поиск"], - ["Buckets", "Бакеты"], - ["Stopwatch", "Секундомер"], - ["Timespiral", "Временная спираль"], - ["Categorization helper", "Помощник категоризации"], - ["Categorization", "Категоризация"], - ["Tools", "Инструменты"], - ["Raw Data", "Сырые данные"], - ["Summary", "Сводка"], - ["Worktime", "Рабочее время"], - ["All", "Все"], - ["None", "Нет"], - ["Date", "Дата"], - ["Time", "Время"], - ["to", "до"], - ["Start of day", "Начало дня"], - ["Start of week", "Начало недели"], - ["Duration default value", "Значение длительности по умолчанию"], - ["Landing page", "Стартовая страница"], - ["Theme", "Тема"], - ["New release notification", "Уведомление о новом релизе"], - ["Use fallback colors", "Использовать резервные цвета"], - ["Always count as active pattern", "Шаблон всегда считать активным"], - ["Hostname", "Имя хоста"], - ["Hostname:", "Имя хоста:"], - ["Range", "Диапазон"], - ["Range:", "Диапазон:"], - ["Options", "Параметры"], - ["Toggles", "Переключатели"], - ["Enabled", "Включено"], - ["Host", "Хост"], - ["Version", "Версия"], - ["day", "день"], - ["week", "неделя"], - ["month", "месяц"], - ["year", "год"], - ["Monday", "Понедельник"], - ["Saturday", "Суббота"], - ["Sunday", "Воскресенье"], - ["Start", "Начало"], - ["Stop", "Конец"], - ["End", "Конец"], - ["Refresh", "Обновить"], - ["Apply", "Применить"], - ["Filters", "Фильтры"], - ["Show options", "Показать параметры"], - ["Show last", "Показать за"], - ["Show from", "Показать с"], - ["Last update", "Последнее обновление"], - ["Events shown", "Показано событий"], - ["Window", "Окно"], - ["New view", "Новый вид"], - ["Edit view", "Изменить вид"], - ["Show percent", "Показать проценты"], - ["Top Window Titles", "Топ заголовков окон"], - ["Download", "Скачать"], - ["Upload", "Загрузить"], - ["Show", "Показать"], - ["Hide", "Скрыть"], - ["Close", "Закрыть"], - ["Week", "Неделя"], - ["Month", "Месяц"], - ["Year", "Год"], - ["Today", "Сегодня"], - ["Yesterday", "Вчера"], - ["No data", "Нет данных"], - ["Category", "Категория"], - ["Categories", "Категории"], - ["Duration", "Длительность"], - ["Applications", "Приложения"], - ["Hosts", "Хосты"], - ["Date Range", "Диапазон дат"], - ["Generate", "Сформировать"], - ["Loading", "Загрузка"], - ["Loading...", "Загрузка..."], - ["Dark", "Темная"], - ["Light", "Светлая"], - ["Save", "Сохранить"], - ["Cancel", "Отмена"], - ["Delete", "Удалить"], - ["API Browser", "API-браузер"], - ["Documentation", "Документация"], - ["Restore defaults", "Восстановить значения по умолчанию"], - ["Import", "Импорт"], - ["Export", "Экспорт"], - ["Category Builder", "Конструктор категорий"], - ["Query Explorer", "Конструктор запросов"], - ["Event List", "Список событий"], - ["Raw JSON", "Сырой JSON"], - ["Expand list", "Развернуть список"], - ["Alerts", "Оповещения"], - ["Graph", "Граф"], - ["Developer zone", "Зона разработчика"], - ["History", "История"], - ["Running", "Запущено"], - ["Edit", "Изменить"], - ["No stopwatch running", "Нет активного секундомера"], - ["No label", "Без метки"], - ["Start new", "Новый запуск"], - ["Check", "Проверить"], - ["Name", "Имя"], - ["New alert", "Новое правило"], - ["Custom regex", "Свое регулярное выражение"], - ["Use existing categories", "Использовать существующие категории"], - ["Choose a tag...", "Выберите тег..."], - ["Rule", "Правило"], - ["Open", "Открыть"], - ["More", "Еще"], - ["Bucket ID", "ID бакета"], - ["Updated", "Обновлено"], - ["First seen", "Впервые замечен"], - ["Last updated", "Последнее обновление"], - ["Toggle navigation", "Переключить навигацию"], - ["unknown", "неизвестно"], - ["Uncategorized", "Без категории"] - ]); - - const partial = [ - ["Hello early user,", "Здравствуйте, ранний пользователь,"], - ["early days for ActivityWatch.", "ранний этап для ActivityWatch."], - ["still early days for ActivityWatch.", "все еще ранний этап для ActivityWatch."], - ["It's still early days for ActivityWatch.", "ActivityWatch еще находится на раннем этапе."], - ["We've come a long way but we need users (like you!) to provide feedback and help us turn ActivityWatch into a successful project.", "Мы уже прошли большой путь, но нам нужны пользователи вроде вас, чтобы давать обратную связь и помогать развивать проект."], - ["Early users like you mean a lot to us, and we hope you'll reach out to us with any ideas you have for improvements!", "Такие ранние пользователи очень важны, и мы рассчитываем на ваши идеи по улучшению системы."], - ["If you have a minute, we'd really appreciate you taking our short user survey!", "Если у вас есть минута, пожалуйста, пройдите наш короткий опрос пользователей."], - ["If you have a minute to spare, please take the time to fill out our user survey, vote on features in the forum, or just share ActivityWatch with your friends and colleagues.", "Если у вас есть немного времени, пожалуйста, заполните наш пользовательский опрос, проголосуйте за функции на форуме или просто расскажите об ActivityWatch друзьям и коллегам."], - ["Spread the word", "Расскажите другим"], - ["It's still early days for ActivityWatch. We've come a long way but we need users (like you!) to provide feedback and help us turn ActivityWatch into a successful project. Early users like you mean a lot to us, and we hope you'll reach out to us with any ideas you have for improvements!", "ActivityWatch еще находится на раннем этапе. Мы уже прошли большой путь, но нам нужны пользователи вроде вас, чтобы давать обратную связь и помогать развивать проект. Такие ранние пользователи очень важны, и мы рассчитываем на ваши идеи по улучшению системы."], - ["If you are a developer, we hope you can contribute by writing a watcher, visualization, or something else, and share it with us on the forum!", "Если вы разработчик, вы можете помочь проекту: написать watcher, визуализацию или что-то еще и поделиться этим на форуме."], - ["Thank you for using ActivityWatch!", "Спасибо за использование ActivityWatch!"], - ["If you are not interested in this message, then just ignore it. We won't show it very often.", "Если это сообщение вам не нужно, просто проигнорируйте его. Мы показываем его нечасто."], - ["Trends for ", "Тренды за "], - ["Activity for ", "Активность за "], - ["Активность for ", "Активность за "], - ["7 days", "7 дней"], - ["30 days", "30 дней"], - ["Time active:", "Активное время:"], - ["This feature is still in early development.", "Эта функция пока находится на ранней стадии разработки."], - ["This is a work-in-progress experiment.", "Это экспериментальная функция, она ещё не доведена до готового состояния."], - ["Bucket: ", "Бакет: "], - ["Events: ", "События: "], - ["This tool will help you create categories from your uncategorized time.", "Этот инструмент поможет создавать категории из некатегоризированного времени."], - ["Note: These settings are meant for developers who (hopefully) know what they are doing, and as such, may break things unexpectedly.", "Примечание: эти настройки предназначены для разработчиков, которые понимают, что делают, и поэтому могут неожиданно что-нибудь сломать."], - ["It works by fetching all uncategorized time for a recent timeperiod, and then finds the most common words (by time, not count) each of which may then either be ignored (if too broad/irrelevant), or used to create a new (sub)category, or to append the word to a pre-existing category rule. Words with less than 60s of time will not be shown.", "Инструмент получает некатегоризированное время за недавний период и ищет самые частые слова по длительности, а не по количеству. Их можно игнорировать, если они слишком общие, использовать для создания новой подкатегории или добавить в уже существующее правило. Слова с длительностью меньше 60 секунд не показываются."], - ["When you're done, you can inspect the categories in the Settings page.", "После завершения вы сможете проверить категории на странице Настройки."], - ['The time at which days "start", since humans don\'t always go to bed before midnight. Set to 04:00 by default.', 'Время, с которого начинается новый день, так как люди не всегда ложатся спать до полуночи. По умолчанию установлено 04:00.'], - ["The weekday which starts a new week.", "День недели, с которого начинается новая неделя."], - ["The default duration used for 'show last' in the timeline view.", "Длительность по умолчанию для режима 'показать последние' в таймлайне."], - ["The page to open when opening ActivityWatch, or clicking the logo in the top menu.", "Страница, которая открывается при запуске ActivityWatch или при нажатии на логотип в верхнем меню."], - ["Change color theme of the application (you need to change categories colors manually to be suitable with dark mode).", "Изменение цветовой темы приложения. Цвета категорий для темного режима нужно настраивать вручную."], - ["Devmode enables some features that are still work-in-progress.", "Devmode включает некоторые функции, которые всё ещё находятся в стадии разработки."], - ["Querying an entire year is a very heavy operation, and is likely to lead to timeouts. However, the query might be fast enough if you're running aw-server-rust.", "Запрос за целый год является очень тяжёлой операцией и может приводить к таймаутам. Но если у вас работает aw-server-rust, такой запрос может выполняться достаточно быстро."], - ["Multidevice query is where events are collected from several hosts in the Activity view. It is an early experiment, that currently does not support browser buckets (or the audible-as-active feature).", "Multidevice query собирает события с нескольких хостов в представлении Активность. Это ранний эксперимент, который пока не поддерживает бакеты браузера и функцию активной вкладки со звуком."], - ["The maximum amount of time a server request can take before timing out. Setting this to a high value can be useful for large queries. Note that you need to reload the web UI for it to apply.", "Максимальное время выполнения серверного запроса до срабатывания таймаута. Увеличенное значение может быть полезно для больших запросов. Чтобы изменение вступило в силу, нужно перезагрузить Web UI."], - ["We will send you a notification if there is a new release available for download, this check will happen at most once per day.", "При появлении нового релиза для скачивания будет показано уведомление. Проверка выполняется не чаще одного раза в день."], - ["Uses the old coloring style for some visualizations when uncategorized or no category color.", "Использует старую схему раскраски для некоторых визуализаций, когда категория не задана или у нее нет цвета."], - ["Apps or titles matching this regular expression will never be counted as AFK.", "Приложения или заголовки, подходящие под это регулярное выражение, никогда не будут считаться AFK."], - ["Can be used to count time as active, despite no input (like meetings, or games with controllers). An empty string disables it.", "Позволяет считать время активным даже без ввода, например на встречах или в играх с контроллером. Пустая строка отключает функцию."], - ["Example expression:", "Пример выражения:"], - ["Rules for categorizing events. An event can only have one category. If several categories match, the deepest one will be chosen.", "Правила категоризации событий. Событие может иметь только одну категорию. Если подходят несколько, будет выбрана самая глубокая."], - ["You can use the Category Builder to quickly create categories from uncategorized activity.", "Через Конструктор категорий можно быстро создавать категории из некатегоризированной активности."], - ["You can also find and share categorization rule presets on the forum.", "Готовые наборы правил категоризации можно находить и публиковать на форуме."], - ["For help on how to write categorization rules, see the documentation.", "Как писать правила категоризации, описано в документации."], - ["Generate a report of time spent on a certain category of device activity.", "Сформировать отчет по времени в выбранной категории активности устройства."], - ["See the documentation for help on how to write queries.", "Как писать запросы, смотрите в документации."], - ["See the documentation for help", "См. документацию для справки"], - ["See the documentation", "См. документацию"], - ["on how to write queries.", "по написанию запросов."], - ["Number of events:", "Количество событий:"], - ["Query", "Запрос"], - ["EventsShowing", "Показано событий"], - ["Drag to pan and scroll to zoom", "Перетаскивайте для прокрутки и используйте колесо мыши для масштабирования"], - ["Made with", "Сделано с"], - ["by the", "командой"], - ["Сделано скомандой", "Сделано командой"], - ["ActivityWatch developers", "разработчиков ActivityWatch"], - ["Report a bug", "Сообщить об ошибке"], - ["Ask for help", "Получить помощь"], - ["Vote on features", "Голосовать за функции"], - ["Donate", "Поддержать"], - ["TwitterGitHub", "Twitter GitHub"], - ["Using bucket:", "Используется бакет:"], - ["This is an early experiment. Data entered here is not shown in the Activity view, yet.", "Это ранний эксперимент. Данные, введенные здесь, пока не отображаются в представлении Активность."], - ["Started ", "Запущен "], - ["hours ago", "часов назад"], - ["days ago", "дней назад"], - ["0s ago", "0 с назад"], - ["minutes", "минут"], - ["Generate a report", "Сформировать отчет"], - ["Goal name:", "Имя цели:"], - ["Category:", "Категория:"], - ["Current:", "Текущее:"], - ["Toggle autorefresh every ", "Автообновление каждые "], - ["No events match selected criteria. Timeline is not updated.", "Нет событий, соответствующих выбранным критериям. Таймлайн не обновлен."], - ["Last update:", "Последнее обновление:"], - ["See PR aw-webui#365 for more information.", "Подробности см. в PR aw-webui#365."], - ["See PR aw-webui#365", "См. PR aw-webui#365"], - ["for more information.", "для дополнительной информации."], - ["Displays a graph of categories and their transitions.", "Показывает граф категорий и переходов между ними."], - ["Max category depth", "Максимальная глубина категории"], - ["Exclude uncategorized", "Исключать некатегоризированное"], - ["Just some tools to aid in development and debugging.", "Набор инструментов для разработки и отладки."], - ["Nothing to see here right now...", "Сейчас здесь ничего полезного нет..."], - ["Are you looking to collect more data? Check out the docs for more watchers.", "Нужно собирать больше данных? Посмотрите документацию по дополнительным watcher-модулям."], - ["Are you looking to collect more data?", "Нужно собирать больше данных?"], - ["Check out the docs for more watchers.", "Посмотрите документацию по дополнительным watcher-модулям."], - ["Check out the docs for more watchers", "Посмотрите документацию по дополнительным watcher-модулям"], - ["Click to sort ascending", "Нажмите для сортировки по возрастанию"], - ["Host:", "Хост:"], - ["Hostname:", "Имя хоста:"], - ["Range:", "Диапазон:"], - ["serverVersion", "Версия"], - ["Version:", "Версия:"], - ["Last updated:", "Последнее обновление:"], - ["First seen:", "Впервые замечен:"], - ["When you're done, you can inspect the categories", "После завершения вы сможете проверить категории"], - ["in the Settings page.", "на странице Настройки."], - ["Activity (", "Активность ("], - ["Exclude time away from computer", "Исключать время отсутствия за компьютером"], - ['Common words in "Uncategorized" events', 'Частые слова в событиях "Без категории"'], - ["No words with significant duration. You're good to go!", "Нет слов со значимой длительностью. Здесь всё в порядке."], - ["Top apps", "Топ приложений"], - ["Top Applications", "Топ приложений"], - ["Top titles", "Топ заголовков"], - ["Top URLs", "Топ URL"], - ["Top domains", "Топ доменов"], - ["Top Browser Domains", "Топ доменов браузера"], - ["Top Browser URLs", "Топ URL браузера"], - ["Top Browser Titles", "Топ заголовков браузера"], - ["Top Categories", "Топ категорий"], - ["Category Tree", "Дерево категорий"], - ["Timeline (barchart)", "Таймлайн (гистограмма)"], - ["Calculate Work Time", "Рассчитать рабочее время"], - ["Export CSV", "Экспорт CSV"], - ["Export JSON", "Экспорт JSON"], - ["No duplicate events found.", "Дубликаты событий не найдены."], - ["No overlapping events found.", "Пересекающиеся события не найдены."], - ["No zero-duration events found.", "События нулевой длительности не найдены."] - ]; - - const hiddenNavLabels = new Set([ - "Raw Data", - "Сырые данные" - ]); - - const hiddenNavHrefPatterns = [ - /\/raw-data\b/i, - /\/raw\b/i - ]; - - const dlpVerdictOptions = [ - { value: "false_positive", label: "Ложное срабатывание" }, - { value: "allowed", label: "Разрешено" }, - { value: "review_needed", label: "На проверку" }, - { value: "incident", label: "Инцидент" } - ]; - - function replaceText(text) { - if (!text) return text; - if (exact.has(text.trim())) { - return text.replace(text.trim(), exact.get(text.trim())); - } - let result = text; - for (const [en, ru] of partial) { - result = result.split(en).join(ru); - } - return result; - } - - function walk(root) { - const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null); - const nodes = []; - while (walker.nextNode()) nodes.push(walker.currentNode); - for (const node of nodes) { - const nextValue = replaceText(node.nodeValue); - if (nextValue !== node.nodeValue) { - node.nodeValue = nextValue; - } - } - } - - function translateAttributes(root) { - const elements = root.querySelectorAll("[title],[placeholder],[aria-label]"); - for (const element of elements) { - ["title", "placeholder", "aria-label"].forEach(function (attr) { - const value = element.getAttribute(attr); - if (value) { - element.setAttribute(attr, replaceText(value)); - } - }); - } - } - - function injectStyles() { - if (document.getElementById("aw-ru-hide-noise-style")) return; - const style = document.createElement("style"); - style.id = "aw-ru-hide-noise-style"; - style.textContent = [ - '[href*="/raw-data"], [href*="/raw"], a[data-testid*="raw"], button[data-testid*="raw"] { display: none !important; }', - '[aria-label="Raw Data"], [aria-label="Сырые данные"] { display: none !important; }', - '.aw-ru-dlp-center { margin: 16px 0; padding: 16px; border: 1px solid rgba(120,120,120,.35); border-radius: 8px; background: rgba(20,20,20,.03); }', - '.aw-ru-dlp-toolbar { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 12px; }', - '.aw-ru-dlp-toolbar input, .aw-ru-dlp-toolbar select, .aw-ru-dlp-toolbar textarea { min-height: 32px; }', - '.aw-ru-dlp-toolbar button, .aw-ru-dlp-row button { min-height: 32px; padding: 4px 10px; }', - '.aw-ru-dlp-table { width: 100%; border-collapse: collapse; font-size: 13px; }', - '.aw-ru-dlp-table th, .aw-ru-dlp-table td { border: 1px solid rgba(120,120,120,.25); padding: 6px; vertical-align: top; }', - '.aw-ru-dlp-table td input, .aw-ru-dlp-table td select { width: 100%; box-sizing: border-box; }', - '.aw-ru-dlp-muted { opacity: .55; }', - '.aw-ru-dlp-pill { display: inline-block; padding: 2px 8px; border-radius: 999px; background: rgba(90,140,255,.15); font-size: 12px; }', - '.aw-ru-dlp-status { margin-left: auto; font-size: 12px; opacity: .8; }', - '.aw-ru-dlp-message { margin-top: 8px; font-size: 12px; }', - '.aw-ru-dlp-actions { display: flex; gap: 6px; flex-wrap: wrap; }', - '.aw-ru-dlp-section { margin-top: 18px; }', - '.aw-ru-dlp-section h5 { margin: 0 0 8px; }', - '.aw-ru-host-groups { margin: 16px 0; padding: 16px; border: 1px solid rgba(120,120,120,.35); border-radius: 8px; background: rgba(20,20,20,.03); }', - '.aw-ru-host-groups-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; }', - '.aw-ru-host-group-card { border: 1px solid rgba(120,120,120,.25); border-radius: 8px; padding: 12px; background: rgba(255,255,255,.02); }', - '.aw-ru-host-group-card h4 { margin: 0 0 8px; }', - '.aw-ru-host-group-card p { margin: 0 0 12px; font-size: 13px; opacity: .85; }', - '.aw-ru-host-list { display: flex; flex-direction: column; gap: 8px; }', - '.aw-ru-host-item { border: 1px solid rgba(120,120,120,.2); border-radius: 6px; padding: 8px; }', - '.aw-ru-host-item-title { font-weight: 600; margin-bottom: 6px; }', - '.aw-ru-host-links { display: flex; flex-wrap: wrap; gap: 6px; }', - '.aw-ru-host-links a { display: inline-block; padding: 4px 8px; border-radius: 999px; background: rgba(90,140,255,.15); text-decoration: none; }', - '.aw-ru-pve-audit { margin: 16px 0; padding: 16px; border: 1px solid rgba(120,120,120,.35); border-radius: 8px; background: rgba(10,20,40,.04); }', - '.aw-ru-pve-audit-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin: 12px 0 16px; }', - '.aw-ru-pve-audit-card { border: 1px solid rgba(120,120,120,.22); border-radius: 8px; padding: 12px; background: rgba(255,255,255,.02); }', - '.aw-ru-pve-audit-card h5 { margin: 0 0 6px; font-size: 13px; opacity: .8; }', - '.aw-ru-pve-audit-value { font-size: 24px; font-weight: 700; }', - '.aw-ru-pve-audit-table { width: 100%; border-collapse: collapse; margin-top: 8px; }', - '.aw-ru-pve-audit-table th, .aw-ru-pve-audit-table td { padding: 6px 8px; border-bottom: 1px solid rgba(120,120,120,.18); vertical-align: top; text-align: left; font-size: 13px; }', - '.aw-ru-pve-audit-muted { opacity: .72; font-size: 13px; }', - '.aw-ru-rdp-center { margin: 16px 0; padding: 16px; border: 1px solid rgba(120,120,120,.35); border-radius: 8px; background: rgba(10,20,40,.04); }', - '.aw-ru-rdp-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin: 12px 0 16px; }', - '.aw-ru-rdp-card { border: 1px solid rgba(120,120,120,.22); border-radius: 8px; padding: 12px; background: rgba(255,255,255,.02); }', - '.aw-ru-rdp-card h5 { margin: 0 0 6px; font-size: 13px; opacity: .8; }', - '.aw-ru-rdp-value { font-size: 24px; font-weight: 700; }', - '.aw-ru-rdp-table { width: 100%; border-collapse: collapse; margin-top: 8px; }', - '.aw-ru-rdp-table th, .aw-ru-rdp-table td { padding: 6px 8px; border-bottom: 1px solid rgba(120,120,120,.18); vertical-align: top; text-align: left; font-size: 13px; }', - '.aw-ru-rdp-links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }', - '.aw-ru-rdp-links a { display: inline-block; padding: 4px 8px; border-radius: 999px; background: rgba(90,140,255,.15); text-decoration: none; }' - ].join("\n"); - document.head.appendChild(style); - } - - function hideNoiseNavigation(root) { - const links = root.querySelectorAll("a, button, [role='button']"); - for (const element of links) { - const text = (element.textContent || "").trim(); - const href = (element.getAttribute("href") || "").trim(); - if (hiddenNavLabels.has(text) || hiddenNavHrefPatterns.some((pattern) => pattern.test(href))) { - const container = element.closest("li, nav, div") || element; - container.style.display = "none"; - } - } - } - - function getCurrentHostFromHash() { - const hash = window.location.hash || ""; - const activityMatch = hash.match(/#\/activity\/([^/?#]+)/); - if (activityMatch && activityMatch[1]) return decodeURIComponent(activityMatch[1]); - const trendsMatch = hash.match(/#\/trends\/([^/?#]+)/); - if (trendsMatch && trendsMatch[1]) return decodeURIComponent(trendsMatch[1]); - return ""; - } - - function getCurrentActivityDayFromHash() { - const hash = window.location.hash || ""; - const match = hash.match(/#\/activity\/[^/]+\/day\/([^/?#]+)/i); - return match && match[1] ? decodeURIComponent(match[1]) : "today"; - } - - function isPveLikeHost(host) { - return /^pve[-_]/i.test(String(host || "")); - } - - function isLikelyClientHost(host) { - const value = String(host || "").trim(); - if (!value) return false; - if (/^(?:unknown|undefined|null)$/i.test(value)) return false; - if (/^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1)$/i.test(value)) return false; - if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(value)) return false; - if (value.indexOf(":") !== -1 && /^[0-9a-f:\[\]]+$/i.test(value)) return false; - return true; - } - - function isClientActivityRoute() { - const hash = window.location.hash || ""; - const match = hash.match(/^#\/activity\/([^/]+)(?:\/day\/([^/]+))?\/view\/([^/?#]+)/i); - if (!match) return false; - const host = decodeURIComponent(match[1] || ""); - return isLikelyClientHost(host) && !isPveLikeHost(host); - } - - function getRdpReportBaseUrl() { - const url = new URL(window.location.href); - url.hash = ""; - url.search = ""; - url.pathname = "/reports/worktime/today"; - url.port = "5610"; - return url; - } - - function buildRdpReportUrl(format, day) { - const url = getRdpReportBaseUrl(); - url.searchParams.set("day", day || "today"); - if (format) url.searchParams.set("format", format); - return url.toString(); - } - - function normalizeActivityDay(day) { - if (day && day !== "today") return day; - const now = new Date(); - return [ - now.getFullYear(), - String(now.getMonth() + 1).padStart(2, "0"), - String(now.getDate()).padStart(2, "0") - ].join("-"); - } - - function getActivityDayRange(day) { - const normalizedDay = normalizeActivityDay(day); - const start = new Date(normalizedDay + "T00:00:00"); - const end = new Date(normalizedDay + "T23:59:59"); - return { normalizedDay: normalizedDay, start: start, end: end }; - } - - function formatActiveHhmm(totalSeconds) { - const seconds = Math.max(0, Number(totalSeconds) || 0); - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - return String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0"); - } - - function isWorktimeRowActive(data) { - if (!data || typeof data !== "object") return false; - if (typeof data.active === "boolean") return data.active; - const state = String(data.state || "").trim().toLowerCase(); - return state === "active" || state === "активно"; - } - - function formatDurationSeconds(totalSeconds) { - const seconds = Math.max(0, Number(totalSeconds) || 0); - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = Math.floor(seconds % 60); - if (hours > 0) return hours + "ч " + String(minutes).padStart(2, "0") + "м"; - if (minutes > 0) return minutes + "м " + String(secs).padStart(2, "0") + "с"; - return secs + "с"; - } - - function formatIsoForUi(value) { - if (!value) return "—"; - try { - return new Date(value).toLocaleString(); - } catch (error) { - return value; - } - } - - async function fetchRdpWorktimeReport(host, day) { - if (!host) return null; - const cacheKey = host + "|" + (day || "today"); - if (!window.__awRuRdpReportCache) window.__awRuRdpReportCache = {}; - if (window.__awRuRdpReportCache[cacheKey]) return window.__awRuRdpReportCache[cacheKey]; - const range = getActivityDayRange(day); - const bucketId = "aw-worktime-sessions_" + host; - const params = new URLSearchParams(); - params.set("start", range.start.toISOString()); - params.set("end", new Date(range.end.getTime() + 1000).toISOString()); - params.set("limit", "100000"); - const response = await fetch("/api/0/buckets/" + encodeURIComponent(bucketId) + "/events?" + params.toString(), { credentials: "same-origin" }); - if (!response.ok) throw new Error("rdp-report-fetch-failed"); - const events = await response.json(); - if (!Array.isArray(events)) return null; - const rowsByUser = new Map(); - events.forEach(function (event) { - const data = event && event.data ? event.data : {}; - const ts = event && event.timestamp ? String(event.timestamp) : ""; - if (!ts) return; - const tsDate = new Date(ts); - if (Number.isNaN(tsDate.getTime())) return; - const tsDay = [ - tsDate.getFullYear(), - String(tsDate.getMonth() + 1).padStart(2, "0"), - String(tsDate.getDate()).padStart(2, "0") - ].join("-"); - if (tsDay !== range.normalizedDay) return; - const userId = String(data.userId || ""); - const userName = String(data.username || userId || "").trim(); - if (!userName) return; - const key = userId || userName; - if (!rowsByUser.has(key)) { - rowsByUser.set(key, { - user: userName, - user_id: userId || userName, - active_seconds: 0, - first_activity: "", - last_activity: "", - sessions_count: new Set(), - samples_count: 0, - active_samples: 0 - }); - } - const row = rowsByUser.get(key); - row.samples_count += 1; - if (data.sessionId !== undefined && data.sessionId !== null) row.sessions_count.add(String(data.sessionId)); - if (isWorktimeRowActive(data)) { - const sampleSeconds = Math.max(0, Number(data.sampleSeconds || event.duration || 0)); - row.active_seconds += sampleSeconds; - row.active_samples += 1; - if (!row.first_activity || ts < row.first_activity) row.first_activity = ts; - if (!row.last_activity || ts > row.last_activity) row.last_activity = ts; - } - }); - const payload = { - host: host, - report_date: range.normalizedDay, - rows: Array.from(rowsByUser.values()).map(function (row) { - return { - user: row.user, - user_id: row.user_id, - active_seconds: row.active_seconds, - active_hhmm: formatActiveHhmm(row.active_seconds), - first_activity: row.first_activity, - last_activity: row.last_activity, - sessions_count: row.sessions_count.size, - samples_count: row.samples_count, - active_samples: row.active_samples - }; - }) - }; - window.__awRuRdpReportCache[cacheKey] = payload; - return payload; - } - - async function injectRdpWorktimeCenter(root) { - if (!isClientActivityRoute()) return; - const host = getCurrentHostFromHash(); - const day = getCurrentActivityDayFromHash(); - const report = await fetchRdpWorktimeReport(host, day); - if (!report || !Array.isArray(report.rows) || !report.rows.length) return; - - const totalActiveSeconds = report.rows.reduce(function (sum, row) { - return sum + Math.max(0, Number(row && row.active_seconds || 0)); - }, 0); - const activeUsers = report.rows.filter(function (row) { - return Number(row && row.active_seconds || 0) > 0; - }); - const topRows = activeUsers - .slice() - .sort(function (left, right) { - return Number(right.active_seconds || 0) - Number(left.active_seconds || 0); - }) - .slice(0, 5); - - Array.from(root.querySelectorAll("li")).forEach(function (item) { - const text = (item.textContent || "").trim(); - if (/^(?:Активное время|Time active):/i.test(text)) { - item.textContent = "Активное время: " + formatDurationSeconds(totalActiveSeconds); - } - }); - - const heading = root.querySelector("h3"); - if (!heading || !heading.parentElement) return; - - let center = root.querySelector("[data-aw-ru-rdp-center='1']"); - if (!center) { - center = document.createElement("section"); - center.className = "aw-ru-rdp-center"; - center.setAttribute("data-aw-ru-rdp-center", "1"); - const anchor = heading.parentElement.querySelector("img") || null; - heading.parentElement.insertBefore(center, anchor); - } - - const latestActivity = topRows.reduce(function (latest, row) { - const value = row && row.last_activity ? String(row.last_activity) : ""; - if (!value) return latest; - if (!latest) return value; - return value > latest ? value : latest; - }, ""); - - center.innerHTML = - '

RDP сводка

' + - '

Этот блок строится из bucket aw-worktime-sessions через AW API и показывает сводку по RDP-сессиям выбранного хоста.

' + - '
' + - '
Активное время
' + escapeHtml(formatDurationSeconds(totalActiveSeconds)) + '
' + - '
Активных пользователей
' + escapeHtml(String(activeUsers.length)) + '
' + - '
Последняя активность
' + escapeHtml(formatIsoForUi(latestActivity)) + '
' + - '
' + - '' + - '' + - '' + - (topRows.length ? topRows.map(function (row) { - return '' + - '' + - '' + - '' + - '' + - ''; - }).join("") : '') + - '' + - '
ПользовательАктивное времяПервая активностьПоследняя активность
' + escapeHtml(row.user || row.user_id || "") + '' + escapeHtml(row.active_hhmm || formatDurationSeconds(row.active_seconds || 0)) + '' + escapeHtml(formatIsoForUi(row.first_activity || "")) + '' + escapeHtml(formatIsoForUi(row.last_activity || "")) + '
Нет активных пользователей в отчёте.
' + - ''; - } - - function enforceSafeActivityViewForPveHost() { - const hash = window.location.hash || ""; - const match = hash.match(/^#\/activity\/([^/]+)(?:\/day\/([^/]+))?\/view\/([^/?#]+)/i); - if (!match) return; - const host = decodeURIComponent(match[1] || ""); - const day = match[2] ? decodeURIComponent(match[2]) : ""; - const viewId = decodeURIComponent(match[3] || ""); - const prefix = day - ? "#/activity/" + encodeURIComponent(host) + "/day/" + encodeURIComponent(day) + "/view/" - : "#/activity/" + encodeURIComponent(host) + "/view/"; - if (isPveLikeHost(host)) { - const safeHash = prefix + encodeURIComponent("pve_audit"); - if (safeHash !== hash && !/^pve_audit$/i.test(viewId)) { - window.location.replace(safeHash); - } - return; - } - if (/^pve_audit$/i.test(viewId)) { - window.location.replace(prefix + encodeURIComponent("summary")); - } - } - - function getDlpHostFromSettings(settings) { - const routeHost = getCurrentHostFromHash(); - if (isLikelyClientHost(routeHost)) return routeHost; - const bucketHost = getDlpHostFromBucketId(getDlpBucketIdFromHash()); - if (isLikelyClientHost(bucketHost)) return bucketHost; - return getTrendsHostFromSettings(settings); - } - - function getDlpHref(host) { - if (!host) return "#/buckets"; - return "#/buckets/" + encodeURIComponent("aw-dlp-endpoint-signals_" + host); - } - - function isDlpSignalBucketRoute() { - return /^#\/buckets\/aw-dlp-endpoint-signals_/i.test(window.location.hash || ""); - } - - function isAlertsRoute() { - return /^#\/alerts(?:[/?#]|$)/i.test(window.location.hash || ""); - } - - function getDlpBucketIdFromHash() { - const hash = window.location.hash || ""; - const match = hash.match(/^#\/buckets\/([^/?#]+)/i); - return match && match[1] ? decodeURIComponent(match[1]) : ""; - } - - function getDlpHostFromBucketId(bucketId) { - const prefix = "aw-dlp-endpoint-signals_"; - return bucketId.startsWith(prefix) ? bucketId.slice(prefix.length) : ""; - } - - function escapeHtml(value) { - return String(value == null ? "" : value) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - - function normalizeText(value) { - return String(value == null ? "" : value).trim(); - } - - function buildDlpKey(event) { - const data = event && event.data ? event.data : {}; - return [ - event && event.timestamp || "", - data.signalType || "", - data.username || "", - data.owner || "", - data.documentName || "", - data.printerName || "" - ].join("|"); - } - - function generateDlpId(prefix) { - return prefix + "-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10); - } - - async function awApiJson(url, options) { - const response = await fetch(url, Object.assign({ - credentials: "same-origin", - headers: { - "Content-Type": "application/json; charset=utf-8" - } - }, options || {})); - if (response.status === 304 || response.status === 409) { - return null; - } - if (!response.ok) { - throw new Error("aw-api-" + response.status); - } - if (response.status === 204) return null; - const text = await response.text(); - return text ? JSON.parse(text) : null; - } - - async function ensureAwBucket(bucketId, clientName, bucketType, hostname) { - await awApiJson("/api/0/buckets/" + encodeURIComponent(bucketId), { - method: "POST", - body: JSON.stringify({ - client: clientName, - type: bucketType, - hostname: hostname - }) - }); - } - - async function saveAwHeartbeat(bucketId, payload, pulsetimeSeconds) { - const pulsetime = pulsetimeSeconds || 1; - await awApiJson("/api/0/buckets/" + encodeURIComponent(bucketId) + "/heartbeat?pulsetime=" + pulsetime, { - method: "POST", - body: JSON.stringify(payload) - }); - } - - async function loadBucketEvents(bucketId, limit) { - const data = await awApiJson("/api/0/buckets/" + encodeURIComponent(bucketId) + "/events?limit=" + (limit || 100), { - method: "GET", - headers: {} - }); - return Array.isArray(data) ? data : []; - } - - function getRuleMatchFields(event) { - const data = event && event.data ? event.data : {}; - return { - signalType: normalizeText(data.signalType), - username: normalizeText(data.username), - owner: normalizeText(data.owner), - documentName: normalizeText(data.documentName), - printerName: normalizeText(data.printerName), - hostname: normalizeText(data.hostname) - }; - } - - function serializeRuleMatch(match) { - const normalized = match || {}; - return [ - normalized.signalType || "", - normalized.username || "", - normalized.owner || "", - normalized.documentName || "", - normalized.printerName || "", - normalized.hostname || "" - ].join("|"); - } - - function getRuleId(ruleEvent) { - const data = ruleEvent && ruleEvent.data ? ruleEvent.data : {}; - return normalizeText(data.ruleId) || [ - serializeRuleMatch(data.match || {}), - normalizeText(data.category), - normalizeText(data.action) - ].join("|"); - } - - function getReviewId(reviewEvent) { - const data = reviewEvent && reviewEvent.data ? reviewEvent.data : {}; - const review = data.review || {}; - return normalizeText(review.reviewId) || [ - data.sourceEvent && data.sourceEvent.timestamp || "", - data.sourceEvent && data.sourceEvent.data ? buildDlpKey({ timestamp: data.sourceEvent.timestamp, data: data.sourceEvent.data }) : "", - normalizeText(review.verdict), - normalizeText(review.category) - ].join("|"); - } - - function collapseRuleEvents(events) { - const ordered = (events || []).slice().sort(function (a, b) { - return String(a.timestamp).localeCompare(String(b.timestamp)); - }); - const map = new Map(); - ordered.forEach(function (event) { - map.set(getRuleId(event), event); - }); - return Array.from(map.values()).sort(function (a, b) { - return String(b.timestamp).localeCompare(String(a.timestamp)); - }); - } - - function collapseReviewEvents(events) { - const ordered = (events || []).slice().sort(function (a, b) { - return String(a.timestamp).localeCompare(String(b.timestamp)); - }); - const map = new Map(); - ordered.forEach(function (event) { - map.set(getReviewId(event), event); - }); - return Array.from(map.values()).sort(function (a, b) { - return String(b.timestamp).localeCompare(String(a.timestamp)); - }); - } - - function ruleMatchesEvent(rule, event) { - const eventFields = getRuleMatchFields(event); - const match = rule && rule.data && rule.data.match ? rule.data.match : {}; - return Object.keys(eventFields).every(function (key) { - const ruleValue = normalizeText(match[key]); - return !ruleValue || ruleValue === eventFields[key]; - }); - } - - function getSuppressionState() { - if (!window.__awRuDlpState) { - window.__awRuDlpState = { - rules: [], - activeRules: [], - reviews: [], - events: [], - loading: false - }; - } - return window.__awRuDlpState; - } - - function removeBadDlpLinks(root) { - const links = Array.from(root.querySelectorAll("a[href], [role='link']")); - links.forEach(function (link) { - const href = String(link.getAttribute("href") || ""); - const label = normalizeText(link.textContent || ""); - const isBrokenActivityDlpLink = /\/view\/dlp(?:[/?#]|$)/i.test(href); - const isActivityTabDlpLabel = label === "DLP" && !!link.closest("li"); - if (!isBrokenActivityDlpLink && !isActivityTabDlpLabel) return; - const item = link.closest("li") || link; - if (item && item.getAttribute && item.getAttribute("data-aw-ru-dlp-item") === "1") return; - item.remove(); - }); - } - - function updateDlpLinks(root, href) { - const links = root.querySelectorAll("a[data-aw-ru-dlp-link='1']"); - for (const link of links) { - if (link.getAttribute("href") !== href) { - link.setAttribute("href", href); - } - } - } - - function buildDlpNavItem(templateItem, href) { - const templateLink = templateItem.querySelector("a[href], [role='link']"); - if (!templateLink) return null; - - const item = document.createElement("li"); - item.setAttribute("data-aw-ru-dlp-item", "1"); - item.className = templateItem.className || ""; - - const link = document.createElement("a"); - link.setAttribute("href", href); - link.setAttribute("data-aw-ru-dlp-link", "1"); - link.textContent = "DLP"; - link.className = templateLink.className || ""; - - item.appendChild(link); - return item; - } - - function findPrimaryNavList(root) { - const navLists = Array.from(root.querySelectorAll("nav ul")); - return navLists.find(function (list) { - const labels = Array.from(list.querySelectorAll("a")) - .map(function (link) { return normalizeText(link.textContent); }) - .filter(Boolean); - return labels.includes("Главная") || - labels.includes("Home") || - labels.includes("Активность") || - labels.includes("Activity"); - }) || null; - } - - function injectDlpNavigation(root) { - const hostForDlp = window.__awRuPatchSettingsHost || getCurrentHostFromHash(); - if (hostForDlp && isPveLikeHost(hostForDlp)) { - removeBadDlpLinks(root); - const ownItem = root.querySelector("[data-aw-ru-dlp-item='1']"); - if (ownItem) ownItem.remove(); - return; - } - const href = getDlpHref(hostForDlp); - removeBadDlpLinks(root); - updateDlpLinks(root, href); - if (root.querySelector("[data-aw-ru-dlp-item='1']")) return; - - const primaryNav = findPrimaryNavList(root); - if (primaryNav) { - const templateItem = primaryNav.querySelector("li") || primaryNav.parentElement; - const dlpItem = templateItem ? buildDlpNavItem(templateItem, href) : null; - if (dlpItem) { - primaryNav.appendChild(dlpItem); - return; - } - const item = document.createElement("li"); - item.setAttribute("data-aw-ru-dlp-item", "1"); - const link = document.createElement("a"); - link.setAttribute("href", href); - link.setAttribute("data-aw-ru-dlp-link", "1"); - link.textContent = "DLP"; - item.appendChild(link); - primaryNav.appendChild(item); - } - } - - function isHomeRoute() { - const hash = window.location.hash || ""; - return !hash || /^#\/home(?:[/?#]|$)/i.test(hash); - } - - function getDefaultHostGroupsConfig() { - return { - groups: [ - { - id: "windows-rdp", - name: "Windows RDP", - description: "Пользовательские Windows/RDP хосты.", - patterns: ["^(SHARKON|WIN|RDP|TERM|TS-|WS-)"], - links: [ - { label: "Активность", type: "activity" }, - { label: "DLP", type: "bucket", bucket_prefix: "aw-dlp-endpoint-signals_" } - ] - }, - { - id: "linux-remote", - name: "Linux remote workers", - description: "Linux-хосты удалённых сотрудников: GUI активность, SSH/console и browser admin UI.", - patterns: ["^(LINUX-WS|LINUX-DESKTOP|LX-|DESKTOP-|ADMIN-|WORKSTATION-|DEVBOX-)"], - links: [ - { label: "Активность", type: "activity" }, - { label: "SSH сессии", type: "bucket", bucket_prefix: "aw-ssh-sessions_" }, - { label: "Команды shell", type: "bucket", bucket_prefix: "aw-console-commands_" }, - { label: "Web категории", type: "bucket", bucket_prefix: "aw-detmir-web-category_" }, - { label: "Все бакеты", type: "buckets" } - ] - }, - { - id: "virtual-infra", - name: "Virtual servers + Proxmox", - description: "Инфраструктурные VM и узлы Proxmox.", - patterns: ["^(PVE|PROXMOX|DEBIAN|UBUNTU|LINUX|VM-|SRV-|INFRA-)"], - links: [ - { label: "Все бакеты", type: "buckets" } - ] - } - ], - ungrouped_name: "Прочие хосты" - }; - } - - function getHostGroupsState() { - if (!window.__awRuHostGroupsState) { - window.__awRuHostGroupsState = { - config: null, - buckets: null, - loading: false - }; - } - return window.__awRuHostGroupsState; - } - - async function ensureHostGroupsData() { - const state = getHostGroupsState(); - if (state.loading) return state; - if (state.config && state.buckets) return state; - state.loading = true; - try { - if (!state.config) { - try { - state.config = await awApiJson("/js/aw-host-groups.json?v=" + encodeURIComponent(window.__awRuPatchVersion), { method: "GET", headers: {} }); - } catch (error) { - state.config = getDefaultHostGroupsConfig(); - } - } - if (!state.buckets) { - state.buckets = await awApiJson("/api/0/buckets/", { method: "GET", headers: {} }); - } - } finally { - state.loading = false; - } - return state; - } - - function isPveActivityRoute() { - const hash = window.location.hash || ""; - const match = hash.match(/^#\/activity\/([^/]+)/i); - return !!(match && isPveLikeHost(decodeURIComponent(match[1] || ""))); - } - - function extractHostFromBucket(bucketId, bucketMeta) { - if (bucketMeta && bucketMeta.hostname) return String(bucketMeta.hostname); - const prefixes = [ - "aw-watcher-window_", - "aw-watcher-afk_", - "aw-console-commands_", - "aw-ssh-sessions_", - "aw-linux-web-context_", - "aw-detmir-web-category_", - "aw-dlp-endpoint-signals_", - "aw-session-events_", - "aw-worktime-sessions_", - "aw-pve-webadmin-events_", - "aw-pve-task-events_", - "aw-dlp-incidents_", - ]; - for (const prefix of prefixes) { - if (bucketId.indexOf(prefix) === 0) { - return bucketId.slice(prefix.length); - } - } - return ""; - } - - function buildHostBucketMap(rawBuckets) { - const result = new Map(); - const entries = Array.isArray(rawBuckets) - ? rawBuckets.map(function (item) { return [item.id || "", item]; }) - : Object.entries(rawBuckets || {}); - entries.forEach(function (entry) { - const bucketId = entry[0]; - const meta = entry[1] || {}; - const host = extractHostFromBucket(bucketId, meta); - if (!host) return; - if (!result.has(host)) result.set(host, []); - result.get(host).push(bucketId); - }); - return result; - } - - function hostHasBucketPrefix(hostBuckets, prefix) { - return (hostBuckets || []).some(function (bucketId) { - return String(bucketId || "").indexOf(prefix) === 0; - }); - } - - function matchHostGroup(host, groups, hostBuckets) { - const bucketList = hostBuckets || []; - if (hostHasBucketPrefix(bucketList, "aw-dlp-endpoint-signals_") || hostHasBucketPrefix(bucketList, "aw-session-events_")) { - return "windows-rdp"; - } - if ( - hostHasBucketPrefix(bucketList, "aw-console-commands_") || - hostHasBucketPrefix(bucketList, "aw-ssh-sessions_") || - hostHasBucketPrefix(bucketList, "aw-linux-web-context_") || - hostHasBucketPrefix(bucketList, "aw-detmir-web-category_") - ) { - if (!hostHasBucketPrefix(bucketList, "aw-pve-webadmin-events_") && !hostHasBucketPrefix(bucketList, "aw-pve-task-events_")) { - return "linux-remote"; - } - } - for (const group of groups) { - const patterns = Array.isArray(group.patterns) ? group.patterns : []; - for (const pattern of patterns) { - try { - if (new RegExp(pattern, "i").test(host)) { - return group.id; - } - } catch (error) { - } - } - } - return ""; - } - - function buildHostLink(host, hostBuckets, linkDef) { - if (!linkDef || !linkDef.type) return ""; - if (linkDef.type === "activity") { - const viewId = linkDef.view ? String(linkDef.view) : "summary"; - return '#/activity/' + encodeURIComponent(host) + '/day/' + encodeURIComponent(new Date().toISOString().slice(0, 10)) + '/view/' + encodeURIComponent(viewId); - } - if (linkDef.type === "buckets") { - return "#/buckets"; - } - if (linkDef.type === "bucket" && linkDef.bucket_prefix) { - const bucketId = String(linkDef.bucket_prefix) + host; - return hostBuckets.indexOf(bucketId) >= 0 ? '#/buckets/' + encodeURIComponent(bucketId) : ""; - } - return ""; - } - - function renderHostGroupCards(state) { - const config = state.config || getDefaultHostGroupsConfig(); - const groups = Array.isArray(config.groups) ? config.groups : []; - const hostBuckets = buildHostBucketMap(state.buckets); - const grouped = new Map(); - - groups.forEach(function (group) { - grouped.set(group.id, []); - }); - grouped.set("__ungrouped__", []); - - Array.from(hostBuckets.keys()).sort().forEach(function (host) { - const groupId = matchHostGroup(host, groups, hostBuckets.get(host) || []) || "__ungrouped__"; - grouped.get(groupId).push(host); - }); - - const cards = []; - groups.forEach(function (group) { - const hosts = grouped.get(group.id) || []; - const items = hosts.map(function (host) { - const links = (group.links || []).map(function (linkDef) { - const href = buildHostLink(host, hostBuckets.get(host) || [], linkDef); - return href ? '' + escapeHtml(linkDef.label || "Открыть") + '' : ""; - }).filter(Boolean).join(""); - return '
' + - '
' + escapeHtml(host) + '
' + - '' + - '
'; - }).join(""); - cards.push( - '
' + - '

' + escapeHtml(group.name || group.id) + '

' + - '

' + escapeHtml(group.description || "") + '

' + - '
' + (items || '
Хосты пока не обнаружены.
') + '
' + - '
' - ); - }); - - const ungroupedHosts = grouped.get("__ungrouped__") || []; - if (ungroupedHosts.length) { - cards.push( - '
' + - '

' + escapeHtml(config.ungrouped_name || "Прочие хосты") + '

' + - '

Хосты, которые пока не попали под шаблоны группировки.

' + - '
' + - ungroupedHosts.map(function (host) { - return '
' + escapeHtml(host) + '
'; - }).join("") + - '
' + - '
' - ); - } - - return cards.join(""); - } - - async function injectHostGroupsCenter(root) { - if (!isHomeRoute()) return; - const heading = root.querySelector("h3"); - if (!heading) return; - - let center = root.querySelector("[data-aw-ru-host-groups='1']"); - if (!center) { - center = document.createElement("section"); - center.className = "aw-ru-host-groups"; - center.setAttribute("data-aw-ru-host-groups", "1"); - center.innerHTML = - '

Разделы хостов

' + - '

Здесь хосты разделены на Windows RDP, Linux remote workers и инфраструктурные узлы.

' + - '

Загрузка...

'; - heading.parentElement.insertBefore(center, heading.nextSibling); - } - - const state = await ensureHostGroupsData(); - center.querySelector("[data-aw-ru-host-groups-grid]").innerHTML = renderHostGroupCards(state); - } - - function renderDlpTableRows(center, host) { - const state = getSuppressionState(); - const tbody = center.querySelector("[data-aw-ru-dlp-events]"); - if (!tbody) return; - const hideSuppressed = center.querySelector("[data-aw-ru-hide-suppressed]") && center.querySelector("[data-aw-ru-hide-suppressed]").checked; - const rows = []; - for (const event of state.events) { - const data = event.data || {}; - if (String(data.signalType || "").toLowerCase() === "self_test") continue; - const matchedRule = state.activeRules.find(function (rule) { return ruleMatchesEvent(rule, event); }) || null; - if (hideSuppressed && matchedRule) continue; - const eventKey = buildDlpKey(event); - rows.push( - '' + - "" + escapeHtml(new Date(event.timestamp).toLocaleString()) + "" + - "" + escapeHtml(data.signalType || "") + "" + - "" + escapeHtml(data.username || data.owner || "") + "" + - "" + escapeHtml(data.documentName || "") + "" + - "" + escapeHtml(data.printerName || "") + "" + - "" + (matchedRule ? 'Подавлено правилом' : "") + "" + - '" + - '' + - '' + - '' + - '' + - '' + - '' + - "" + - "" - ); - } - tbody.innerHTML = rows.length ? rows.join("") : 'Нет DLP-событий в выборке.'; - center.querySelector("[data-aw-ru-dlp-status]").textContent = - "Событий: " + state.events.length + " · правил: " + state.activeRules.length + "/" + state.rules.length + " · review: " + state.reviews.filter(function (review) { return !(review.data && review.data.review && review.data.review.archived); }).length + "/" + state.reviews.length; - bindDlpRowActions(center, host); - renderDlpRuleManager(center, host); - renderDlpReviewManager(center, host); - } - - async function saveDlpReview(host, event, row) { - const bucketId = "aw-dlp-review_" + host; - await ensureAwBucket(bucketId, "aw-dlp-review", "aw.dlp.review", host); - const verdict = row.querySelector("[data-aw-ru-dlp-verdict]").value; - const category = row.querySelector("[data-aw-ru-dlp-category]").value.trim(); - const comment = row.querySelector("[data-aw-ru-dlp-comment]").value.trim(); - await saveAwHeartbeat(bucketId, { - timestamp: new Date().toISOString(), - duration: 0, - data: { - host: host, - sourceBucket: getDlpBucketIdFromHash(), - sourceEvent: { - timestamp: event.timestamp, - data: event.data || {} - }, - review: { - reviewId: generateDlpId("review"), - verdict: verdict, - category: category, - comment: comment, - archived: false - } - } - }, 1); - if (verdict === "incident") { - await saveDlpIncident(host, event, { - verdict: verdict, - category: category, - comment: comment - }); - } - } - - async function saveDlpIncident(host, event, review) { - const bucketId = "aw-dlp-incidents_" + host; - await ensureAwBucket(bucketId, "aw-dlp-incidents", "aw.dlp.incident", host); - await saveAwHeartbeat(bucketId, { - timestamp: new Date().toISOString(), - duration: 0, - data: { - host: host, - sourceBucket: getDlpBucketIdFromHash(), - sourceEvent: { - timestamp: event.timestamp, - data: event.data || {} - }, - incident: { - incidentId: generateDlpId("incident"), - verdict: review && review.verdict || "incident", - category: review && review.category || "", - comment: review && review.comment || "", - status: "open" - } - } - }, 1); - } - - function getCaseApiBase() { - if (window.__awCaseApiBase && typeof window.__awCaseApiBase === "string") { - return window.__awCaseApiBase.replace(/\/+$/, ""); - } - try { - const origin = window.location.origin || ""; - if (/:\d+$/.test(origin)) return origin.replace(/:\d+$/, ":5602"); - return origin + ":5602"; - } catch (error) { - return "http://127.0.0.1:5602"; - } - } - - async function caseApi(path, init) { - const response = await fetch(getCaseApiBase() + path, Object.assign({ credentials: "omit" }, init || {})); - if (!response.ok) throw new Error("Case API HTTP " + response.status); - if (response.status === 204) return null; - return response.json(); - } - - async function createCaseFromEvent(host, event, row) { - const data = event.data || {}; - if (String(data.signalType || "").toLowerCase() === "self_test") { - throw new Error("self_test не должен превращаться в кейс"); - } - const verdict = row.querySelector("[data-aw-ru-dlp-verdict]").value; - const category = row.querySelector("[data-aw-ru-dlp-category]").value.trim(); - const comment = row.querySelector("[data-aw-ru-dlp-comment]").value.trim(); - const incidentId = buildDlpKey(event); - const title = "DLP " + (data.signalType || "incident") + " · " + (data.username || data.owner || host || "unknown"); - return caseApi("/api/0/dlp/cases", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - incident_id: incidentId, - host: host, - title: title, - severity: verdict === "incident" ? "high" : "medium", - source_bucket: getDlpBucketIdFromHash(), - source_event_ts: event.timestamp, - evidence: { - signalType: data.signalType || "", - username: data.username || data.owner || "", - documentName: data.documentName || "", - printerName: data.printerName || "", - category: category, - comment: comment - } - }) - }); - } - - async function saveDlpRule(host, event, row) { - const bucketId = "aw-dlp-rules_" + host; - await ensureAwBucket(bucketId, "aw-dlp-rules", "aw.dlp.rule", host); - const verdict = row.querySelector("[data-aw-ru-dlp-verdict]").value; - const category = row.querySelector("[data-aw-ru-dlp-category]").value.trim(); - const comment = row.querySelector("[data-aw-ru-dlp-comment]").value.trim(); - await saveAwHeartbeat(bucketId, { - timestamp: new Date().toISOString(), - duration: 0, - data: { - host: host, - ruleId: generateDlpId("rule"), - enabled: true, - action: verdict, - category: category, - comment: comment, - match: getRuleMatchFields(event) - } - }, 1); - } - - function bindDlpRowActions(center, host) { - const state = getSuppressionState(); - const rows = center.querySelectorAll("[data-aw-ru-dlp-key]"); - rows.forEach(function (row) { - if (row.getAttribute("data-aw-ru-bound") === "1") return; - row.setAttribute("data-aw-ru-bound", "1"); - const eventKey = row.getAttribute("data-aw-ru-dlp-key"); - const event = state.events.find(function (item) { return buildDlpKey(item) === eventKey; }); - if (!event) return; - row.querySelector("[data-aw-ru-save-review]").addEventListener("click", async function () { - const message = center.querySelector("[data-aw-ru-dlp-message]"); - try { - await saveDlpReview(host, event, row); - state.reviews = collapseReviewEvents(await loadBucketEvents("aw-dlp-review_" + host, 200)); - renderDlpReviewManager(center, host); - center.querySelector("[data-aw-ru-dlp-status]").textContent = - "Событий: " + state.events.length + " · правил: " + state.activeRules.length + "/" + state.rules.length + " · review: " + state.reviews.filter(function (review) { return !(review.data && review.data.review && review.data.review.archived); }).length + "/" + state.reviews.length; - message.textContent = "Review сохранен."; - } catch (error) { - message.textContent = "Ошибка сохранения review: " + error.message; - } - }); - row.querySelector("[data-aw-ru-save-rule]").addEventListener("click", async function () { - const message = center.querySelector("[data-aw-ru-dlp-message]"); - try { - await saveDlpRule(host, event, row); - state.rules = collapseRuleEvents(await loadBucketEvents("aw-dlp-rules_" + host, 200)); - state.activeRules = state.rules.filter(function (rule) { return !(rule.data && rule.data.enabled === false); }); - message.textContent = "Правило сохранено."; - renderDlpTableRows(center, host); - } catch (error) { - message.textContent = "Ошибка сохранения правила: " + error.message; - } - }); - row.querySelector("[data-aw-ru-create-case]").addEventListener("click", async function () { - const message = center.querySelector("[data-aw-ru-dlp-message]"); - try { - const created = await createCaseFromEvent(host, event, row); - await renderCaseManager(center, host); - message.textContent = "Кейс создан: #" + (created && created.id ? created.id : "?"); - } catch (error) { - message.textContent = "Ошибка создания кейса: " + error.message; - } - }); - }); - } - - async function renderCaseManager(center, host) { - const tbody = center.querySelector("[data-aw-ru-dlp-cases]"); - if (!tbody) return; - try { - const cases = await caseApi("/api/0/dlp/cases?host=" + encodeURIComponent(host) + "&limit=100", { method: "GET" }); - function renderCaseDfir(c) { - const hayabusa = c && c.forensics && c.forensics.hayabusa; - if (!hayabusa) return ""; - const status = String(hayabusa.status || ""); - const mode = String(hayabusa.mode || ""); - const caseHost = normalizeText(c && c.host); - const forensicHost = normalizeText(hayabusa.host); - const reportDir = String(hayabusa.report_dir || ""); - const hostMismatch = caseHost && forensicHost && caseHost !== forensicHost; - const titleParts = []; - if (reportDir) titleParts.push(reportDir); - if (hostMismatch) titleParts.push("host mismatch: case=" + caseHost + " forensic=" + forensicHost); - const title = titleParts.length ? ' title="' + escapeHtml(titleParts.join(" | ")) + '"' : ""; - if (hostMismatch) { - return 'Hayabusa host-mismatch · ' + escapeHtml(forensicHost) + ''; - } - return 'Hayabusa ' + escapeHtml(status) + (mode ? " · " + escapeHtml(mode) : "") + ''; - } - const rows = (cases || []).map(function (c) { - return ( - "" + - "" + escapeHtml(String(c.id || "")) + "" + - "" + escapeHtml(String(c.status || "")) + "" + - "" + escapeHtml(String(c.severity || "")) + "" + - "" + escapeHtml(String(c.title || "")) + "" + - "" + escapeHtml(String(c.assignee || "")) + "" + - "" + escapeHtml(String(c.incident_id || "")) + "" + - "" + renderCaseDfir(c) + "" + - "" + escapeHtml(String(c.updated_at || c.created_at || "")) + "" + - "" - ); - }); - tbody.innerHTML = rows.length ? rows.join("") : 'Кейсов нет.'; - const status = center.querySelector("[data-aw-ru-dlp-cases-status]"); - if (status) status.textContent = "Кейсов: " + (cases || []).length; - } catch (error) { - tbody.innerHTML = 'Ошибка загрузки кейсов: ' + escapeHtml(error.message) + ''; - const status = center.querySelector("[data-aw-ru-dlp-cases-status]"); - if (status) status.textContent = "Кейсы недоступны"; - } - } - - async function setDlpRuleEnabled(host, ruleEvent, enabled) { - const bucketId = "aw-dlp-rules_" + host; - await ensureAwBucket(bucketId, "aw-dlp-rules", "aw.dlp.rule", host); - const data = ruleEvent.data || {}; - await saveAwHeartbeat(bucketId, { - timestamp: new Date().toISOString(), - duration: 0, - data: { - host: host, - ruleId: getRuleId(ruleEvent), - enabled: enabled, - action: data.action || "", - category: data.category || "", - comment: data.comment || "", - match: data.match || {} - } - }, 1); - } - - async function setDlpReviewArchived(host, reviewEvent, archived) { - const bucketId = "aw-dlp-review_" + host; - await ensureAwBucket(bucketId, "aw-dlp-review", "aw.dlp.review", host); - const data = reviewEvent.data || {}; - const review = data.review || {}; - await saveAwHeartbeat(bucketId, { - timestamp: new Date().toISOString(), - duration: 0, - data: { - host: host, - sourceBucket: data.sourceBucket || getDlpBucketIdFromHash(), - sourceEvent: data.sourceEvent || {}, - review: { - reviewId: getReviewId(reviewEvent), - verdict: review.verdict || "", - category: review.category || "", - comment: review.comment || "", - archived: archived - } - } - }, 1); - } - - function renderDlpRuleManager(center, host) { - const state = getSuppressionState(); - const tbody = center.querySelector("[data-aw-ru-dlp-rules]"); - if (!tbody) return; - const showDisabled = center.querySelector("[data-aw-ru-show-disabled-rules]") && center.querySelector("[data-aw-ru-show-disabled-rules]").checked; - const rows = []; - state.rules.forEach(function (ruleEvent) { - const data = ruleEvent.data || {}; - const isEnabled = data.enabled !== false; - if (!isEnabled && !showDisabled) return; - rows.push( - '' + - '' + escapeHtml(new Date(ruleEvent.timestamp).toLocaleString()) + '' + - '' + (isEnabled ? 'Активно' : 'Отключено') + '' + - '' + escapeHtml(data.action || "") + '' + - '' + escapeHtml(data.category || "") + '' + - '' + escapeHtml(serializeRuleMatch(data.match || {})) + '' + - '' + escapeHtml(data.comment || "") + '' + - '' + - '' + - '' + - '' - ); - }); - tbody.innerHTML = rows.length ? rows.join("") : 'Сохраненных правил нет.'; - tbody.querySelectorAll("[data-aw-ru-toggle-rule]").forEach(function (button) { - button.addEventListener("click", async function () { - const row = button.closest("[data-aw-ru-rule-id]"); - const ruleId = row && row.getAttribute("data-aw-ru-rule-id"); - const ruleEvent = state.rules.find(function (item) { return getRuleId(item) === ruleId; }); - if (!ruleEvent) return; - const message = center.querySelector("[data-aw-ru-dlp-message]"); - try { - await setDlpRuleEnabled(host, ruleEvent, ruleEvent.data && ruleEvent.data.enabled === false); - state.rules = collapseRuleEvents(await loadBucketEvents("aw-dlp-rules_" + host, 200)); - state.activeRules = state.rules.filter(function (rule) { return !(rule.data && rule.data.enabled === false); }); - renderDlpTableRows(center, host); - message.textContent = "Статус правила обновлен."; - } catch (error) { - message.textContent = "Ошибка обновления правила: " + error.message; - } - }); - }); - } - - function renderDlpReviewManager(center, host) { - const state = getSuppressionState(); - const tbody = center.querySelector("[data-aw-ru-dlp-reviews]"); - if (!tbody) return; - const showArchived = center.querySelector("[data-aw-ru-show-archived-reviews]") && center.querySelector("[data-aw-ru-show-archived-reviews]").checked; - const rows = []; - state.reviews.forEach(function (reviewEvent) { - const data = reviewEvent.data || {}; - const review = data.review || {}; - const archived = review.archived === true; - if (archived && !showArchived) return; - const sourceData = data.sourceEvent && data.sourceEvent.data ? data.sourceEvent.data : {}; - rows.push( - '' + - '' + escapeHtml(new Date(reviewEvent.timestamp).toLocaleString()) + '' + - '' + (archived ? 'Архив' : 'Активно') + '' + - '' + escapeHtml(review.verdict || "") + '' + - '' + escapeHtml(review.category || "") + '' + - '' + escapeHtml(review.comment || "") + '' + - '' + escapeHtml(sourceData.signalType || "") + ' · ' + escapeHtml(sourceData.documentName || sourceData.printerName || sourceData.username || "") + '' + - '' + - '' + - '' + - '' - ); - }); - tbody.innerHTML = rows.length ? rows.join("") : 'Сохраненных review нет.'; - tbody.querySelectorAll("[data-aw-ru-toggle-review]").forEach(function (button) { - button.addEventListener("click", async function () { - const row = button.closest("[data-aw-ru-review-id]"); - const reviewId = row && row.getAttribute("data-aw-ru-review-id"); - const reviewEvent = state.reviews.find(function (item) { return getReviewId(item) === reviewId; }); - if (!reviewEvent) return; - const message = center.querySelector("[data-aw-ru-dlp-message]"); - try { - await setDlpReviewArchived(host, reviewEvent, !(reviewEvent.data && reviewEvent.data.review && reviewEvent.data.review.archived === true)); - state.reviews = collapseReviewEvents(await loadBucketEvents("aw-dlp-review_" + host, 200)); - renderDlpTableRows(center, host); - message.textContent = "Статус review обновлен."; - } catch (error) { - message.textContent = "Ошибка обновления review: " + error.message; - } - }); - }); - } - - async function refreshDlpCenter(center, host) { - const state = getSuppressionState(); - if (state.loading) return; - state.loading = true; - center.querySelector("[data-aw-ru-dlp-message]").textContent = "Загрузка DLP-событий..."; - try { - state.events = (await loadBucketEvents(getDlpBucketIdFromHash(), 200)) - .sort(function (a, b) { return String(b.timestamp).localeCompare(String(a.timestamp)); }); - try { - state.rules = collapseRuleEvents(await loadBucketEvents("aw-dlp-rules_" + host, 200)); - state.activeRules = state.rules.filter(function (rule) { return !(rule.data && rule.data.enabled === false); }); - } catch (error) { - state.rules = []; - state.activeRules = []; - } - try { - state.reviews = collapseReviewEvents(await loadBucketEvents("aw-dlp-review_" + host, 200)); - } catch (error) { - state.reviews = []; - } - renderDlpTableRows(center, host); - await renderCaseManager(center, host); - center.querySelector("[data-aw-ru-dlp-message]").textContent = "DLP review центр обновлен."; - } catch (error) { - center.querySelector("[data-aw-ru-dlp-message]").textContent = "Ошибка загрузки DLP-событий: " + error.message; - } finally { - state.loading = false; - } - } - - function injectDlpReviewCenter(root) { - if (!isDlpSignalBucketRoute()) return; - const bucketId = getDlpBucketIdFromHash(); - const host = getDlpHostFromBucketId(bucketId); - if (!host) return; - - const heading = root.querySelector("h3"); - if (!heading) return; - - let center = root.querySelector("[data-aw-ru-dlp-center='1']"); - if (!center) { - center = document.createElement("section"); - center.className = "aw-ru-dlp-center"; - center.setAttribute("data-aw-ru-dlp-center", "1"); - center.innerHTML = - '

DLP review и правила

' + - '
' + - 'bucket: ' + escapeHtml(bucketId) + '' + - '' + - '' + - '
Событий: 0 · правил: 0 · review: 0
' + - '
' + - '

Здесь можно категорировать DLP-события и сохранять suppress/rule записи прямо в AW. Это снижает ложные сработки на уровне review-потока.

' + - '' + - '' + - '' + - '
ВремяТипПользовательДокументПринтер/каналСтатусВердиктКатегорияКомментарийДействия
Загрузка...
' + - '
' + - '
' + - '
DLP Rules
' + - '' + - '
' + - '' + - '' + - '' + - '
ВремяСтатусДействиеКатегорияMatchКомментарийУправление
Загрузка...
' + - '
' + - '
' + - '
' + - '
DLP Review
' + - '' + - '
' + - '' + - '' + - '' + - '
ВремяСтатусВердиктКатегорияКомментарийИсточникУправление
Загрузка...
' + - '
' + - '
' + - '
' + - '
Case Management
' + - '
Кейсов: 0
' + - '
' + - '' + - '' + - '' + - '
IDСтатусSeverityЗаголовокИсполнительIncident IDDFIRОбновлено
Загрузка...
' + - '
' + - '
'; - heading.parentElement.insertBefore(center, heading.nextSibling); - center.querySelector("[data-aw-ru-refresh-dlp]").addEventListener("click", function () { - refreshDlpCenter(center, host); - }); - center.querySelector("[data-aw-ru-hide-suppressed]").addEventListener("change", function () { - renderDlpTableRows(center, host); - }); - center.querySelector("[data-aw-ru-show-disabled-rules]").addEventListener("change", function () { - renderDlpRuleManager(center, host); - }); - center.querySelector("[data-aw-ru-show-archived-reviews]").addEventListener("change", function () { - renderDlpReviewManager(center, host); - }); - } - - if (center.getAttribute("data-aw-ru-loaded") !== "1") { - center.setAttribute("data-aw-ru-loaded", "1"); - refreshDlpCenter(center, host); - } - } - - async function refreshDlpAlertsCenter(center, host) { - center.querySelector("[data-aw-ru-dlp-alerts-message]").textContent = "Загрузка DLP-инцидентов..."; - try { - const events = (await loadBucketEvents("aw-dlp-incidents_" + host, 100)) - .sort(function (a, b) { return String(b.timestamp).localeCompare(String(a.timestamp)); }); - const rows = events.map(function (event) { - const data = event.data || {}; - const incident = data.incident || {}; - const sourceData = data.sourceEvent && data.sourceEvent.data ? data.sourceEvent.data : {}; - return '' + - '' + escapeHtml(new Date(event.timestamp).toLocaleString()) + '' + - '' + escapeHtml(incident.status || "open") + '' + - '' + escapeHtml(incident.category || "") + '' + - '' + escapeHtml(incident.comment || "") + '' + - '' + escapeHtml(sourceData.username || sourceData.owner || "") + '' + - '' + escapeHtml(sourceData.signalType || "") + '' + - '' + escapeHtml(sourceData.documentName || sourceData.printerName || "") + '' + - ''; - }); - center.querySelector("[data-aw-ru-dlp-alerts-events]").innerHTML = rows.length - ? rows.join("") - : 'Операторских DLP-инцидентов пока нет.'; - center.querySelector("[data-aw-ru-dlp-alerts-status]").textContent = "Инцидентов: " + events.length; - center.querySelector("[data-aw-ru-dlp-alerts-message]").textContent = "Список DLP-инцидентов обновлен."; - } catch (error) { - center.querySelector("[data-aw-ru-dlp-alerts-events]").innerHTML = 'Не удалось загрузить DLP-инциденты.'; - center.querySelector("[data-aw-ru-dlp-alerts-message]").textContent = "Ошибка загрузки DLP-инцидентов: " + error.message; - } - } - - async function refreshPveAuditCenter(center, host) { - const message = center.querySelector("[data-aw-ru-pve-audit-message]"); - const recentBody = center.querySelector("[data-aw-ru-pve-audit-events]"); - message.textContent = "Загрузка audit-событий..."; - try { - const [webEvents, taskEvents, sshEvents, cmdEvents] = await Promise.all([ - loadBucketEvents("aw-pve-webadmin-events_" + host, 50).catch(function () { return []; }), - loadBucketEvents("aw-pve-task-events_" + host, 50).catch(function () { return []; }), - loadBucketEvents("aw-ssh-sessions_" + host, 50).catch(function () { return []; }), - loadBucketEvents("aw-console-commands_" + host, 50).catch(function () { return []; }) - ]); - const data = { - web: webEvents || [], - tasks: taskEvents || [], - ssh: sshEvents || [], - cmd: cmdEvents || [] - }; - center.querySelector("[data-aw-ru-pve-web-count]").textContent = String(data.web.length); - center.querySelector("[data-aw-ru-pve-task-count]").textContent = String(data.tasks.length); - center.querySelector("[data-aw-ru-pve-ssh-count]").textContent = String(data.ssh.length); - center.querySelector("[data-aw-ru-pve-cmd-count]").textContent = String(data.cmd.length); - const recent = [] - .concat(data.web.map(function (event) { return { kind: "Web-admin", event: event, text: (event.data && (event.data.method || "") + " " + (event.data.path || "")) || "" }; })) - .concat(data.tasks.map(function (event) { return { kind: "PVE task", event: event, text: (event.data && ((event.data.action || "") + " " + (event.data.target || ""))) || "" }; })) - .concat(data.ssh.map(function (event) { return { kind: "SSH", event: event, text: (event.data && ((event.data.event || "") + " " + (event.data.tty || ""))) || "" }; })) - .concat(data.cmd.slice(0, 25).map(function (event) { return { kind: "Shell", event: event, text: (event.data && (event.data.command || "")) || "" }; })) - .sort(function (a, b) { return String(b.event && b.event.timestamp || "").localeCompare(String(a.event && a.event.timestamp || "")); }) - .slice(0, 25); - recentBody.innerHTML = recent.length ? recent.map(function (item) { - const ev = item.event || {}; - const d = ev.data || {}; - return "" + - "" + escapeHtml(new Date(ev.timestamp).toLocaleString()) + "" + - "" + escapeHtml(item.kind) + "" + - "" + escapeHtml(d.user || d.username || "-") + "" + - "" + escapeHtml(d.remote_ip || d.tty || d.host || "-") + "" + - "" + escapeHtml(item.text) + "" + - ""; - }).join("") : 'Пока нет audit-событий.'; - message.textContent = "Audit-панель обновлена."; - } catch (error) { - recentBody.innerHTML = 'Не удалось загрузить audit-события.'; - message.textContent = "Ошибка загрузки audit-событий: " + error.message; - } - } - - function injectPveAuditCenter(root) { - if (!isPveActivityRoute()) return; - const host = getCurrentHostFromHash(); - if (!host) return; - const heading = root.querySelector("h3"); - if (!heading || !heading.parentElement) return; - let center = root.querySelector("[data-aw-ru-pve-audit='1']"); - if (!center) { - center = document.createElement("section"); - center.className = "aw-ru-pve-audit"; - center.setAttribute("data-aw-ru-pve-audit", "1"); - center.innerHTML = - "

PVE Audit

" + - '

Для Proxmox-хоста показывается audit-панель вместо desktop-виджетов ActivityWatch, так как у этого хоста нет window/afk watcher данных.

' + - '
' + - '
Web-admin
0
' + - '
PVE tasks
0
' + - '
SSH events
0
' + - '
Shell commands
0
' + - "
" + - '' + - "" + - '' + - "
ВремяТипПользовательИсточникДетали
Загрузка...
" + - '
'; - heading.parentElement.insertBefore(center, heading.nextSibling); - } - Array.from(heading.parentElement.children).forEach(function (child) { - if (child === heading || child === center) return; - child.style.display = "none"; - }); - const routeKey = host + "|" + (window.location.hash || ""); - if (center.getAttribute("data-aw-ru-pve-route") !== routeKey) { - center.setAttribute("data-aw-ru-pve-route", routeKey); - refreshPveAuditCenter(center, host); - } - } - - function hidePveAuditTabForRegularHost(root) { - const hash = window.location.hash || ""; - const match = hash.match(/^#\/activity\/([^/]+)/i); - const host = match && match[1] ? decodeURIComponent(match[1]) : ""; - if (!host || isPveLikeHost(host)) return; - Array.from(root.querySelectorAll('a[href*="/view/pve_audit"]')).forEach(function (link) { - link.style.display = "none"; - }); - } - - function injectDlpAlertsCenter(root) { - if (!isAlertsRoute()) return; - const host = window.__awRuPatchSettingsHost || getCurrentHostFromHash(); - if (!host) return; - - const heading = root.querySelector("h3"); - if (!heading) return; - - let center = root.querySelector("[data-aw-ru-dlp-alerts='1']"); - if (!center) { - center = document.createElement("section"); - center.className = "aw-ru-dlp-center"; - center.setAttribute("data-aw-ru-dlp-alerts", "1"); - center.innerHTML = - '

DLP-инциденты оператора

' + - '
' + - 'bucket: ' + escapeHtml("aw-dlp-incidents_" + host) + '' + - '' + - '
Инцидентов: 0
' + - '
' + - '

Здесь выводятся DLP-события, которые оператор вручную признал инцидентами через review-центр.

' + - '' + - '' + - '' + - '
ВремяСтатусКатегорияКомментарийПользовательТипДокумент/канал
Загрузка...
' + - '
'; - heading.parentElement.insertBefore(center, heading.nextSibling); - center.querySelector("[data-aw-ru-refresh-dlp-alerts]").addEventListener("click", function () { - refreshDlpAlertsCenter(center, host); - }); - } - - if (center.getAttribute("data-aw-ru-loaded") !== "1") { - center.setAttribute("data-aw-ru-loaded", "1"); - refreshDlpAlertsCenter(center, host); - } - Array.from(heading.parentElement.children).forEach(function (child) { - if (child === heading || child === center) return; - child.style.display = "none"; - }); - } - - let trendsRedirectInFlight = false; - let settingsHostFetchInFlight = false; - let applyPatchScheduled = false; - let networkPatchesInstalled = false; - let dlpOverlayFailureCount = 0; - let applyPatchInFlight = false; - let observerAttached = false; - let staticPatchRouteKey = ""; - - function getTrendsHostFromSettings(settings) { - if (!settings || typeof settings !== "object") return ""; - const landingpage = typeof settings.landingpage === "string" ? settings.landingpage : ""; - const match = landingpage.match(/\/activity\/([^/]+)/); - const host = match && match[1] ? decodeURIComponent(match[1]) : ""; - return isLikelyClientHost(host) ? host : ""; - } - - function getTrendsPath(hash) { - if (!hash) return ""; - const normalized = hash.startsWith("#") ? hash.slice(1) : hash; - return normalized.split("?")[0]; - } - - function shouldRedirectTrends(hash) { - const path = getTrendsPath(hash); - return path === "/trends" || path === "/trends/"; - } - - function redirectBareTrendsRoute() { - if (trendsRedirectInFlight || !shouldRedirectTrends(window.location.hash)) return; - trendsRedirectInFlight = true; - fetch("/api/0/settings/", { credentials: "same-origin" }) - .then(function (response) { - if (!response.ok) throw new Error("settings-fetch-failed"); - return response.json(); - }) - .then(function (settings) { - window.__awRuPatchSettingsHost = getDlpHostFromSettings(settings); - const host = getTrendsHostFromSettings(settings); - if (!host || !shouldRedirectTrends(window.location.hash)) return; - const target = "#/trends/" + encodeURIComponent(host); - if (window.location.hash !== target) { - window.location.replace(target); - } - }) - .catch(function () {}) - .finally(function () { - trendsRedirectInFlight = false; - }); - } - - function ensureSettingsHost() { - if (window.__awRuPatchSettingsHost || settingsHostFetchInFlight) return; - settingsHostFetchInFlight = true; - fetch("/api/0/settings/", { credentials: "same-origin" }) - .then(function (response) { - if (!response.ok) throw new Error("settings-fetch-failed"); - return response.json(); - }) - .then(function (settings) { - window.__awRuPatchSettingsHost = getDlpHostFromSettings(settings); - }) - .catch(function () {}) - .finally(function () { - settingsHostFetchInFlight = false; - injectDlpNavigation(document.body); - if (isAlertsRoute()) { - scheduleApplyPatch(); - } - }); - } - - function getPreferredWindowHostFromBuckets() { - const state = getHostGroupsState(); - const rawBuckets = state && state.buckets ? state.buckets : {}; - const settingsHost = normalizeText(window.__awRuPatchSettingsHost || ""); - const bucketIds = Array.isArray(rawBuckets) - ? rawBuckets.map(function (item) { return item && item.id ? String(item.id) : ""; }) - : Object.keys(rawBuckets || {}); - const hosts = bucketIds - .filter(function (bucketId) { return /^aw-watcher-window_/i.test(bucketId); }) - .map(function (bucketId) { return bucketId.replace(/^aw-watcher-window_/i, ""); }) - .filter(Boolean) - .filter(function (host) { return !/^unknown$/i.test(host); }); - if (isLikelyClientHost(settingsHost) && hosts.indexOf(settingsHost) >= 0) return settingsHost; - if (isLikelyClientHost(settingsHost) && !hosts.length) return settingsHost; - hosts.sort(); - return hosts[0] || ""; - } - - function rewriteUnknownCategoryBuilderQueryBody(body) { - if (typeof body !== "string") return body; - function stripUnknownBucketTokens(raw) { - return raw - .replace(/aw-watcher-window_unknown/gi, "__AW_RU_UNKNOWN_WINDOW__") - .replace(/aw-watcher-afk_unknown/gi, "__AW_RU_UNKNOWN_AFK__") - .replace(/find_bucket\((\\?["'])__AW_RU_UNKNOWN_WINDOW__(\\?["'])\)/gi, "[]") - .replace(/find_bucket\((\\?["'])__AW_RU_UNKNOWN_AFK__(\\?["'])\)/gi, "[]") - .replace(/query_bucket\((\\?["'])__AW_RU_UNKNOWN_WINDOW__(\\?["'])\)/gi, "[]") - .replace(/query_bucket\((\\?["'])__AW_RU_UNKNOWN_AFK__(\\?["'])\)/gi, "[]") - .replace(/__AW_RU_UNKNOWN_WINDOW__/g, "") - .replace(/__AW_RU_UNKNOWN_AFK__/g, ""); - } - function stripUnknownBucketQueries(raw) { - return raw - .replace(/flood\(query_bucket\(find_bucket\(\\"aw-watcher-window_unknown\\"\)\)\)/g, '[]') - .replace(/flood\(query_bucket\(find_bucket\(\\"aw-watcher-afk_unknown\\"\)\)\)/g, '[]') - .replace(/query_bucket\(find_bucket\(\\"aw-watcher-window_unknown\\"\)\)/g, '[]') - .replace(/query_bucket\(find_bucket\(\\"aw-watcher-afk_unknown\\"\)\)/g, '[]') - .replace(/flood\(query_bucket\(\\"aw-watcher-window_unknown\\"\)\)/g, '[]') - .replace(/flood\(query_bucket\(\\"aw-watcher-afk_unknown\\"\)\)/g, '[]') - .replace(/query_bucket\(\\"aw-watcher-window_unknown\\"\)/g, '[]') - .replace(/query_bucket\(\\"aw-watcher-afk_unknown\\"\)/g, '[]'); - } - if (body.indexOf("undefined") !== -1) { - body = body - .replace(/flood\(query_bucket\(find_bucket\(\\"undefined\\"\)\)\)/g, '[]') - .replace(/query_bucket\(find_bucket\(\\"undefined\\"\)\)/g, '[]') - .replace(/flood\(query_bucket\(\\"undefined\\"\)\)/g, '[]') - .replace(/query_bucket\(\\"undefined\\"\)/g, '[]'); - const ph = getPreferredWindowHostFromBuckets(); - if (ph) { - body = body - .replace(/aw-watcher-window_undefined/g, "aw-watcher-window_" + ph) - .replace(/aw-watcher-afk_undefined/g, "aw-watcher-afk_" + ph); - } - } - if (body.indexOf("aw-watcher-window_unknown") !== -1 || body.indexOf("aw-watcher-afk_unknown") !== -1) { - const preferredHost = getPreferredWindowHostFromBuckets(); - if (preferredHost) { - body = body - .replace(/aw-watcher-window_unknown/g, "aw-watcher-window_" + preferredHost) - .replace(/aw-watcher-afk_unknown/g, "aw-watcher-afk_" + preferredHost); - } else { - body = stripUnknownBucketQueries(body); - } - } - if (body.indexOf("aw-watcher-window_unknown") !== -1 || body.indexOf("aw-watcher-afk_unknown") !== -1) { - body = stripUnknownBucketQueries(body); - body = stripUnknownBucketTokens(body); - } - return body; - } - - function installCategoryBuilderNetworkPatch() { - if (networkPatchesInstalled) return; - networkPatchesInstalled = true; - - const originalFetch = window.fetch; - if (typeof originalFetch === "function" && !originalFetch.__awRuCategoryBuilderPatched) { - const patchedFetch = function (input, init) { - let nextInput = input; - let nextInit = init; - try { - const url = typeof nextInput === "string" ? nextInput : String(nextInput && nextInput.url || ""); - if (/\/api\/0\/query\/?$/i.test(url) && nextInit && typeof nextInit.body === "string") { - nextInit = Object.assign({}, nextInit, { - body: rewriteUnknownCategoryBuilderQueryBody(nextInit.body) - }); - } - } catch (error) { - } - return originalFetch.call(this, nextInput, nextInit); - }; - patchedFetch.__awRuCategoryBuilderPatched = true; - patchedFetch.__awRuOriginalFetch = originalFetch; - window.fetch = patchedFetch; - } - - if (window.XMLHttpRequest && window.XMLHttpRequest.prototype) { - const proto = window.XMLHttpRequest.prototype; - if (!proto.__awRuCategoryBuilderPatched) { - const originalOpen = proto.open; - const originalSend = proto.send; - proto.open = function (method, url) { - this.__awRuMethod = method; - this.__awRuUrl = url; - return originalOpen.apply(this, arguments); - }; - proto.send = function (body) { - try { - const url = String(this.__awRuUrl || ""); - if (/\/api\/0\/query\/?$/i.test(url) && typeof body === "string") { - body = rewriteUnknownCategoryBuilderQueryBody(body); - } - } catch (error) { - } - return originalSend.call(this, body); - }; - proto.__awRuCategoryBuilderPatched = true; - } - } - } - - function patchCategoryBuilderHostLabel(root) { - if (!/^#\/settings\/category-builder(?:[/?#]|$)/i.test(window.location.hash || "")) return; - const preferredHost = getPreferredWindowHostFromBuckets(); - if (!preferredHost) return; - Array.from(root.querySelectorAll("*")).forEach(function (element) { - if (element.children.length) return; - const text = element.textContent || ""; - if (!/Имя хоста:\s*(unknown|неизвестно)\b|Hostname:\s*unknown\b/i.test(text)) return; - const next = text - .replace(/Имя хоста:\s*(unknown|неизвестно)\b/i, "Имя хоста: " + preferredHost) - .replace(/Hostname:\s*unknown\b/i, "Hostname: " + preferredHost); - if (next !== text) { - element.textContent = next; - } - }); - } - - function normalizeCategoryBuilderUnknownHostRefs() { - const hash = window.location.hash || ""; - if (!/^#\/settings\/category-builder(?:[/?#]|$)/i.test(hash)) return; - const preferredHost = getPreferredWindowHostFromBuckets(); - if (!preferredHost) return; - - const nextHash = hash - .replace(/aw-watcher-window_unknown/gi, "aw-watcher-window_" + preferredHost) - .replace(/aw-watcher-afk_unknown/gi, "aw-watcher-afk_" + preferredHost); - if (nextHash !== hash) { - window.location.replace(nextHash); - return; - } - - try { - for (let i = 0; i < window.localStorage.length; i += 1) { - const key = window.localStorage.key(i); - if (!key) continue; - const value = window.localStorage.getItem(key); - if (!value || (value.indexOf("aw-watcher-window_unknown") === -1 && value.indexOf("aw-watcher-afk_unknown") === -1)) continue; - window.localStorage.setItem( - key, - value - .replace(/aw-watcher-window_unknown/gi, "aw-watcher-window_" + preferredHost) - .replace(/aw-watcher-afk_unknown/gi, "aw-watcher-afk_" + preferredHost) - ); - } - } catch (error) { - } - } - - function primeCategoryBuilderEarlyFix() { - const hash = window.location.hash || ""; - if (!/^#\/settings\/category-builder(?:[/?#]|$)/i.test(hash)) return; - ensureSettingsHost(); - ensureHostGroupsData().catch(function () {}); - normalizeCategoryBuilderUnknownHostRefs(); - } - - function patchActivityHeading(root) { - const heading = root.querySelector("h3"); - if (!heading) return; - const inlineParts = heading.querySelectorAll("span"); - inlineParts.forEach(function (element) { - const text = (element.textContent || "").trim(); - if (text === "for") { - element.textContent = "за "; - } - }); - } - - function applyTextAndNavigationPatches(root) { - if (!root) return; - walk(root); - translateAttributes(root); - hideNoiseNavigation(root); - hidePveAuditTabForRegularHost(root); - patchActivityHeading(root); - patchCategoryBuilderHostLabel(root); - } - - function detachObserver() { - if (!observerAttached) return; - observer.disconnect(); - observerAttached = false; - } - - function attachObserver() { - if (observerAttached || !document.body) return; - observer.observe(document.body, { childList: true, subtree: true }); - observerAttached = true; - } - - function applyPatch() { - if (applyPatchInFlight || !document.body) return; - applyPatchInFlight = true; - detachObserver(); - try { - const routeKey = window.location.hash || "#"; - const routeChanged = routeKey !== staticPatchRouteKey; - const dlpRoute = isDlpSignalBucketRoute(); - installCategoryBuilderNetworkPatch(); - injectStyles(); - if (dlpRoute) { - ensureSettingsHost(); - injectDlpNavigation(document.body); - if (dlpOverlayFailureCount === 0) { - try { - injectDlpReviewCenter(document.body); - } catch (error) { - dlpOverlayFailureCount += 1; - const existing = document.body.querySelector("[data-aw-ru-dlp-center='1']"); - if (existing && existing.parentElement) existing.parentElement.removeChild(existing); - } - } - applyTextAndNavigationPatches(document.body); - staticPatchRouteKey = routeKey; - return; - } - enforceSafeActivityViewForPveHost(); - ensureSettingsHost(); - ensureHostGroupsData().catch(function () {}); - normalizeCategoryBuilderUnknownHostRefs(); - applyTextAndNavigationPatches(document.body); - if (routeChanged) { - staticPatchRouteKey = routeKey; - } - injectPveAuditCenter(document.body); - injectRdpWorktimeCenter(document.body).catch(function () {}); - injectDlpNavigation(document.body); - injectDlpReviewCenter(document.body); - injectDlpAlertsCenter(document.body); - injectHostGroupsCenter(document.body).catch(function () {}); - redirectBareTrendsRoute(); - } finally { - applyPatchInFlight = false; - attachObserver(); - } - } - - function scheduleApplyPatch() { - if (applyPatchScheduled || applyPatchInFlight) return; - applyPatchScheduled = true; - window.setTimeout(function () { - applyPatchScheduled = false; - applyPatch(); - }, 50); - } - - const observer = new MutationObserver(function () { - if (applyPatchInFlight) return; - scheduleApplyPatch(); - }); - - installCategoryBuilderNetworkPatch(); - primeCategoryBuilderEarlyFix(); - - window.addEventListener("load", function () { - applyPatch(); - attachObserver(); - }); - window.addEventListener("hashchange", function () { - redirectBareTrendsRoute(); - dlpOverlayFailureCount = 0; - staticPatchRouteKey = ""; - scheduleApplyPatch(); - }); -})(); diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.service b/install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.service deleted file mode 100644 index b5ba629..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=AW-RUS unified health orchestrator -After=network-online.target activitywatch-server.service aw-worktime-api.service -Wants=network-online.target activitywatch-server.service aw-worktime-api.service - -[Service] -Type=oneshot -EnvironmentFile=/etc/activitywatch/aw-server.env -ExecStart=/usr/local/bin/aw-rus-healthd-rust -User=root -Group=root -StandardOutput=journal -StandardError=journal -SyslogIdentifier=aw-rus-healthd diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.timer b/install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.timer deleted file mode 100644 index 08e74a0..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.timer +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Run AW-RUS unified health orchestrator every 2 minutes - -[Timer] -OnCalendar=*:0/2:25 -AccuracySec=15s -RandomizedDelaySec=15s -Unit=aw-rus-healthd.service -Persistent=false - -[Install] -WantedBy=timers.target diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-server.env.example b/install-kit-awindows-20260427-211240/aw-server/aw-server.env.example deleted file mode 100755 index 6e26bad..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-server.env.example +++ /dev/null @@ -1,77 +0,0 @@ -# Copy to /etc/activitywatch/aw-server.env and fill with real values. - -# Core AW Server Configuration -AW_SERVER_VERSION=0.13.2 -AW_SERVER_DOWNLOAD_URL=https://github.com/ActivityWatch/aw-server-rust/releases/download/v0.13.2/aw-server-rust-linux-x86_64.zip -AW_SERVER_BIND_HOST=0.0.0.0 -AW_SERVER_PORT=5600 -AW_SERVER_WEBUI_DIR=/opt/activitywatch/webui-ru -AW_SERVER_DATA_DIR=/var/lib/activitywatch -AW_SERVER_DB_PATH=/var/lib/activitywatch/pebble.db -AW_SERVER_LOG_DIR=/var/log/activitywatch -AW_SERVER_USER=activitywatch -AW_SERVER_GROUP=activitywatch - -# Worktime API Configuration -AW_SERVER_PUBLIC_HOST=aw-server -AW_WORKTIME_REPORT_BASE=http://aw-server:5610 -AW_WORKTIME_TZ=Europe/Moscow -AW_SERVER_URL=http://127.0.0.1:5600 -AW_DLP_AW_API_BASE=http://127.0.0.1:5600/api/0 -AW_WORKTIME_MANAGER_CACHE_TTL_SECONDS=300 -AW_WORKTIME_MANAGER_ALIASES_JSON=/etc/activitywatch/worktime-manager-aliases.json -AW_WORKTIME_MANAGER_EXCLUDE_USERS= -AW_WORKTIME_MANAGEMENT_WARM_ENABLED=1 -AW_WORKTIME_MANAGEMENT_WARM_URL=http://127.0.0.1:5610/reports/worktime/management?day=today&format=json -AW_WORKTIME_MANAGEMENT_WARM_TIMEOUT_SECONDS=70 - -# DLP IOC Configuration -AW_DLP_IOC_DIR=/opt/activitywatch/dlp-ioc/output - -# DLP Policy Engine Configuration -AW_DLP_POLICY_ENGINE_BIND_HOST=0.0.0.0 -AW_DLP_POLICY_ENGINE_PORT=5601 -AW_DLP_POLICY_ENGINE_DB_PATH=/var/lib/activitywatch/dlp-policy-engine.sqlite - -# Logging Configuration -AW_LOG_LEVEL=info -AW_LOG_TO_JOURNAL=true -AW_LOG_TO_FILE=true - -# Health Check Configuration -AW_HEALTH_CHECK_ENABLED=true -AW_HEALTH_CHECK_INTERVAL=60 -AW_EXPECT_START_OF_DAY=00:00 -AW_EXPECT_ALWAYS_ACTIVE_PATTERN=aw-watcher-window -AW_EXPECT_LANDINGPAGE=/#/activity/SHARKON2025/view/ -AW_HEALTH_STRICT_FILEOPS=0 -AW_MONITORED_WINDOWS_HOST= -AW_MONITORED_WINDOWS_HOSTNAME=SHARKON2025 -AW_RUS_HEALTH_WORKTIME_API=http://127.0.0.1:5610 -AW_RUS_HEALTH_STATE_DIR=/var/lib/activitywatch/health -AW_RUS_HEALTH_VALIDATION_DIR=/var/lib/activitywatch/health/windows-validation -AW_RUS_HEALTH_SESSION_EVENTS_MAX_AGE_SECONDS=86400 -AW_RUS_HEALTH_GUARD_MAX_AGE_SECONDS=300 -AW_RUS_HEALTH_GUARD_REQUIRED=1 -AW_RUS_SLO_AW_BASE=http://127.0.0.1:5600 -AW_RUS_SLO_WORKTIME_BASE=http://127.0.0.1:5610 -AW_RUS_SLO_TARGET_PERCENT=99.97 -AW_BROWSER_SMOKE_AW_BASE=http://127.0.0.1:5600 -AW_BROWSER_SMOKE_WORKTIME_BASE=http://127.0.0.1:5610 -AW_BROWSER_SMOKE_HOST=SHARKON2025 -AW_BROWSER_SMOKE_OUTPUT_DIR=/var/lib/activitywatch/browser-smoke -AW_BROWSER_SMOKE_KEEP_RUNS=24 -AW_BROWSER_SMOKE_ENGINE=chromium-cli -AW_BROWSER_SMOKE_TIMEOUT_MS=20000 -AW_BROWSER_SMOKE_RENDER_TIMEOUT_MS=15000 - -# Hayabusa auto-case / alerting -AW_HAYABUSA_AUTO_CASE_ENABLED=true -AW_HAYABUSA_AUTO_CASE_MIN_SEVERITY=medium -AW_HAYABUSA_TELEGRAM_ENABLED=true -AW_HAYABUSA_TELEGRAM_MIN_SEVERITY=high -AW_HAYABUSA_TELEGRAM_BOT_TOKEN= -AW_HAYABUSA_TELEGRAM_CHAT_IDS= - -# Integration Test Configuration -AW_INTEGRATION_TEST_ENABLED=false diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-slo-monitor.service b/install-kit-awindows-20260427-211240/aw-server/aw-slo-monitor.service deleted file mode 100644 index 0fbba5e..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-slo-monitor.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=AW-RUS SLO sampler -After=network-online.target activitywatch-server.service aw-worktime-api.service aw-rus-healthd.service -Wants=network-online.target activitywatch-server.service aw-worktime-api.service - -[Service] -Type=oneshot -EnvironmentFile=/etc/activitywatch/aw-server.env -ExecStart=/usr/local/bin/aw-slo-monitor-rust -SuccessExitStatus=1 -User=activitywatch -Group=activitywatch -Nice=5 -StandardOutput=journal -StandardError=journal -SyslogIdentifier=aw-slo-monitor diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-slo-monitor.timer b/install-kit-awindows-20260427-211240/aw-server/aw-slo-monitor.timer deleted file mode 100644 index b5e2ce3..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-slo-monitor.timer +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Run AW-RUS SLO sampler every minute - -[Timer] -OnCalendar=*:0/1:50 -AccuracySec=10s -RandomizedDelaySec=10s -Persistent=false -Unit=aw-slo-monitor.service - -[Install] -WantedBy=timers.target diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-sw-cleanup.js b/install-kit-awindows-20260427-211240/aw-server/aw-sw-cleanup.js deleted file mode 100755 index b7aa742..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-sw-cleanup.js +++ /dev/null @@ -1,18 +0,0 @@ -self.addEventListener("install", function (event) { - self.skipWaiting(); - event.waitUntil((async function () { - const keys = await caches.keys(); - await Promise.all(keys.map(function (key) { return caches.delete(key); })); - })()); -}); - -self.addEventListener("activate", function (event) { - event.waitUntil((async function () { - const keys = await caches.keys(); - await Promise.all(keys.map(function (key) { return caches.delete(key); })); - await self.clients.claim(); - await self.registration.unregister(); - })()); -}); - -self.addEventListener("fetch", function () {}); diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-worktime-api.service b/install-kit-awindows-20260427-211240/aw-server/aw-worktime-api.service deleted file mode 100644 index 07e32d8..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-worktime-api.service +++ /dev/null @@ -1,21 +0,0 @@ -[Unit] -Description=AW Worktime Report API -After=network.target activitywatch-server.service -Wants=activitywatch-server.service -StartLimitBurst=3 -StartLimitIntervalSec=60 - -[Service] -Type=simple -EnvironmentFile=/etc/activitywatch/aw-server.env -ExecStart=/usr/local/bin/aw-worktime-api-rust -Restart=on-failure -RestartSec=5 -User=activitywatch -Group=activitywatch -StandardOutput=journal -StandardError=journal -SyslogIdentifier=aw-worktime-api - -[Install] -WantedBy=multi-user.target diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-worktime-panel.js b/install-kit-awindows-20260427-211240/aw-server/aw-worktime-panel.js deleted file mode 100644 index 0621520..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-worktime-panel.js +++ /dev/null @@ -1,68 +0,0 @@ -(function () { - var reportBase = "__AW_WORKTIME_REPORT_BASE__"; - function defaultDayQuery() { - return "day=today"; - } - - var dayQuery = defaultDayQuery(); - var htmlUrl = reportBase + "/reports/worktime/today?format=html&" + dayQuery; - var csvUrl = reportBase + "/reports/worktime/today?format=csv&" + dayQuery; - var jsonUrl = reportBase + "/reports/worktime/today?" + dayQuery; - var managerHtmlUrl = reportBase + "/reports/worktime/management?format=html&" + dayQuery; - var managerJsonUrl = reportBase + "/reports/worktime/management?" + dayQuery; - var existing = document.getElementById("aw-report-links"); - if (!existing) return; - - existing.innerHTML = - 'RDP отчёт: ' + - 'HTML | ' + - 'CSV | ' + - 'JSON | ' + - 'Менеджмент | ' + - 'Mgmt JSON | ' + - 'Панель'; - - var panel = document.createElement("div"); - panel.id = "aw-report-panel"; - panel.style.cssText = [ - "position:fixed", - "top:16px", - "right:16px", - "width:min(980px,calc(100vw - 32px))", - "height:min(760px,calc(100vh - 32px))", - "background:#fff", - "border:1px solid rgba(15,23,42,.15)", - "border-radius:12px", - "box-shadow:0 24px 80px rgba(15,23,42,.28)", - "overflow:hidden", - "z-index:100000", - "display:none" - ].join(";"); - - panel.innerHTML = - '
' + - '
Отчёт по работе в RDP
' + - '
" + - ''; - - document.body.appendChild(panel); - - function openPanel(ev) { - if (ev) ev.preventDefault(); - panel.style.display = "block"; - } - - function closePanel(ev) { - if (ev) ev.preventDefault(); - panel.style.display = "none"; - } - - var toggle = document.getElementById("aw-report-toggle"); - if (toggle) toggle.addEventListener("click", openPanel); - var close = panel.querySelector("#aw-report-close"); - if (close) close.addEventListener("click", closePanel); -})(); diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-worktime-prewarm.service b/install-kit-awindows-20260427-211240/aw-server/aw-worktime-prewarm.service deleted file mode 100644 index 6f3cb29..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-worktime-prewarm.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=AW Worktime Report Cache Prewarm -After=network-online.target activitywatch-server.service aw-worktime-api.service -Wants=network-online.target activitywatch-server.service aw-worktime-api.service - -[Service] -Type=oneshot -EnvironmentFile=/etc/activitywatch/aw-server.env -ExecStart=/usr/local/bin/aw-worktime-prewarm-rust -User=activitywatch -Group=activitywatch -StandardOutput=journal -StandardError=journal -SyslogIdentifier=aw-worktime-prewarm diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-worktime-prewarm.timer b/install-kit-awindows-20260427-211240/aw-server/aw-worktime-prewarm.timer deleted file mode 100644 index 5167be5..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/aw-worktime-prewarm.timer +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Run AW Worktime Report Cache Prewarm - -[Timer] -OnCalendar=*:2/5:40 -AccuracySec=30s -Persistent=false -Unit=aw-worktime-prewarm.service - -[Install] -WantedBy=timers.target diff --git a/install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh b/install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh deleted file mode 100755 index cdc1241..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/bin/bash -set -euo pipefail - -ENV_FILE="/etc/activitywatch/aw-server.env" -if [[ ! -f "$ENV_FILE" ]]; then - echo "missing env file: $ENV_FILE" >&2 - exit 1 -fi - -source "$ENV_FILE" - -required_vars=( - AW_SERVER_VERSION - AW_SERVER_DOWNLOAD_URL - AW_SERVER_BIND_HOST - AW_SERVER_PORT - AW_SERVER_WEBUI_DIR - AW_SERVER_DATA_DIR - AW_SERVER_LOG_DIR - AW_SERVER_USER - AW_SERVER_GROUP -) - -BOOTSTRAP_DIR="/root/bootstrap" -VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json" -CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json" -WORKTIME_API_RUST_SRC="$BOOTSTRAP_DIR/worktime-api" -WORKTIME_API_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-api.service" -WORKTIME_ALIASES_SRC="$BOOTSTRAP_DIR/worktime-manager-aliases.example.json" -WORKTIME_UI_BRIDGE_RUST_SRC="$BOOTSTRAP_DIR/worktime-ui-bridge" -WORKTIME_UI_BRIDGE_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.service" -WORKTIME_UI_BRIDGE_TIMER_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.timer" -HEALTHD_RUST_SRC="$BOOTSTRAP_DIR/aw-rus-healthd" -HEALTHD_SERVICE_SRC="$BOOTSTRAP_DIR/aw-rus-healthd.service" -HEALTHD_TIMER_SRC="$BOOTSTRAP_DIR/aw-rus-healthd.timer" - -for var_name in "${required_vars[@]}"; do - if [[ -z "${!var_name:-}" ]]; then - echo "missing required variable: $var_name" >&2 - exit 1 - fi -done - -export DEBIAN_FRONTEND=noninteractive -apt-get update -apt-get install -y curl ca-certificates unzip jq - -if ! getent group "$AW_SERVER_GROUP" >/dev/null; then - groupadd --system "$AW_SERVER_GROUP" -fi - -if ! id "$AW_SERVER_USER" >/dev/null 2>&1; then - useradd --system --gid "$AW_SERVER_GROUP" --home-dir "$AW_SERVER_DATA_DIR" --shell /usr/sbin/nologin "$AW_SERVER_USER" -fi - -install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" /opt/activitywatch/bin -install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" /opt/activitywatch/releases -install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$AW_SERVER_WEBUI_DIR" -install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$AW_SERVER_DATA_DIR" -install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$AW_SERVER_LOG_DIR" -install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$AW_SERVER_DATA_DIR/health/windows-validation" - -tmp_dir=$(mktemp -d) -trap 'rm -rf "$tmp_dir"' EXIT - -curl -fL "$AW_SERVER_DOWNLOAD_URL" -o "$tmp_dir/aw-server.zip" -unzip -q "$tmp_dir/aw-server.zip" -d "$tmp_dir/unpacked" - -server_bin=$(find "$tmp_dir/unpacked" -type f \( -name 'aw-server-rust' -o -name 'aw-server' \) | head -n 1) -webui_dir=$(find "$tmp_dir/unpacked" -type d \( -name 'webui' -o -name 'aw-webui' \) | head -n 1 || true) - -if [[ -z "$server_bin" || ! -f "$server_bin" ]]; then - echo "aw-server binary not found in archive" >&2 - exit 1 -fi - -release_dir="/opt/activitywatch/releases/aw-server-rust-v${AW_SERVER_VERSION}" -rm -rf "$release_dir" -install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$release_dir" -install -m 0755 -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$server_bin" "$release_dir/aw-server-rust" -ln -sfn "$release_dir/aw-server-rust" /opt/activitywatch/bin/aw-server-rust - -if [[ -n "$webui_dir" && -d "$webui_dir" ]]; then - rm -rf "$AW_SERVER_WEBUI_DIR" - mkdir -p "$AW_SERVER_WEBUI_DIR" - cp -a "$webui_dir"/. "$AW_SERVER_WEBUI_DIR"/ - chown -R "$AW_SERVER_USER:$AW_SERVER_GROUP" "$AW_SERVER_WEBUI_DIR" -fi - -sed \ - -e "s#__AW_SERVER_USER__#$AW_SERVER_USER#g" \ - -e "s#__AW_SERVER_GROUP__#$AW_SERVER_GROUP#g" \ - -e "s#__AW_SERVER_DATA_DIR__#$AW_SERVER_DATA_DIR#g" \ - /root/bootstrap/activitywatch-server.service > /etc/systemd/system/activitywatch-server.service -chmod 0644 /etc/systemd/system/activitywatch-server.service - -systemctl daemon-reload -systemctl enable activitywatch-server.service -systemctl restart activitywatch-server.service -systemctl --no-pager --full status activitywatch-server.service || true - -if [[ -f "$WORKTIME_API_RUST_SRC" ]]; then - install -m 0755 "$WORKTIME_API_RUST_SRC" /usr/local/bin/aw-worktime-api-rust -fi - -if [[ -f "$WORKTIME_API_SERVICE_SRC" && -f /usr/local/bin/aw-worktime-api-rust ]]; then - install -m 0644 "$WORKTIME_API_SERVICE_SRC" /etc/systemd/system/aw-worktime-api.service - systemctl daemon-reload - systemctl enable aw-worktime-api.service - systemctl restart aw-worktime-api.service - systemctl --no-pager --full status aw-worktime-api.service || true -fi - -if [[ -f "$WORKTIME_ALIASES_SRC" ]]; then - install -d -m 0755 /etc/activitywatch - install -m 0644 "$WORKTIME_ALIASES_SRC" /etc/activitywatch/worktime-manager-aliases.json.example - if [[ ! -f /etc/activitywatch/worktime-manager-aliases.json ]]; then - install -m 0644 "$WORKTIME_ALIASES_SRC" /etc/activitywatch/worktime-manager-aliases.json - fi -fi - -if [[ -f "$WORKTIME_UI_BRIDGE_RUST_SRC" ]]; then - install -m 0755 "$WORKTIME_UI_BRIDGE_RUST_SRC" /usr/local/bin/aw-worktime-ui-bridge-rust -fi - -if [[ -f "$WORKTIME_UI_BRIDGE_SERVICE_SRC" && -f /usr/local/bin/aw-worktime-ui-bridge-rust ]]; then - install -m 0644 "$WORKTIME_UI_BRIDGE_SERVICE_SRC" /etc/systemd/system/aw-worktime-ui-bridge.service -fi - -if [[ -f "$WORKTIME_UI_BRIDGE_TIMER_SRC" && -f /usr/local/bin/aw-worktime-ui-bridge-rust ]]; then - install -m 0644 "$WORKTIME_UI_BRIDGE_TIMER_SRC" /etc/systemd/system/aw-worktime-ui-bridge.timer - systemctl daemon-reload - systemctl disable --now aw-worktime-afk-bridge.timer >/dev/null 2>&1 || true - systemctl enable aw-worktime-ui-bridge.timer - systemctl restart aw-worktime-ui-bridge.timer - systemctl start aw-worktime-ui-bridge.service || true - systemctl --no-pager --full status aw-worktime-ui-bridge.timer || true -fi - -if [[ -f "$HEALTHD_RUST_SRC" ]]; then - install -m 0755 "$HEALTHD_RUST_SRC" /usr/local/bin/aw-rus-healthd-rust -fi - -if [[ -f "$HEALTHD_SERVICE_SRC" && -f /usr/local/bin/aw-rus-healthd-rust ]]; then - install -m 0644 "$HEALTHD_SERVICE_SRC" /etc/systemd/system/aw-rus-healthd.service -fi - -if [[ -f "$HEALTHD_TIMER_SRC" && -f /usr/local/bin/aw-rus-healthd-rust ]]; then - install -m 0644 "$HEALTHD_TIMER_SRC" /etc/systemd/system/aw-rus-healthd.timer - systemctl daemon-reload - systemctl enable aw-rus-healthd.timer - systemctl restart aw-rus-healthd.timer - systemctl start aw-rus-healthd.service || true - systemctl --no-pager --full status aw-rus-healthd.timer || true -fi - -for _ in $(seq 1 20); do - if curl -fsS "http://127.0.0.1:${AW_SERVER_PORT}/api/0/info" >/dev/null 2>&1; then - break - fi - sleep 2 -done - -if [[ -f "$CLASSES_JSON" ]]; then - curl -fsS -X POST \ - -H 'Content-Type: application/json' \ - --data-binary @"$CLASSES_JSON" \ - "http://127.0.0.1:${AW_SERVER_PORT}/api/0/settings/classes" >/dev/null - echo "Applied worktime classes from $CLASSES_JSON" -else - echo "Worktime classes bootstrap not found, skipped: $CLASSES_JSON" -fi - -if [[ -f "$VIEWS_JSON" ]]; then - curl -fsS -X POST \ - -H 'Content-Type: application/json' \ - --data-binary @"$VIEWS_JSON" \ - "http://127.0.0.1:${AW_SERVER_PORT}/api/0/settings/views" >/dev/null - echo "Applied baseline views from $VIEWS_JSON" -else - echo "Views bootstrap not found, skipped: $VIEWS_JSON" -fi diff --git a/install-kit-awindows-20260427-211240/aw-server/settings/classes-worktime.json b/install-kit-awindows-20260427-211240/aw-server/settings/classes-worktime.json deleted file mode 100644 index 11ee810..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/settings/classes-worktime.json +++ /dev/null @@ -1,90 +0,0 @@ -[ - { - "id": 0, - "name": ["Работа"], - "rule": { "type": "none" }, - "data": {} - }, - { - "id": 1, - "name": ["Работа", "1С"], - "rule": { - "type": "regex", - "regex": "\\b(1cv8s?|1cv8c|1cestart)\\.exe\\b|1С:Предприятие|Запуск 1С:Предприятия|Загрузка конфигурационной информации|Доступ к информационной базе", - "ignore_case": true - }, - "data": { "color": "#194D33" } - }, - { - "id": 2, - "name": ["Работа", "Документы"], - "rule": { - "type": "regex", - "regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64|libreoffice|writer|calc)\\.exe\\b|LibreOffice|OnlyOffice|Adobe Reader|Acrobat", - "ignore_case": true - }, - "data": { "color": "#2E7D32" } - }, - { - "id": 3, - "name": ["Работа", "Коммуникации"], - "rule": { - "type": "regex", - "regex": "\\b(teams|telegram|slack|thunderbird|zoom|skype|whatsapp|viber|discord)\\.exe\\b|Mattermost|Element|Riot", - "ignore_case": true - }, - "data": { "color": "#1E88E5" } - }, - { - "id": 4, - "name": ["Работа", "Администрирование"], - "rule": { - "type": "regex", - "regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd|gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\.exe\\b|\\b(gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\b|Proxmox Virtual Environment|\\bpfSense\\b|\\bGrafana\\b|\\bKibana\\b|\\bPortainer\\b", - "ignore_case": true - }, - "data": { "color": "#6D4C41" } - }, - { - "id": 5, - "name": ["Интернет"], - "rule": { "type": "none" }, - "data": {} - }, - { - "id": 6, - "name": ["Интернет", "Браузер"], - "rule": { - "type": "regex", - "regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser|chromium)\\.exe\\b|\\b(chrome|chromium|firefox|opera|brave|vivaldi)\\b", - "ignore_case": true - }, - "data": { "color": "#00897B" } - }, - { - "id": 7, - "name": ["Система"], - "rule": { "type": "none" }, - "data": {} - }, - { - "id": 8, - "name": ["Система", "Windows"], - "rule": { - "type": "regex", - "regex": "\\b(SearchHost|explorer|ShellExperienceHost|ApplicationFrameHost|RuntimeBroker|sihost|dwm|svchost|fontdrvhost|userinit)\\.exe\\b|\\\\Windows\\\\System32", - "ignore_case": true - }, - "data": { "color": "#607D8B" } - }, - { - "id": 9, - "name": ["ActivityWatch"], - "rule": { - "type": "regex", - "regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b|\\baw-(watcher|qt)\\b", - "ignore_case": true - }, - "data": {} - } -] diff --git a/install-kit-awindows-20260427-211240/aw-server/settings/views-default.json b/install-kit-awindows-20260427-211240/aw-server/settings/views-default.json deleted file mode 100644 index 5698738..0000000 --- a/install-kit-awindows-20260427-211240/aw-server/settings/views-default.json +++ /dev/null @@ -1,29 +0,0 @@ -[ - { - "id": "summary", - "name": "Summary", - "elements": [ - { "type": "top_titles", "size": 3 }, - { "type": "timeline_barchart", "size": 3 }, - { "type": "top_categories", "size": 3 }, - { "type": "category_tree", "size": 3 } - ] - }, - { - "id": "window", - "name": "Window", - "elements": [ - { "type": "top_apps", "size": 3, "props": {} } - ] - }, - { - "id": "worktime", - "name": "Worktime", - "elements": [ - { "type": "top_categories", "size": 3, "props": {} }, - { "type": "timeline_barchart", "size": 3, "props": {} }, - { "type": "category_tree", "size": 3, "props": {} }, - { "type": "top_apps", "size": 3, "props": {} } - ] - } -] diff --git a/install-kit-awindows-20260427-211240/scripts/aw-webui-browser-smoke.mjs b/install-kit-awindows-20260427-211240/scripts/aw-webui-browser-smoke.mjs deleted file mode 100644 index 0f97304..0000000 --- a/install-kit-awindows-20260427-211240/scripts/aw-webui-browser-smoke.mjs +++ /dev/null @@ -1,499 +0,0 @@ -#!/usr/bin/env node -import { createRequire } from "node:module"; -import { execFile } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; -import process from "node:process"; -import { promisify } from "node:util"; - -const require = createRequire(import.meta.url); -const execFileAsync = promisify(execFile); -const homeDir = process.env.HOME || ""; - -function loadPlaywright() { - const candidates = [ - "playwright", - "playwright-core", - process.env.PLAYWRIGHT_CORE_MODULE, - homeDir ? path.join(homeDir, ".agents/skills/playwright/node_modules/playwright-core") : "", - ].filter(Boolean); - const errors = []; - for (const candidate of candidates) { - try { - return require(candidate); - } catch (error) { - errors.push(`${candidate}: ${error.message}`); - } - } - return null; -} - -function firstExisting(candidates) { - return candidates.find((item) => item && fs.existsSync(item)) || ""; -} - -function env(name, fallback) { - const value = process.env[name]; - return value && value.trim() ? value.trim() : fallback; -} - -function normalizeBase(url) { - return url.replace(/\/+$/, ""); -} - -function safeName(value) { - return value.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-|-$/g, ""); -} - -function envInt(name, fallback) { - const value = Number(env(name, String(fallback))); - return Number.isFinite(value) ? value : fallback; -} - -function isRunDirectoryName(name) { - return /^20\d{2}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/.test(name); -} - -function pruneOutputRuns(outRoot, keepRuns) { - if (keepRuns <= 0 || !fs.existsSync(outRoot)) { - return []; - } - const entries = fs - .readdirSync(outRoot, { withFileTypes: true }) - .filter((entry) => entry.isDirectory() && isRunDirectoryName(entry.name)) - .map((entry) => entry.name) - .sort(); - const stale = entries.slice(0, Math.max(0, entries.length - keepRuns)); - for (const name of stale) { - fs.rmSync(path.join(outRoot, name), { recursive: true, force: true }); - } - return stale; -} - -function decodeHtmlEntities(value) { - return value - .replace(/ /g, " ") - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, '"') - .replace(/'/g, "'"); -} - -function htmlToText(html) { - return decodeHtmlEntities( - html - .replace(//gi, " ") - .replace(//gi, " ") - .replace(/<[^>]+>/g, " ") - .replace(/\s+/g, " ") - .trim(), - ); -} - -function commandInPath(name) { - const dirs = (process.env.PATH || "").split(path.delimiter); - for (const dir of dirs) { - const candidate = path.join(dir, name); - if (fs.existsSync(candidate)) { - return candidate; - } - } - return ""; -} - -function findChromiumExecutable(explicitPath) { - return firstExisting([ - explicitPath, - homeDir ? path.join(homeDir, ".cache/ms-playwright/chromium-1217/chrome-linux64/chrome") : "", - homeDir ? path.join(homeDir, ".cache/ms-playwright/chromium-1208/chrome-linux64/chrome") : "", - homeDir ? path.join(homeDir, ".cache/rod/browser/chromium-1321438/chrome") : "", - commandInPath("chromium"), - commandInPath("chromium-browser"), - commandInPath("google-chrome"), - "/usr/bin/chromium", - "/usr/bin/chromium-browser", - "/usr/bin/google-chrome", - ]); -} - -function isBenignConsoleError(text) { - return text.includes("Failed to load resource: the server responded with a status of 404 (Not Found)"); -} - -function isBenignRequestFailure(failure) { - return failure.error === "net::ERR_ABORTED"; -} - -async function waitForTextMarkers(page, markers, timeoutMs) { - if (!markers.length) { - return; - } - try { - await page.waitForFunction( - (expected) => { - const text = `${document.title}\n${document.body ? document.body.innerText : ""}`; - return expected.every((marker) => text.includes(marker)); - }, - markers, - { timeout: timeoutMs }, - ); - } catch { - // The final assertion below reports the exact missing markers. - } -} - -async function runPageCheck(browser, spec, runDir) { - const page = await browser.newPage({ - viewport: { width: 1366, height: 768 }, - locale: "ru-RU", - timezoneId: "Europe/Moscow", - }); - const started = Date.now(); - const consoleErrors = []; - const pageErrors = []; - const requestFailures = []; - const badResponses = []; - const responses = []; - - page.on("console", (message) => { - if (message.type() === "error") { - const text = message.text(); - if (!isBenignConsoleError(text)) { - consoleErrors.push(text); - } - } - }); - page.on("pageerror", (error) => { - pageErrors.push(error.message); - }); - page.on("requestfailed", (request) => { - const url = request.url(); - if (spec.relevantHosts.some((host) => url.startsWith(host))) { - const failure = { url, error: request.failure()?.errorText || "unknown" }; - if (!isBenignRequestFailure(failure)) { - requestFailures.push(failure); - } - } - }); - page.on("response", (response) => { - const url = response.url(); - if (!spec.relevantHosts.some((host) => url.startsWith(host))) { - return; - } - const item = { url, status: response.status() }; - responses.push(item); - if (response.status() >= 400) { - badResponses.push(item); - } - }); - - let status = null; - let title = ""; - let bodyText = ""; - let screenshot = ""; - let error = ""; - try { - const response = await page.goto(spec.url, { waitUntil: "commit", timeout: spec.timeoutMs }); - status = response ? response.status() : null; - await waitForTextMarkers(page, spec.requiredText, spec.renderTimeoutMs); - await page.waitForTimeout(spec.settleMs); - title = await page.title(); - bodyText = await page.locator("body").innerText({ timeout: 5000 }); - screenshot = path.join(runDir, `${safeName(spec.name)}.png`); - await page.screenshot({ path: screenshot, fullPage: true }); - } catch (caught) { - error = caught && caught.message ? caught.message : String(caught); - try { - screenshot = path.join(runDir, `${safeName(spec.name)}-failed.png`); - await page.screenshot({ path: screenshot, fullPage: true }); - } catch { - screenshot = ""; - } - } finally { - await page.close(); - } - - const visibleText = `${title}\n${bodyText}`; - const missingMarkers = spec.requiredText.filter((marker) => !visibleText.includes(marker)); - const ok = - !error && - status !== null && - status >= 200 && - status < 300 && - bodyText.length >= spec.minBodyText && - missingMarkers.length === 0 && - consoleErrors.length === 0 && - pageErrors.length === 0 && - requestFailures.length === 0 && - badResponses.length === 0; - - return { - engine: "playwright", - name: spec.name, - ok, - url: spec.url, - status, - title, - body_text_length: bodyText.length, - missing_markers: missingMarkers, - console_errors: consoleErrors, - page_errors: pageErrors, - request_failures: requestFailures, - bad_responses: badResponses, - response_count: responses.length, - latency_ms: Date.now() - started, - screenshot, - error, - }; -} - -async function runChromiumCliPageCheck(chromiumPath, spec, runDir) { - const started = Date.now(); - const screenshot = path.join(runDir, `${safeName(spec.name)}.png`); - const profileDir = path.join(runDir, `${safeName(spec.name)}-profile`); - const screenshotRequired = spec.cliScreenshotRequired !== false; - fs.mkdirSync(profileDir, { recursive: true }); - const browserArgs = [ - "--headless", - "--no-sandbox", - "--noerrdialogs", - "--disable-gpu", - "--disable-crash-reporter", - "--disable-crashpad", - "--disable-dev-shm-usage", - `--user-data-dir=${profileDir}`, - `--virtual-time-budget=${spec.renderTimeoutMs + spec.settleMs}`, - ]; - - let status = null; - let error = ""; - let fetchedText = ""; - try { - const controller = AbortSignal.timeout(spec.timeoutMs); - const response = await fetch(spec.url, { signal: controller }); - status = response.status; - fetchedText = await response.text(); - } catch (caught) { - error = caught && caught.message ? caught.message : String(caught); - } - - let html = ""; - try { - const { stdout } = await execFileAsync(chromiumPath, [...browserArgs, "--dump-dom", spec.url], { - timeout: spec.timeoutMs + spec.renderTimeoutMs + spec.settleMs + 10000, - maxBuffer: 10 * 1024 * 1024, - }); - html = stdout || ""; - } catch (caught) { - const message = caught && caught.message ? caught.message : String(caught); - error = error ? `${error}; ${message}` : message; - } - - if (screenshotRequired) { - try { - await execFileAsync( - chromiumPath, - [...browserArgs, "--window-size=1366,768", `--screenshot=${screenshot}`, spec.url], - { - timeout: spec.timeoutMs + spec.renderTimeoutMs + spec.settleMs + 10000, - maxBuffer: 1024 * 1024, - }, - ); - } catch (caught) { - const message = caught && caught.message ? caught.message : String(caught); - error = error ? `${error}; screenshot: ${message}` : `screenshot: ${message}`; - } - } - - const rawHtml = html || fetchedText; - const titleMatch = rawHtml.match(/]*>([\s\S]*?)<\/title>/i); - const title = titleMatch ? decodeHtmlEntities(titleMatch[1].trim()) : ""; - const bodyText = htmlToText(rawHtml); - const visibleText = `${title}\n${bodyText}\n${rawHtml}`; - const requiredText = spec.cliRequiredText || spec.requiredText; - const minBodyText = spec.cliMinBodyText || spec.minBodyText; - const missingMarkers = requiredText.filter((marker) => !visibleText.includes(marker)); - const ok = - !error && - status !== null && - status >= 200 && - status < 300 && - bodyText.length >= minBodyText && - missingMarkers.length === 0 && - (!screenshotRequired || fs.existsSync(screenshot)); - - return { - engine: "chromium-cli", - name: spec.name, - ok, - url: spec.url, - status, - title, - body_text_length: bodyText.length, - missing_markers: missingMarkers, - console_errors: [], - page_errors: [], - request_failures: [], - bad_responses: [], - response_count: 0, - latency_ms: Date.now() - started, - screenshot_required: screenshotRequired, - screenshot: fs.existsSync(screenshot) ? screenshot : "", - error, - }; -} - -async function runPageCheckWithRetries(runOnce, spec, maxRetries) { - let result = await runOnce(spec); - for (let attempt = 1; !result.ok && attempt <= maxRetries; attempt += 1) { - await new Promise((resolve) => setTimeout(resolve, 1000 * attempt)); - const retry = await runOnce({ ...spec, name: `${spec.name}_retry${attempt}` }); - retry.retry_of = spec.name; - retry.retry_attempt = attempt; - if (retry.ok) { - retry.name = spec.name; - retry.recovered_after_retry = attempt; - return retry; - } - result = retry; - result.retry_of = spec.name; - result.retry_attempt = attempt; - } - return result; -} - -async function main() { - const requestedEngine = env("AW_BROWSER_SMOKE_ENGINE", "auto"); - const playwright = requestedEngine === "chromium-cli" ? null : loadPlaywright(); - const awBase = normalizeBase(env("AW_BROWSER_SMOKE_AW_BASE", env("AW_SMOKE_AW_SERVER", "http://127.0.0.1:5600"))); - const worktimeBase = normalizeBase(env("AW_BROWSER_SMOKE_WORKTIME_BASE", env("AW_SMOKE_WORKTIME_API", "http://127.0.0.1:5610"))); - const host = env("AW_BROWSER_SMOKE_HOST", env("AW_SMOKE_SOURCE_HOSTNAME", "SHARKON2025")); - const timeoutMs = Number(env("AW_BROWSER_SMOKE_TIMEOUT_MS", "20000")); - const settleMs = Number(env("AW_BROWSER_SMOKE_SETTLE_MS", "6000")); - const renderTimeoutMs = Number(env("AW_BROWSER_SMOKE_RENDER_TIMEOUT_MS", "15000")); - const pageRetries = Number(env("AW_BROWSER_SMOKE_PAGE_RETRIES", "1")); - const keepRuns = envInt("AW_BROWSER_SMOKE_KEEP_RUNS", 24); - const outRoot = env("AW_BROWSER_SMOKE_OUTPUT_DIR", path.resolve("output", "browser-smoke")); - const runId = new Date().toISOString().replace(/[:.]/g, "-"); - const runDir = path.join(outRoot, runId); - fs.mkdirSync(outRoot, { recursive: true }); - const prunedRuns = pruneOutputRuns(outRoot, keepRuns); - fs.mkdirSync(runDir, { recursive: true }); - - const executablePath = findChromiumExecutable(env( - "PLAYWRIGHT_CHROMIUM_EXECUTABLE", - "", - )); - - const launchOptions = { - headless: true, - args: ["--no-sandbox", "--disable-dev-shm-usage"], - }; - if (executablePath) { - launchOptions.executablePath = executablePath; - } - - const relevantHosts = [awBase, worktimeBase]; - const specs = [ - { - name: "aw_webui_home", - url: `${awBase}/`, - relevantHosts, - requiredText: ["Активность", "Windows RDP", host, "DLP"], - cliRequiredText: ["ActivityWatch", "ru-patch-v5.js", "aw-report-links"], - cliMinBodyText: 100, - cliScreenshotRequired: false, - minBodyText: 500, - timeoutMs, - settleMs, - renderTimeoutMs, - }, - { - name: "worktime_today_html", - url: `${worktimeBase}/reports/worktime/today?format=html&day=today&host=${encodeURIComponent(host)}&allow_stale=1`, - relevantHosts, - requiredText: ["AW-rus", "Отчёт", "RDP"], - minBodyText: 500, - timeoutMs, - settleMs: 1000, - renderTimeoutMs, - }, - { - name: "worktime_management_html", - url: `${worktimeBase}/reports/worktime/management?format=html&day=today&host=${encodeURIComponent(host)}&allow_stale=1`, - relevantHosts, - requiredText: ["AW-rus", "Управленческий", "RDP"], - minBodyText: 500, - timeoutMs, - settleMs: 1000, - renderTimeoutMs, - }, - ]; - - const pages = []; - if (playwright) { - const { chromium } = playwright; - const browser = await chromium.launch(launchOptions); - try { - for (const spec of specs) { - pages.push(await runPageCheckWithRetries((item) => runPageCheck(browser, item, runDir), spec, pageRetries)); - } - } finally { - await browser.close(); - } - } else { - if (requestedEngine === "playwright") { - throw new Error("AW_BROWSER_SMOKE_ENGINE=playwright requested, but Playwright could not be loaded"); - } - if (!executablePath) { - throw new Error("Unable to load Playwright and no Chromium executable found"); - } - for (const spec of specs) { - pages.push(await runPageCheckWithRetries((item) => runChromiumCliPageCheck(executablePath, item, runDir), spec, pageRetries)); - } - } - - const ok = pages.every((page) => page.ok); - const result = { - ok, - engine: pages[0]?.engine || "unknown", - generated_at_utc: new Date().toISOString(), - aw_base: awBase, - worktime_base: worktimeBase, - host, - output_dir: runDir, - retention: { - keep_runs: keepRuns, - pruned_runs: prunedRuns, - }, - pages, - }; - const jsonPath = path.join(runDir, "result.json"); - const latestPath = path.join(outRoot, "latest-result.json"); - result.result_json = jsonPath; - result.latest_result_json = latestPath; - fs.writeFileSync(jsonPath, `${JSON.stringify(result, null, 2)}\n`, "utf8"); - fs.writeFileSync(latestPath, `${JSON.stringify(result, null, 2)}\n`, "utf8"); - process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); - return ok ? 0 : 2; -} - -main() - .then((code) => { - process.exitCode = code; - }) - .catch((error) => { - process.stdout.write( - `${JSON.stringify( - { - ok: false, - generated_at_utc: new Date().toISOString(), - error: error && error.message ? error.message : String(error), - }, - null, - 2, - )}\n`, - ); - process.exitCode = 2; - }); diff --git a/install-kit-awindows-20260427-211240/scripts/aw-webui-browser-smoke.sh b/install-kit-awindows-20260427-211240/scripts/aw-webui-browser-smoke.sh deleted file mode 100644 index 17177df..0000000 --- a/install-kit-awindows-20260427-211240/scripts/aw-webui-browser-smoke.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - -rust_candidates=( - "${AW_BROWSER_SMOKE_RUST:-}" - "${CARGO_TARGET_DIR:-}/release/aw-browser-smoke" - "$ROOT_DIR/adk-rust/target/release/aw-browser-smoke" - "/usr/local/bin/aw-browser-smoke" -) - -for rust_bin in "${rust_candidates[@]}"; do - if [[ -n "$rust_bin" && -x "$rust_bin" ]]; then - exec "$rust_bin" --root "$ROOT_DIR" -- "$@" - fi -done - -if [[ -z "${NODE_PATH:-}" && -d "$HOME/.agents/skills/playwright/node_modules" ]]; then - export NODE_PATH="$HOME/.agents/skills/playwright/node_modules" -fi - -exec node "$ROOT_DIR/scripts/aw-webui-browser-smoke.mjs" "$@" diff --git a/install-kit-awindows-20260427-211240/scripts/check_install_kit_vs_repo.sh b/install-kit-awindows-20260427-211240/scripts/check_install_kit_vs_repo.sh deleted file mode 100644 index a6db8d7..0000000 --- a/install-kit-awindows-20260427-211240/scripts/check_install_kit_vs_repo.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT_DIR" - -KIT_DIR="install-kit-awindows-20260427-211240" -TARGET_ROOT="${CARGO_TARGET_DIR:-$ROOT_DIR/adk-rust/target}" -RUST_BIN="${CHECK_INSTALL_KIT_VS_REPO_RUST:-}" - -rust_candidates=() -if [[ -n "$RUST_BIN" ]]; then - rust_candidates+=("$RUST_BIN") -fi -rust_candidates+=( - "$TARGET_ROOT/release/check-install-kit-vs-repo" - "$ROOT_DIR/adk-rust/target/release/check-install-kit-vs-repo" - "/usr/local/bin/check-install-kit-vs-repo" -) - -for candidate in "${rust_candidates[@]}"; do - if [[ -x "$candidate" ]]; then - exec "$candidate" --root "$ROOT_DIR" --kit-dir "$KIT_DIR" "$@" - fi -done - -echo "ERROR: Rust checker not found. Build it with: cd '$ROOT_DIR/adk-rust' && cargo build --release -p check-install-kit-vs-repo" >&2 -exit 127 diff --git a/install-kit-awindows-20260427-211240/scripts/quality-gate.sh b/install-kit-awindows-20260427-211240/scripts/quality-gate.sh deleted file mode 100644 index 6a434a6..0000000 --- a/install-kit-awindows-20260427-211240/scripts/quality-gate.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT_DIR" - -TARGET_ROOT="${CARGO_TARGET_DIR:-$ROOT_DIR/adk-rust/target}" -RUST_BIN="${QUALITY_GATE_RUST:-}" - -rust_candidates=() -if [[ -n "$RUST_BIN" ]]; then - rust_candidates+=("$RUST_BIN") -fi -rust_candidates+=( - "$TARGET_ROOT/release/quality-gate" - "$ROOT_DIR/adk-rust/target/release/quality-gate" - "/usr/local/bin/quality-gate" -) - -for candidate in "${rust_candidates[@]}"; do - if [[ -x "$candidate" ]]; then - exec "$candidate" --root "$ROOT_DIR" "$@" - fi -done - -echo "[1/6] Bash syntax check" -find aw-server proxmox scripts -type f -name "*.sh" -print0 | xargs -0 -r -n1 bash -n - -echo "[2/6] Shellcheck (if available)" -if command -v shellcheck >/dev/null 2>&1; then - { - find aw-server proxmox -type f -name "*.sh" - printf '%s\n' scripts/aw-webui-browser-smoke.sh - } | xargs -r shellcheck -e SC1007,SC1090,SC2016 -else - echo "shellcheck not found, skipping." -fi - -echo "[3/6] Node syntax check (if node available)" -if command -v node >/dev/null 2>&1; then - node --check scripts/aw-webui-browser-smoke.mjs >/dev/null -else - echo "node not found, skipping." -fi - -echo "[4/6] PowerShell parse check (if pwsh available)" -if command -v pwsh >/dev/null 2>&1; then - pwsh -NoLogo -NoProfile -Command ' - $ErrorActionPreference = "Stop" - Get-ChildItem windows -Filter *.ps1 | ForEach-Object { - [void][System.Management.Automation.Language.Parser]::ParseFile($_.FullName,[ref]$null,[ref]$null) - } - [void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path "windows/ActivityWatch.Windows.Common.psm1"),[ref]$null,[ref]$null) - [void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path "windows/ActivityWatch.Windows.Common.psd1"),[ref]$null,[ref]$null) - ' - pwsh -NoLogo -NoProfile -File windows/aw-collector-guard.ps1 -SelfTest >/dev/null -else - echo "pwsh not found, skipping." -fi - - - -echo "[5/6] Ansible syntax check (if ansible-playbook available)" -if command -v ansible-playbook >/dev/null 2>&1; then - for playbook in ansible/*.yml; do - ansible-playbook --syntax-check "$playbook" -i ansible/inventory.example.ini >/dev/null - done -else - echo "ansible-playbook not found, skipping." -fi - -echo "[6/6] DetMir Python runtime retirement guard" -if command -v git >/dev/null 2>&1; then - mapfile -t tracked_py < <(git ls-files '*.py') -else - mapfile -t tracked_py < <(find aw-server proxmox scripts ansible -type f -name '*.py' 2>/dev/null) -fi -violations=() -for path in "${tracked_py[@]}"; do - case "$path" in - aw-server/dlp-content-analysis/*|clickhouse-1c/ai/*|clickhouse-1c/etl/*|detmir-mcp/main.py|grafana-1c/*|pfsense/*|proxmox/tsj_guardian_bot.py|proxmox/test_tsj_guardian_bot.py) - continue - ;; - esac - case "$path" in - aw-server/*|proxmox/*|scripts/*|ansible/*) - violations+=("$path") - ;; - esac -done -if (( ${#violations[@]} > 0 )); then - printf 'Python runtime regression in Rust-retired DetMir paths:\\n' >&2 - printf '%s\\n' "${violations[@]}" >&2 - exit 1 -fi - -echo "quality-gate: OK" diff --git a/install-kit-awindows-20260427-211240/scripts/rebuild_install_kit.sh b/install-kit-awindows-20260427-211240/scripts/rebuild_install_kit.sh deleted file mode 100644 index 001c39e..0000000 --- a/install-kit-awindows-20260427-211240/scripts/rebuild_install_kit.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT_DIR" - -TARGET_ROOT="${CARGO_TARGET_DIR:-$ROOT_DIR/adk-rust/target}" -RUST_BIN="${REBUILD_INSTALL_KIT_RUST:-}" - -rust_candidates=() -if [[ -n "$RUST_BIN" ]]; then - rust_candidates+=("$RUST_BIN") -fi -rust_candidates+=( - "$TARGET_ROOT/release/rebuild-install-kit" - "$ROOT_DIR/adk-rust/target/release/rebuild-install-kit" - "/usr/local/bin/rebuild-install-kit" -) - -for candidate in "${rust_candidates[@]}"; do - if [[ -x "$candidate" ]]; then - exec "$candidate" --root "$ROOT_DIR" "$@" - fi -done - -echo "ERROR: Rust install-kit builder not found. Build it with: cd '$ROOT_DIR/adk-rust' && cargo build --release -p rebuild-install-kit" >&2 -exit 127 diff --git a/install-kit-awindows-20260427-211240/scripts/validate_install_kit.sh b/install-kit-awindows-20260427-211240/scripts/validate_install_kit.sh deleted file mode 100644 index c209127..0000000 --- a/install-kit-awindows-20260427-211240/scripts/validate_install_kit.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT_DIR" - -KIT_DIR="install-kit-awindows-20260427-211240" -MANIFEST="$KIT_DIR/MANIFEST.txt" -ZIP_ARCHIVE="install-kit-awindows-20260427-211240.zip" -TAR_ARCHIVE="install-kit-awindows-20260427-211240.tar.gz" -TARGET_ROOT="${CARGO_TARGET_DIR:-$ROOT_DIR/adk-rust/target}" -RUST_BIN="${VALIDATE_INSTALL_KIT_RUST:-}" - -rust_candidates=() -if [[ -n "$RUST_BIN" ]]; then - rust_candidates+=("$RUST_BIN") -fi -rust_candidates+=( - "$TARGET_ROOT/release/validate-install-kit" - "$ROOT_DIR/adk-rust/target/release/validate-install-kit" - "/usr/local/bin/validate-install-kit" -) - -for candidate in "${rust_candidates[@]}"; do - if [[ -x "$candidate" ]]; then - exec "$candidate" \ - --root "$ROOT_DIR" \ - --kit-dir "$KIT_DIR" \ - --zip-archive "$ZIP_ARCHIVE" \ - --tar-archive "$TAR_ARCHIVE" \ - "$@" - fi -done - -echo "ERROR: Rust validator not found. Build it with: cd '$ROOT_DIR/adk-rust' && cargo build --release -p validate-install-kit" >&2 -exit 127 diff --git a/install-kit-awindows-20260427-211240/scripts/verify_innosetup_installer.sh b/install-kit-awindows-20260427-211240/scripts/verify_innosetup_installer.sh deleted file mode 100644 index fb89657..0000000 --- a/install-kit-awindows-20260427-211240/scripts/verify_innosetup_installer.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT_DIR" - -INSTALLER="${1:-windows/installkit/innosetup/AWatch-rus-InstallKit.exe}" -WINEPREFIX_VERIFY="${WINEPREFIX_VERIFY:-/tmp/aw-inno-verify-wineprefix}" -TARGET_ROOT="${CARGO_TARGET_DIR:-$ROOT_DIR/adk-rust/target}" -RUST_BIN="${VERIFY_INNOSETUP_INSTALLER_RUST:-}" -INSTALL_DIR_WIN='C:\AWatchRusExtract' -INSTALL_DIR_UNIX="${WINEPREFIX_VERIFY}/drive_c/AWatchRusExtract" - -rust_candidates=() -if [[ -n "$RUST_BIN" ]]; then - rust_candidates+=("$RUST_BIN") -fi -rust_candidates+=( - "$TARGET_ROOT/release/verify-innosetup-installer" - "$ROOT_DIR/adk-rust/target/release/verify-innosetup-installer" - "/usr/local/bin/verify-innosetup-installer" -) - -for candidate in "${rust_candidates[@]}"; do - if [[ -x "$candidate" ]]; then - exec "$candidate" "$INSTALLER" --root "$ROOT_DIR" --wineprefix "$WINEPREFIX_VERIFY" "${@:2}" - fi -done - -if [[ ! -f "$INSTALLER" ]]; then - echo "Installer not found: $INSTALLER" >&2 - exit 1 -fi - -if ! command -v wine >/dev/null 2>&1; then - echo "wine not found" >&2 - exit 1 -fi - -rm -rf "$WINEPREFIX_VERIFY" -mkdir -p "$WINEPREFIX_VERIFY" -export WINEPREFIX="$WINEPREFIX_VERIFY" -export WINEDEBUG="${WINEDEBUG:--all}" - -wineboot -u >/dev/null 2>&1 -wine "$INSTALLER" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP- /TASKS="" /DIR="$INSTALL_DIR_WIN" >/dev/null 2>&1 -wineserver -w >/dev/null 2>&1 - -required_files=( - windows/AWatchRusCollectorGuardService.cs - windows/aw-collector-guard.ps1 - windows/install-collector-guard-service.ps1 - windows/dlp-policy.native-cross-os.example.json -) - -for rel in "${required_files[@]}"; do - extracted="${INSTALL_DIR_UNIX}/${rel}" - if [[ ! -f "$extracted" ]]; then - echo "Missing extracted file: $rel" >&2 - exit 1 - fi - if ! cmp -s "$rel" "$extracted"; then - echo "Extracted file differs from repo: $rel" >&2 - exit 1 - fi -done - -if ! grep -q 'collector guard self-test OK' "${INSTALL_DIR_UNIX}/windows/aw-collector-guard.ps1"; then - echo "Guard self-test marker missing in extracted installer payload" >&2 - exit 1 -fi - -echo "verify_innosetup_installer: OK" diff --git a/install-kit-awindows-20260427-211240/windows/AWatchRusCollectorGuardService.cs b/install-kit-awindows-20260427-211240/windows/AWatchRusCollectorGuardService.cs deleted file mode 100644 index efcbdd1..0000000 --- a/install-kit-awindows-20260427-211240/windows/AWatchRusCollectorGuardService.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.ServiceProcess; - -namespace AWatchRus -{ - public sealed class CollectorGuardService : ServiceBase - { - private Process child; - private readonly ServiceOptions options; - - public CollectorGuardService(ServiceOptions options) - { - this.options = options; - ServiceName = options.ServiceName; - CanStop = true; - CanShutdown = true; - } - - protected override void OnStart(string[] args) - { - Directory.CreateDirectory(Path.GetDirectoryName(options.LogPath)); - File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " service starting" + Environment.NewLine); - - var psi = new ProcessStartInfo - { - FileName = options.PowerShellPath, - Arguments = string.Format( - "-NoProfile -ExecutionPolicy Bypass -File \"{0}\" -ConfigPath \"{1}\" -Mode {2} -LoopSeconds {3}", - options.ScriptPath, - options.ConfigPath, - options.Mode, - options.LoopSeconds), - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = false, - RedirectStandardError = false, - }; - child = Process.Start(psi); - File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " child pid=" + child.Id + Environment.NewLine); - } - - protected override void OnStop() - { - StopChild("service stopping"); - } - - protected override void OnShutdown() - { - StopChild("system shutdown"); - } - - private void StopChild(string reason) - { - try - { - File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " " + reason + Environment.NewLine); - if (child != null && !child.HasExited) - { - child.Kill(); - child.WaitForExit(10000); - } - } - catch (Exception ex) - { - try - { - File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " stop error: " + ex.Message + Environment.NewLine); - } - catch - { - } - } - } - } - - public sealed class ServiceOptions - { - public string ServiceName = "AWatchRusCollectorGuard"; - public string PowerShellPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32\\WindowsPowerShell\\v1.0\\powershell.exe"); - public string ScriptPath = @"C:\Program Files\AWatch-rus\windows\aw-collector-guard.ps1"; - public string ConfigPath = @"C:\ProgramData\AWatch-rus\deployment-config.json"; - public string Mode = "shadow"; - public int LoopSeconds = 60; - public string LogPath = @"C:\ProgramData\AWatch-rus\logs\collector-guard-service.log"; - } - - internal static class Program - { - private static void Main(string[] args) - { - var options = Parse(args); - ServiceBase.Run(new CollectorGuardService(options)); - } - - private static ServiceOptions Parse(string[] args) - { - var options = new ServiceOptions(); - for (var i = 0; i < args.Length; i++) - { - var key = args[i].ToLowerInvariant(); - var value = i + 1 < args.Length ? args[i + 1] : null; - if (value == null || value.StartsWith("--", StringComparison.Ordinal)) - { - continue; - } - if (key == "--service-name") options.ServiceName = value; - else if (key == "--script") options.ScriptPath = value; - else if (key == "--config") options.ConfigPath = value; - else if (key == "--mode") options.Mode = value; - else if (key == "--loop") - { - int parsed; - if (int.TryParse(value, out parsed)) options.LoopSeconds = parsed; - } - else if (key == "--log") options.LogPath = value; - i++; - } - return options; - } - } -} diff --git a/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1 b/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1 deleted file mode 100644 index 9bc5fa8..0000000 --- a/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1 +++ /dev/null @@ -1,25 +0,0 @@ -@{ - RootModule = 'ActivityWatch.Windows.Common.psm1' - ModuleVersion = '1.0.0' - GUID = '90b3fcf6-df9f-4f9b-9ee0-8a7de4dc0ee2' - Author = 'igor04091968' - CompanyName = 'Private' - Description = 'Common PowerShell functions for ActivityWatch Windows deployment, hardening and recovery.' - PowerShellVersion = '5.1' - FunctionsToExport = @( - '*-ActivityWatch*', - 'Assert-Administrator', - 'Normalize-ActivityWatchUsers', - 'Get-ActivityWatchPackageUrl', - 'Remove-LegacyActivityWatchEntries' - ) - CmdletsToExport = @() - VariablesToExport = '*' - AliasesToExport = @() - PrivateData = @{ - PSData = @{ - Tags = @('ActivityWatch', 'Windows', 'Deployment', 'Recovery') - ProjectUri = 'https://github.com/igor04091968/AWatch-rus' - } - } -} diff --git a/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 b/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 deleted file mode 100755 index 053a497..0000000 --- a/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 +++ /dev/null @@ -1,2548 +0,0 @@ -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' -$script:ActivityWatchBuiltInAdministratorName = $null - -function Assert-Administrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Запустите этот скрипт из PowerShell с правами администратора.' - } -} - -function New-ActivityWatchDirectory { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -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' - ) - - return "https://github.com/ActivityWatch/activitywatch/releases/download/$Version/activitywatch-$Version-windows-x86_64.zip" -} - -function Get-ActivityWatchArchive { - param( - [string]$PackageZipPath, - [string]$PackageUrl, - [string]$Version = 'v0.13.2', - [Parameter(Mandatory = $true)] - [string]$WorkingRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - - if ($PackageZipPath) { - $resolved = Resolve-Path -LiteralPath $PackageZipPath -ErrorAction Stop - return $resolved.Path - } - - if (-not $PackageUrl) { - $PackageUrl = Get-ActivityWatchPackageUrl -Version $Version - } - - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Get-ChildItem -LiteralPath $WorkingRoot -File -Filter 'activitywatch-*.zip' -ErrorAction SilentlyContinue | - Sort-Object LastWriteTime -Descending | - Select-Object -Skip 2 | - ForEach-Object { - try { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue } catch {} - } - $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 -} - -function Remove-ActivityWatchOldInstallBackups { - param( - [Parameter(Mandatory = $true)] - [string]$BackupRoot, - [int]$Keep = 2 - ) - - if (-not (Test-Path -LiteralPath $BackupRoot)) { - return - } - - Get-ChildItem -LiteralPath $BackupRoot -Directory -ErrorAction SilentlyContinue | - Where-Object { $_.Name -like 'install-*' } | - Sort-Object LastWriteTime -Descending | - Select-Object -Skip $Keep | - ForEach-Object { - try { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } catch {} - } -} - -function Get-ActivityWatchPackageRoot { - param( - [Parameter(Mandatory = $true)] - [string]$ExpandedRoot - ) - - $afkBinary = Get-ChildItem -Path $ExpandedRoot -Filter 'aw-watcher-afk.exe' -File -Recurse | - Select-Object -First 1 - - if (-not $afkBinary) { - throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot." - } - - return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent) -} - -function Expand-ActivityWatchArchiveSafe { - param( - [Parameter(Mandatory = $true)] - [string]$ArchivePath, - [Parameter(Mandatory = $true)] - [string]$DestinationPath, - [int]$Attempts = 3 - ) - - for ($attempt = 1; $attempt -le $Attempts; $attempt++) { - try { - if (Test-Path -LiteralPath $DestinationPath) { - Remove-Item -LiteralPath $DestinationPath -Recurse -Force -ErrorAction SilentlyContinue - } - New-ActivityWatchDirectory -Path $DestinationPath - Expand-Archive -Path $ArchivePath -DestinationPath $DestinationPath -Force -ErrorAction Stop - return - } - catch { - if ($attempt -lt $Attempts) { - Start-Sleep -Milliseconds (500 * $attempt) - continue - } - } - } - - # Fallback for intermittent Expand-Archive issues in Windows PowerShell. - if (Test-Path -LiteralPath $DestinationPath) { - Remove-Item -LiteralPath $DestinationPath -Recurse -Force -ErrorAction SilentlyContinue - } - New-ActivityWatchDirectory -Path $DestinationPath - Add-Type -AssemblyName System.IO.Compression.FileSystem - [System.IO.Compression.ZipFile]::ExtractToDirectory($ArchivePath, $DestinationPath) -} - -function Install-ActivityWatchPackage { - param( - [Parameter(Mandatory = $true)] - [string]$ArchivePath, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$WorkingRoot, - [Parameter(Mandatory = $true)] - [string]$BackupRoot - ) - - New-ActivityWatchDirectory -Path $WorkingRoot - New-ActivityWatchDirectory -Path $BackupRoot - Remove-ActivityWatchOldInstallBackups -BackupRoot $BackupRoot - - # Cleanup stale extraction directories from previous failed deployments. - Get-ChildItem -LiteralPath $WorkingRoot -Directory -ErrorAction SilentlyContinue | - Where-Object { $_.Name -like 'extract-*' } | - ForEach-Object { - try { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } catch {} - } - - # 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 - } - New-ActivityWatchDirectory -Path $extractRoot - - $archiveSize = (Get-Item -LiteralPath $ArchivePath -ErrorAction Stop).Length - $workDriveName = [System.IO.Path]::GetPathRoot($WorkingRoot).TrimEnd('\').TrimEnd(':') - $workDrive = Get-PSDrive -Name $workDriveName -ErrorAction SilentlyContinue - $freeBytes = $null - if ($workDrive -and $null -ne $workDrive.Free) { - $freeBytes = [int64]$workDrive.Free - } - elseif ($workDriveName) { - try { - $disk = Get-CimInstance Win32_LogicalDisk -Filter ("DeviceID='{0}:'" -f $workDriveName) -ErrorAction Stop - if ($disk -and $null -ne $disk.FreeSpace) { - $freeBytes = [int64]$disk.FreeSpace - } - } - catch { - } - } - if ($null -ne $freeBytes) { - # Require at least ~2.5x archive size to handle extraction + copy safely. - $required = [int64]([Math]::Ceiling($archiveSize * 2.5)) - if ($freeBytes -lt $required) { - throw ("Недостаточно свободного места на {0}: free={1} bytes, required>={2} bytes" -f $workDriveName, $freeBytes, $required) - } - } - - try { - Expand-ActivityWatchArchiveSafe -ArchivePath $ArchivePath -DestinationPath $extractRoot - $packageRoot = Get-ActivityWatchPackageRoot -ExpandedRoot $extractRoot - - if (Test-Path -LiteralPath $InstallRoot) { - $existingItems = Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue - if ($existingItems) { - $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' - $backupPath = Join-Path $BackupRoot ("install-$stamp") - New-ActivityWatchDirectory -Path $backupPath - Copy-Item -Path (Join-Path $InstallRoot '*') -Destination $backupPath -Recurse -Force - Get-ChildItem -LiteralPath $InstallRoot -Force | Remove-Item -Recurse -Force - } - } - else { - New-ActivityWatchDirectory -Path $InstallRoot - } - - Copy-Item -Path (Join-Path $packageRoot '*') -Destination $InstallRoot -Recurse -Force - - return [pscustomobject]@{ - PackageRoot = $packageRoot - ExtractRoot = $extractRoot - BackupRoot = $BackupRoot - } - } - finally { - if (Test-Path -LiteralPath $extractRoot) { - try { Remove-Item -LiteralPath $extractRoot -Recurse -Force -ErrorAction SilentlyContinue } catch {} - } - if ((Test-Path -LiteralPath $ArchivePath) -and ($ArchivePath -like (Join-Path $WorkingRoot 'activitywatch-*.zip'))) { - try { Remove-Item -LiteralPath $ArchivePath -Force -ErrorAction SilentlyContinue } catch {} - } - Remove-ActivityWatchOldInstallBackups -BackupRoot $BackupRoot - } -} - -function Get-ActivityWatchExecutableMap { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot - ) - - $map = [ordered]@{ - Afk = Join-Path $InstallRoot 'aw-watcher-afk\aw-watcher-afk.exe' - Window = Join-Path $InstallRoot 'aw-watcher-window\aw-watcher-window.exe' - } - - foreach ($entry in $map.GetEnumerator()) { - if (-not (Test-Path -LiteralPath $entry.Value)) { - throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)" - } - } - - return [pscustomobject]$map -} - -function Repair-ActivityWatchPotentialMojibake { - param([string]$Value) - - if ([string]::IsNullOrWhiteSpace($Value)) { - return $Value - } - - if ($Value -notmatch '[\u0400-\u04FF]') { - return $Value - } - - try { - $bytes = [Text.Encoding]::GetEncoding(1251).GetBytes($Value) - $repaired = [Text.Encoding]::UTF8.GetString($bytes) - if (-not [string]::IsNullOrWhiteSpace($repaired) -and $repaired -match '[\u0400-\u04FF]') { - return $repaired - } - } - catch { - } - - return $Value -} - -function Get-ActivityWatchBuiltInAdministratorName { - if ($script:ActivityWatchBuiltInAdministratorName) { - return $script:ActivityWatchBuiltInAdministratorName - } - - if (-not [string]::IsNullOrWhiteSpace($env:AWATCH_RUS_BUILTIN_ADMINISTRATOR_NAME)) { - $script:ActivityWatchBuiltInAdministratorName = [string]$env:AWATCH_RUS_BUILTIN_ADMINISTRATOR_NAME - return $script:ActivityWatchBuiltInAdministratorName - } - - try { - $account = Get-CimInstance Win32_UserAccount -Filter "LocalAccount=True" -ErrorAction Stop | - Where-Object { [string]$_.SID -match '-500$' } | - Select-Object -First 1 - if ($account -and -not [string]::IsNullOrWhiteSpace([string]$account.Name)) { - $script:ActivityWatchBuiltInAdministratorName = [string]$account.Name - return $script:ActivityWatchBuiltInAdministratorName - } - } - catch { - } - - if ([string]$env:COMPUTERNAME -ieq 'SHARKON2025') { - $script:ActivityWatchBuiltInAdministratorName = 'Администратор' - return $script:ActivityWatchBuiltInAdministratorName - } - - $script:ActivityWatchBuiltInAdministratorName = 'Administrator' - return $script:ActivityWatchBuiltInAdministratorName -} - -function Normalize-ActivityWatchUserId { - param( - [string]$UserId, - [string]$Domain - ) - - if ([string]::IsNullOrWhiteSpace($UserId)) { - return $null - } - - $normalized = Repair-ActivityWatchPotentialMojibake -Value $UserId.Trim() - $resolvedDomain = $null - $leafUser = $normalized - - if ($normalized -match '^([^\\]+)\\(.+)$') { - $resolvedDomain = Repair-ActivityWatchPotentialMojibake -Value $Matches[1] - $leafUser = Repair-ActivityWatchPotentialMojibake -Value $Matches[2] - } - - if ($leafUser -match '^(?i:administrator|администратор)$') { - $leafUser = Get-ActivityWatchBuiltInAdministratorName - } - - if ([string]::IsNullOrWhiteSpace($resolvedDomain) -and -not [string]::IsNullOrWhiteSpace($Domain)) { - $resolvedDomain = Repair-ActivityWatchPotentialMojibake -Value $Domain.Trim() - } - - if (-not [string]::IsNullOrWhiteSpace($resolvedDomain)) { - return ('{0}\{1}' -f $resolvedDomain, $leafUser) - } - - return $leafUser -} - -function Normalize-ActivityWatchUsers { - param( - [string[]]$Users, - [string]$UserListPath, - [string]$Domain - ) - - $collected = New-Object System.Collections.Generic.List[string] - - if ($Users) { - foreach ($user in $Users) { - if (-not [string]::IsNullOrWhiteSpace($user)) { - $collected.Add($user.Trim()) - } - } - } - - if ($UserListPath) { - $resolved = Resolve-Path -LiteralPath $UserListPath -ErrorAction Stop - $extension = [IO.Path]::GetExtension($resolved.Path) - if ($extension -ieq '.csv') { - $rows = Import-Csv -LiteralPath $resolved.Path -Encoding UTF8 - foreach ($row in $rows) { - foreach ($column in 'User', 'Username', 'SamAccountName', 'Login') { - if ($row.PSObject.Properties.Name -contains $column) { - $value = [string]$row.$column - if (-not [string]::IsNullOrWhiteSpace($value)) { - $collected.Add($value.Trim()) - break - } - } - } - } - } - else { - Get-Content -LiteralPath $resolved.Path -Encoding UTF8 | ForEach-Object { - $line = $_.Trim() - if ($line -and -not $line.StartsWith('#')) { - $collected.Add($line) - } - } - } - } - - $normalized = @($collected | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - ForEach-Object { Normalize-ActivityWatchUserId -UserId $_ -Domain $Domain } | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - Sort-Object -Unique) - - if (-not $normalized -or $normalized.Count -eq 0) { - throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.' - } - - return @($normalized) -} - -function Get-ActivityWatchTaskNameToken { - param( - [Parameter(Mandatory = $true)] - [string]$UserId - ) - - $buffer = [Text.StringBuilder]::new() - foreach ($character in $UserId.ToCharArray()) { - if ([char]::IsLetterOrDigit($character)) { - [void]$buffer.Append($character) - } - else { - [void]$buffer.Append('_') - } - } - - return $buffer.ToString().Trim('_') -} - -function Test-ActivityWatchScheduledTaskExistsExact { - param([string]$TaskName) - - if ([string]::IsNullOrWhiteSpace($TaskName)) { - return $false - } - - try { - & schtasks.exe /Query /TN $TaskName *> $null - return ($LASTEXITCODE -eq 0) - } - catch { - return $false - } -} - -function New-ActivityWatchUserTaskDefinitions { - param( - [Parameter(Mandatory = $true)] - [string[]]$Users - ) - - $result = foreach ($user in $Users) { - $normalizedUser = Normalize-ActivityWatchUserId -UserId $user - $token = Get-ActivityWatchTaskNameToken -UserId $normalizedUser - [pscustomobject]@{ - UserId = $normalizedUser - LaunchTaskName = "ActivityWatch Launch [$token]" - } - } - - return @($result) -} - -function Get-ActivityWatchLoggedOnUsers { - $users = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - - try { - $lines = & quser.exe 2>$null - foreach ($line in @($lines)) { - $normalized = [string]$line - if ([string]::IsNullOrWhiteSpace($normalized)) { - continue - } - - $normalized = $normalized.TrimStart(' ', '>') - if ([string]::IsNullOrWhiteSpace($normalized)) { - continue - } - - if ($normalized -match '^(USERNAME|ПОЛЬЗОВАТЕЛЬ)\s+') { - continue - } - - $parts = $normalized -split '\s+' - if ($parts.Count -lt 1) { - continue - } - - $user = [string]$parts[0] - if ([string]::IsNullOrWhiteSpace($user)) { - continue - } - - [void]$users.Add($user) - [void]$users.Add(('{0}\{1}' -f $env:COMPUTERNAME, $user)) - if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) { - [void]$users.Add(('{0}\{1}' -f $env:USERDOMAIN, $user)) - } - } - } - catch { - } - - return @($users) -} - -function Get-ActivityWatchSessionRecords { - $sessions = New-Object System.Collections.Generic.List[object] - - try { - $lines = & qwinsta.exe 2>$null - foreach ($line in @($lines)) { - $normalized = [string]$line - if ([string]::IsNullOrWhiteSpace($normalized)) { - continue - } - - $normalized = $normalized.TrimStart(' ', '>') - if ([string]::IsNullOrWhiteSpace($normalized)) { - continue - } - - if ($normalized -match '^(SESSIONNAME|ИМЯ СЕАНСА)\s+') { - continue - } - - $columns = @( - (($normalized -replace '\s{2,}', '|') -split '\|') | - ForEach-Object { $_.Trim() } | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } - ) - if ($columns.Count -lt 3) { - continue - } - - $sessionName = [string]$columns[0] - $userName = $null - $sessionIdIndex = 1 - - if ($columns[1] -notmatch '^\d+$') { - $userName = [string]$columns[1] - $sessionIdIndex = 2 - } - - if ($columns.Count -le $sessionIdIndex -or $columns[$sessionIdIndex] -notmatch '^\d+$') { - continue - } - - $sessionId = [int]$columns[$sessionIdIndex] - $state = if ($columns.Count -gt ($sessionIdIndex + 1)) { [string]$columns[$sessionIdIndex + 1] } else { '' } - $isLive = $state -match '^(Active|Conn|Активно|Подкл\w*)$' - - $sessions.Add([pscustomobject]@{ - SessionName = $sessionName - UserName = $userName - SessionId = $sessionId - State = $state - IsLive = $isLive - }) | Out-Null - } - } - catch { - } - - $explorerUsers = Get-ActivityWatchExplorerUsersBySession - foreach ($session in @($sessions.ToArray())) { - $sessionId = [int]$session.SessionId - if ($explorerUsers.ContainsKey($sessionId)) { - $session.UserName = [string]$explorerUsers[$sessionId] - } - } - - return @($sessions.ToArray()) -} - -function Resolve-ActivityWatchUserCandidates { - param( - [Parameter(Mandatory = $true)] - [string]$UserId - ) - - $normalizedUserId = Normalize-ActivityWatchUserId -UserId $UserId - $candidateIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - [void]$candidateIds.Add($normalizedUserId) - - $leafUser = $normalizedUserId - if ($leafUser -match '^[^\\]+\\(.+)$') { - $leafUser = $Matches[1] - [void]$candidateIds.Add($leafUser) - } - - [void]$candidateIds.Add(('{0}\{1}' -f $env:COMPUTERNAME, $leafUser)) - if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) { - [void]$candidateIds.Add(('{0}\{1}' -f $env:USERDOMAIN, $leafUser)) - } - - return @($candidateIds) -} - -function Test-ActivityWatchUserHasSession { - param( - [Parameter(Mandatory = $true)] - [string]$UserId, - [string[]]$LoggedOnUsers - ) - - if ([string]::IsNullOrWhiteSpace($UserId)) { - return $false - } - - foreach ($candidate in @(Resolve-ActivityWatchUserCandidates -UserId $UserId)) { - if ($LoggedOnUsers -contains $candidate) { - return $true - } - } - - return $false -} - -function Test-ActivityWatchUserHasLiveSession { - param( - [Parameter(Mandatory = $true)] - [string]$UserId, - [object[]]$SessionRecords - ) - - if ([string]::IsNullOrWhiteSpace($UserId)) { - return $false - } - - foreach ($candidate in @(Resolve-ActivityWatchUserCandidates -UserId $UserId)) { - if (@($SessionRecords | Where-Object { - $_.IsLive -and - -not [string]::IsNullOrWhiteSpace([string]$_.UserName) -and - ( - [string]$_.UserName -ieq $candidate -or - ('{0}\{1}' -f $env:COMPUTERNAME, [string]$_.UserName) -ieq $candidate -or - ((-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) -and ('{0}\{1}' -f $env:USERDOMAIN, [string]$_.UserName) -ieq $candidate) - ) - }).Count -gt 0) { - return $true - } - } - - return $false -} - -function Test-ActivityWatchSessionMatchesUserId { - param( - [Parameter(Mandatory = $true)] - [object]$SessionRecord, - [Parameter(Mandatory = $true)] - [string]$UserId - ) - - if ([string]::IsNullOrWhiteSpace($UserId) -or $null -eq $SessionRecord) { - return $false - } - - $sessionUser = [string]$SessionRecord.UserName - if ([string]::IsNullOrWhiteSpace($sessionUser)) { - return $false - } - - foreach ($candidate in @(Resolve-ActivityWatchUserCandidates -UserId $UserId)) { - if ($sessionUser -ieq $candidate -or - ('{0}\{1}' -f $env:COMPUTERNAME, $sessionUser) -ieq $candidate -or - ((-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) -and ('{0}\{1}' -f $env:USERDOMAIN, $sessionUser) -ieq $candidate)) { - return $true - } - } - - return $false -} - -function Test-ActivityWatchUserHasManagedSession { - param( - [Parameter(Mandatory = $true)] - [string]$UserId, - [object[]]$SessionRecords, - [switch]$IncludeLive, - [switch]$IncludeDisconnected - ) - - foreach ($session in @($SessionRecords)) { - if ([int]$session.SessionId -le 0) { - continue - } - if ([string]::IsNullOrWhiteSpace([string]$session.UserName)) { - continue - } - if ([bool]$session.IsLive -and -not $IncludeLive.IsPresent) { - continue - } - if (-not [bool]$session.IsLive -and -not $IncludeDisconnected.IsPresent) { - continue - } - if (Test-ActivityWatchSessionMatchesUserId -SessionRecord $session -UserId $UserId) { - return $true - } - } - - return $false -} - -function Get-ActivityWatchManagedInteractiveSessions { - param( - [pscustomobject[]]$TaskDefinitions, - [object[]]$SessionRecords, - [switch]$IncludeLive, - [switch]$IncludeDisconnected - ) - - $result = New-Object System.Collections.Generic.List[object] - $seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - - foreach ($taskDef in @($TaskDefinitions)) { - $userId = [string]$taskDef.userId - $taskName = [string]$taskDef.taskName - if ([string]::IsNullOrWhiteSpace($userId) -or [string]::IsNullOrWhiteSpace($taskName)) { - continue - } - - foreach ($session in @($SessionRecords)) { - if ([int]$session.SessionId -le 0) { - continue - } - if ([string]::IsNullOrWhiteSpace([string]$session.UserName)) { - continue - } - if ([bool]$session.IsLive -and -not $IncludeLive.IsPresent) { - continue - } - if (-not [bool]$session.IsLive -and -not $IncludeDisconnected.IsPresent) { - continue - } - if (-not (Test-ActivityWatchSessionMatchesUserId -SessionRecord $session -UserId $userId)) { - continue - } - - $key = '{0}|{1}|{2}' -f $taskName, [int]$session.SessionId, $userId - if (-not $seen.Add($key)) { - continue - } - - $result.Add([pscustomobject]@{ - TaskName = $taskName - UserId = $userId - SessionName = [string]$session.SessionName - SessionId = [int]$session.SessionId - State = [string]$session.State - UserName = [string]$session.UserName - IsLive = [bool]$session.IsLive - }) | Out-Null - } - } - - return @($result.ToArray()) -} - -function Copy-ActivityWatchCollectorAssets { - param( - [Parameter(Mandatory = $true)] - [string]$CollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScriptSource, - [string]$PolicyClientScriptSource, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScriptSource, - [string]$EvtxExportScriptSource, - [string]$HayabusaUploadScriptSource, - [string]$File1CTelemetryScriptSource, - [string]$EmailCollectorScriptSource, - [Parameter(Mandatory = $true)] - [string]$ExampleRulesSource, - [Parameter(Mandatory = $true)] - [string]$ExamplePolicySource, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [string]$CustomRulesSource, - [string]$CustomPolicySource - ) - - New-ActivityWatchDirectory -Path $StateRoot - - $collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1' - $endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' - $policyClientTarget = Join-Path $StateRoot 'dlp-policy-client.ps1' - $fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1' - $sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1' - $evtxExportTarget = Join-Path $StateRoot 'export-evtx-for-hayabusa.ps1' - $hayabusaUploadTarget = Join-Path $StateRoot 'export-upload-hayabusa-to-aw-server.ps1' - $file1cTelemetryTarget = Join-Path $StateRoot 'export-upload-file-1c-telemetry.ps1' - $emailCollectorTarget = Join-Path $StateRoot 'email-outbound-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' - $policyTarget = Join-Path $StateRoot 'dlp-policy.json' - - Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force - Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force - if ($PolicyClientScriptSource -and (Test-Path -LiteralPath $PolicyClientScriptSource)) { - Copy-Item -LiteralPath $PolicyClientScriptSource -Destination $policyClientTarget -Force - } - Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force - Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force - if ($EvtxExportScriptSource -and (Test-Path -LiteralPath $EvtxExportScriptSource)) { - Copy-Item -LiteralPath $EvtxExportScriptSource -Destination $evtxExportTarget -Force - } - if ($HayabusaUploadScriptSource -and (Test-Path -LiteralPath $HayabusaUploadScriptSource)) { - Copy-Item -LiteralPath $HayabusaUploadScriptSource -Destination $hayabusaUploadTarget -Force - } - if ($File1CTelemetryScriptSource -and (Test-Path -LiteralPath $File1CTelemetryScriptSource)) { - Copy-Item -LiteralPath $File1CTelemetryScriptSource -Destination $file1cTelemetryTarget -Force - } - if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) { - Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force - } - Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force - Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force - - if ($CustomRulesSource) { - $resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force - } - else { - Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force - } - - if ($CustomPolicySource) { - $resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop - Copy-Item -LiteralPath $resolvedPolicy.Path -Destination $policyTarget -Force - } - else { - Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force - } - - return [pscustomobject]@{ - CollectorScript = $collectorTarget - EndpointCollectorScript = $endpointCollectorTarget - PolicyClientScript = $policyClientTarget - FileCollectorScript = $fileCollectorTarget - SessionCollectorScript = $sessionCollectorTarget - EvtxExportScript = $evtxExportTarget - HayabusaUploadScript = $hayabusaUploadTarget - File1CTelemetryScript = $file1cTelemetryTarget - EmailCollectorScript = $emailCollectorTarget - ExampleRules = $exampleRulesTarget - ActiveRules = $rulesTarget - ExamplePolicy = $examplePolicyTarget - ActivePolicy = $policyTarget - } -} - -function New-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [Parameter(Mandatory = $true)] - [int]$ServerPort, - [Parameter(Mandatory = $true)] - [string]$ServerScheme, - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot, - [Parameter(Mandatory = $true)] - [string]$CollectorScript, - [Parameter(Mandatory = $true)] - [string]$EndpointCollectorScript, - [string]$PolicyClientScript, - [Parameter(Mandatory = $true)] - [string]$FileCollectorScript, - [Parameter(Mandatory = $true)] - [string]$SessionCollectorScript, - [string]$EvtxExportScript, - [string]$HayabusaUploadScript, - [string]$File1CTelemetryScript, - [string]$EmailCollectorScript, - [Parameter(Mandatory = $true)] - [string]$RulesPath, - [Parameter(Mandatory = $true)] - [string]$PolicyPath, - [Parameter(Mandatory = $true)] - [int]$PollSeconds, - [Parameter(Mandatory = $true)] - [int]$PulseSeconds, - [Parameter(Mandatory = $true)] - [int]$RecoveryIntervalSeconds, - [bool]$AfkEnabled = $true, - [bool]$WindowEnabled = $true, - [bool]$FileOpsEnabled = $true, - [bool]$LocalAgentLogsEnabled = $true, - [bool]$IncidentCaptureEnabled = $true, - [bool]$IncidentScreenshotEnabled = $true, - [string]$IncidentArtifactsRoot, - [string]$EvtxExportRoot, - [int]$EvtxRetentionDays = 14, - [string[]]$EvtxChannels = @(), - [bool]$LogonMarkerEnabled = $true, - [bool]$ProcessEventsEnabled = $false, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [string]$AwHostname, - [ValidateSet('local', 'server')] - [string]$PolicyMode = 'local', - [bool]$PolicyEngineEnabled = $false, - [string]$PolicyEngineHost, - [int]$PolicyEnginePort = 5601, - [ValidateSet('http', 'https')] - [string]$PolicyEngineScheme = 'http', - [int]$PolicyRefreshSeconds = 300, - [string]$PolicyCachePath, - [Parameter(Mandatory = $true)] - [pscustomobject[]]$UserTasks, - [string]$PackageVersion = 'v0.13.2', - [bool]$HayabusaAutoUploadEnabled = $true, - [int]$HayabusaAutoUploadIntervalHours = 6, - [int]$HayabusaAutoUploadHoursBack = 6, - [string]$HayabusaAutoUploadMode = 'incident', - [string]$HayabusaAutoUploadTaskName = 'ActivityWatch Hayabusa Upload', - [bool]$File1CAutoUploadEnabled = $true, - [int]$File1CAutoUploadIntervalHours = 6, - [string]$File1CAutoUploadTaskName = 'ActivityWatch File1C Upload', - [string]$File1CTargetHost, - [string]$File1CTargetUser = 'igor', - [string]$File1CRemoteRoot = '/opt/activitywatch/clickhouse-1c/landing', - [string]$File1CRegistryWorkbookPath = 'E:\USER1\СПИСОК ПРЕДПРИЯТИЙ И ИХ РАСПРЕДЕЛЕНИЕ.xlsx', - [switch]$IntegrationTestEnabled - ) - - $effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' } - $effectiveEvtxExportRoot = if ($EvtxExportRoot) { $EvtxExportRoot } else { Join-Path $StateRoot 'forensics\evtx-exports' } - $effectiveEvtxChannels = if ($EvtxChannels -and $EvtxChannels.Count -gt 0) { - @($EvtxChannels) - } else { - @( - 'Security', - 'System', - 'Application', - 'Microsoft-Windows-PowerShell/Operational', - 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational', - 'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational' - ) - } - $effectivePolicyEngineHost = if ([string]::IsNullOrWhiteSpace($PolicyEngineHost)) { $ServerHost } else { $PolicyEngineHost } - $effectivePolicyCachePath = if ([string]::IsNullOrWhiteSpace($PolicyCachePath)) { Join-Path $StateRoot 'dlp-policy-cache.json' } else { $PolicyCachePath } - if ($File1CAutoUploadEnabled -and [string]::IsNullOrWhiteSpace($File1CTargetHost)) { - throw 'File1CTargetHost is required when File1CAutoUploadEnabled is true.' - } - - return [pscustomobject]@{ - version = 1 - generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - awHostname = if ([string]::IsNullOrWhiteSpace($AwHostname)) { [string]$env:COMPUTERNAME } else { [string]$AwHostname } - server = [pscustomobject]@{ - host = $ServerHost - port = $ServerPort - scheme = $ServerScheme - } - paths = [pscustomobject]@{ - installRoot = $InstallRoot - stateRoot = $StateRoot - logsRoot = $LogsRoot - collectorScript = $CollectorScript - endpointCollectorScript = $EndpointCollectorScript - policyClientScript = $PolicyClientScript - emailCollectorScript = $EmailCollectorScript - fileCollectorScript = $FileCollectorScript - sessionCollectorScript = $SessionCollectorScript - evtxExportScript = $EvtxExportScript - hayabusaUploadScript = $HayabusaUploadScript - file1cTelemetryScript = $File1CTelemetryScript - rulesPath = $RulesPath - policyPath = $PolicyPath - launchScript = $LaunchScriptPath - recoveryScript = $RecoveryScriptPath - } - collector = [pscustomobject]@{ - pollSeconds = $PollSeconds - pulseSeconds = $PulseSeconds - } - collectors = [pscustomobject]@{ - afkEnabled = $AfkEnabled - windowEnabled = $WindowEnabled - fileOpsEnabled = $FileOpsEnabled - emailEnabled = $false - } - logging = [pscustomobject]@{ - localAgentLogsEnabled = $LocalAgentLogsEnabled - } - incidentCapture = [pscustomobject]@{ - enabled = $IncidentCaptureEnabled - screenshotEnabled = $IncidentScreenshotEnabled - artifactsRoot = $effectiveIncidentArtifactsRoot - } - forensics = [pscustomobject]@{ - evtxExportRoot = $effectiveEvtxExportRoot - retentionDays = $EvtxRetentionDays - evtxChannels = @($effectiveEvtxChannels) - hayabusaAutomation = [pscustomobject]@{ - enabled = [bool]$HayabusaAutoUploadEnabled - intervalHours = $HayabusaAutoUploadIntervalHours - hoursBack = $HayabusaAutoUploadHoursBack - mode = $HayabusaAutoUploadMode - taskName = $HayabusaAutoUploadTaskName - } - } - analytics = [pscustomobject]@{ - file1cAutomation = [pscustomobject]@{ - enabled = [bool]$File1CAutoUploadEnabled - intervalHours = $File1CAutoUploadIntervalHours - taskName = $File1CAutoUploadTaskName - targetHost = $File1CTargetHost - targetUser = $File1CTargetUser - remoteRoot = $File1CRemoteRoot - registryWorkbookPath = $File1CRegistryWorkbookPath - } - } - sessionEvents = [pscustomobject]@{ - logonEnabled = $LogonMarkerEnabled - processEventsEnabled = $ProcessEventsEnabled - bucketPrefix = 'aw-session-events' - } - recovery = [pscustomobject]@{ - intervalSeconds = $RecoveryIntervalSeconds - taskName = 'ActivityWatch Recovery' - } - dlp = [pscustomobject]@{ - incidentBucketPrefix = 'aw-dlp-incidents' - enabled = $true - } - policyEngine = [pscustomobject]@{ - enabled = $PolicyEngineEnabled - mode = $PolicyMode - host = $effectivePolicyEngineHost - port = $PolicyEnginePort - scheme = $PolicyEngineScheme - refreshSeconds = $PolicyRefreshSeconds - cachePath = $effectivePolicyCachePath - } - package = [pscustomobject]@{ - version = $PackageVersion - } - userTasks = @($UserTasks) - integrationTestEnabled = [bool]$IntegrationTestEnabled - } -} - -function Write-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$Config, - [Parameter(Mandatory = $true)] - [string]$Path - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $json = $Config | ConvertTo-Json -Depth 8 - Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 -} - -function Read-ActivityWatchDeploymentConfig { - param( - [Parameter(Mandatory = $true)] - [string]$Path - ) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация развёртывания не найдена: $Path" - } - - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Write-ActivityWatchLaunchScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Stop' - -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -Add-Type -AssemblyName System.Net.Http -`$script:MaxCollectorPowerShellProcesses = 48 -`$script:CollectorProcessSnapshotLoaded = `$false -`$script:CollectorProcessSnapshot = @() - -function Get-DeploymentConfig { - param([string]`$Path) - return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json -} - -function Test-ProcessInSession { - param( - [string]`$Name, - [int]`$SessionId - ) - - return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1) -} - -function Get-CollectorProcessSnapshot { - if (`$script:CollectorProcessSnapshotLoaded) { - return @(`$script:CollectorProcessSnapshot) - } - - `$script:CollectorProcessSnapshotLoaded = `$true - `$script:CollectorProcessSnapshot = @() - `$job = `$null - try { - `$job = Start-Job -ScriptBlock { - Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe' OR Name = 'pwsh.exe'" -ErrorAction SilentlyContinue | - Where-Object { - `$_.CommandLine -match 'AWatch-rus' -and - `$_.CommandLine -match '\.ps1' - } | - Select-Object ProcessId, SessionId, CommandLine - } - - if (Wait-Job -Job `$job -Timeout 4) { - `$script:CollectorProcessSnapshot = @(Receive-Job -Job `$job -ErrorAction SilentlyContinue) - } - } - catch { - `$script:CollectorProcessSnapshot = @() - } - finally { - if (`$job) { - Stop-Job -Job `$job -ErrorAction SilentlyContinue | Out-Null - Remove-Job -Job `$job -Force -ErrorAction SilentlyContinue | Out-Null - } - } - - return @(`$script:CollectorProcessSnapshot) -} - -function Test-CollectorRunning { - param( - [string]`$ScriptPath, - [int]`$SessionId - ) - - `$escapedCollector = [Regex]::Escape(`$ScriptPath) - `$processes = Get-CollectorProcessSnapshot | - Where-Object { - `$_.SessionId -eq `$SessionId -and - `$_.CommandLine -match `$escapedCollector - } - - return [bool](`$processes | Select-Object -First 1) -} - -function Get-CollectorPowerShellProcessCount { - return @(Get-CollectorProcessSnapshot).Count -} - -function New-LaunchLock { - param([string]`$StateRoot, [int]`$SessionId) - - `$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId) - if (Test-Path -LiteralPath `$lockPath) { - try { - `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json - `$existingPid = [int]`$lockData.pid - if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) { - return `$null - } - } - catch { - } - } - - `$payload = @{ - pid = `$PID - sessionId = `$SessionId - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8 - return `$lockPath -} - -function Get-SessionMarkerToken { - param([int]`$SessionId) - - try { - `$explorer = Get-Process -Name 'explorer' -ErrorAction SilentlyContinue | - Where-Object { `$_.SessionId -eq `$SessionId } | - Sort-Object StartTime | - Select-Object -First 1 - if (`$explorer -and `$explorer.StartTime) { - return `$explorer.StartTime.ToUniversalTime().ToString('yyyyMMddTHHmmssZ') - } - } - catch { - } - - try { - `$currentProcess = Get-Process -Id `$PID -ErrorAction Stop - if (`$currentProcess.StartTime) { - return `$currentProcess.StartTime.ToUniversalTime().ToString('yyyyMMddTHHmmssZ') - } - } - catch { - } - - return [string]`$SessionId -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = `$true)][string]`$Uri, - [Parameter(Mandatory = `$true)][string]`$Json - ) - - `$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 { - param( - [string]`$BucketId, - [string]`$ClientName, - [string]`$BucketType - ) - - if (`$script:KnownBuckets.ContainsKey(`$BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - `$script:KnownBuckets[`$BucketId] = `$true - return - } - catch { - } - - `$body = @{ - client = `$ClientName - type = `$BucketType - hostname = `$script:Hostname - } | ConvertTo-Json -Compress - - try { - if (-not (Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body)) { - return - } - } - catch { - try { - Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null - } - catch { - return - } - } - - `$script:KnownBuckets[`$BucketId] = `$true -} - -function Send-LogonMarkerIfNeeded { - param( - [pscustomobject]`$Config, - [int]`$SessionId - ) - - `$sessionEvents = if (`$Config.PSObject.Properties.Name -contains 'sessionEvents') { `$Config.sessionEvents } else { `$null } - `$logging = if (`$Config.PSObject.Properties.Name -contains 'logging') { `$Config.logging } else { `$null } - `$logonEnabled = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]`$sessionEvents.logonEnabled } else { `$false } - if (-not `$logonEnabled) { - return - } - - `$bucketPrefix = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]`$sessionEvents.bucketPrefix)) { - [string]`$sessionEvents.bucketPrefix - } - else { - 'aw-session-events' - } - - `$stateRoot = [string]`$Config.paths.stateRoot - `$markerRoots = New-Object System.Collections.Generic.List[string] - if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) { - `$markerRoots.Add((Join-Path `$stateRoot 'markers')) - } - if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) { - `$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers')) - } - - `$markerDir = `$null - foreach (`$candidate in `$markerRoots) { - try { - if (-not (Test-Path -LiteralPath `$candidate)) { - New-Item -Path `$candidate -ItemType Directory -Force | Out-Null - } - - `$probePath = Join-Path `$candidate 'write-test.tmp' - Set-Content -LiteralPath `$probePath -Value 'ok' -Encoding ASCII - Remove-Item -LiteralPath `$probePath -Force -ErrorAction SilentlyContinue - `$markerDir = `$candidate - break - } - catch { - } - } - - if (-not `$markerDir) { - return - } - - `$sessionMarkerToken = Get-SessionMarkerToken -SessionId `$SessionId - `$markerFile = Join-Path `$markerDir ("logon-{0}-{1}-{2}.marker" -f `$env:USERNAME, `$SessionId, `$sessionMarkerToken) - if (Test-Path -LiteralPath `$markerFile) { - return - } - - Set-Content -LiteralPath `$markerFile -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8 - - `$bucketId = ('{0}_{1}' -f `$bucketPrefix, `$script:Hostname) - Ensure-Bucket -BucketId `$bucketId -ClientName 'aw-session-events' -BucketType 'aw.session.event' - - `$payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - eventType = 'logon' - username = `$env:USERNAME - userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)" - sessionId = `$SessionId - hostname = `$script:Hostname - source = 'launch-watchers-awatch-rus' - } - } | ConvertTo-Json -Depth 5 -Compress - - try { - Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$bucketId/heartbeat?pulsetime=1" -Json `$payload - } - catch { - Remove-Item -LiteralPath `$markerFile -Force -ErrorAction SilentlyContinue - throw - } -} - -function Start-CollectorScriptIfNeeded { - param( - [string]`$ScriptPath, - [string]`$ConfigPath, - [string]`$PowerShellExe, - [int]`$SessionId - ) - - if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) { - return - } - - if (-not (Test-Path -LiteralPath `$ScriptPath)) { - return - } - - if (Test-CollectorRunning -ScriptPath `$ScriptPath -SessionId `$SessionId) { - return - } - - if ((Get-CollectorPowerShellProcessCount) -ge `$script:MaxCollectorPowerShellProcesses) { - return - } - - `$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null } - `$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass') - if (`$staParam) { `$argumentList += `$staParam } - `$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath) - Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden -} - -`$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 = if (`$config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]`$config.awHostname)) { [string]`$config.awHostname } else { `$env:COMPUTERNAME } -`$script:KnownBuckets = @{} -`$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' } -`$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 } -`$emailEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'emailEnabled') { [bool]`$config.collectors.emailEnabled } else { `$false } -`$emailCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'emailCollectorScript') { [string]`$config.paths.emailCollectorScript } else { Join-Path `$stateRoot 'email-outbound-collector.ps1' } -`$launchLockPath = New-LaunchLock -StateRoot `$stateRoot -SessionId `$sessionId -if (-not `$launchLockPath) { - return -} - -try { - if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) { - throw "Не найден aw-watcher-afk.exe: `$afkExe" - } - - if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) { - throw "Не найден aw-watcher-window.exe: `$windowExe" - } - - if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) { - Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) { - Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden - } - - try { - Send-LogonMarkerIfNeeded -Config `$config -SessionId `$sessionId - } - 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 - } - if (`$emailEnabled -and (Test-Path -LiteralPath `$emailCollectorScript)) { - Start-CollectorScriptIfNeeded -ScriptPath `$emailCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId - } -} -finally { - if (`$launchLockPath -and (Test-Path -LiteralPath `$launchLockPath)) { - Remove-Item -LiteralPath `$launchLockPath -Force -ErrorAction SilentlyContinue - } -} -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Write-ActivityWatchRecoveryScript { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' - $content = @" -param( - [string]`$ConfigPath = '$ConfigPath' -) - -Set-StrictMode -Version Latest -`$ErrorActionPreference = 'Continue' -Import-Module '$modulePath' -Force -Invoke-ActivityWatchRecoveryLoop -ConfigPath `$ConfigPath -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 -} - -function Get-ActivityWatchRecoveryConfigPaths { - param([string]$PrimaryConfigPath) - - $paths = New-Object System.Collections.Generic.List[string] - if ($PrimaryConfigPath -and (Test-Path -LiteralPath $PrimaryConfigPath)) { - $paths.Add((Resolve-Path -LiteralPath $PrimaryConfigPath).Path) - } - - $searchRoot = $env:ProgramData - if ($PrimaryConfigPath) { - $stateRoot = Split-Path -Path $PrimaryConfigPath -Parent - $candidateRoot = Split-Path -Path $stateRoot -Parent - if ($candidateRoot -and (Test-Path -LiteralPath $candidateRoot)) { - $searchRoot = $candidateRoot - } - } - - if (Test-Path -LiteralPath $searchRoot) { - Get-ChildItem -LiteralPath $searchRoot -Directory -ErrorAction SilentlyContinue | - Where-Object { $_.Name -like 'ActivityWatch*' } | - ForEach-Object { - $candidate = Join-Path $_.FullName 'deployment-config.json' - if (Test-Path -LiteralPath $candidate) { - $paths.Add($candidate) - } - } - } - - return @($paths | Sort-Object -Unique) -} - -function Get-ActivityWatchRecoveryTaskDefinitions { - param([string[]]$ConfigPaths) - - $taskMap = [ordered]@{} - foreach ($candidatePath in @($ConfigPaths)) { - try { - $config = Read-ActivityWatchDeploymentConfig -Path $candidatePath - foreach ($task in @($config.userTasks)) { - $taskName = [string]$task.launchTaskName - $userId = Normalize-ActivityWatchUserId -UserId ([string]$task.userId) - $canonicalTaskName = "ActivityWatch Launch [$((Get-ActivityWatchTaskNameToken -UserId $userId))]" - if ($canonicalTaskName -ne $taskName -and (Test-ActivityWatchScheduledTaskExistsExact -TaskName $canonicalTaskName)) { - $taskName = $canonicalTaskName - } - if (-not [string]::IsNullOrWhiteSpace($taskName) -and -not $taskMap.Contains($taskName)) { - $taskMap[$taskName] = [pscustomobject]@{ - taskName = $taskName - userId = $userId - } - } - } - } - catch { - } - } - - return @($taskMap.Values) -} - -function New-ActivityWatchRecoveryLock { - param([string]$PrimaryConfigPath) - - $stateRoot = if ($PrimaryConfigPath) { Split-Path -Path $PrimaryConfigPath -Parent } else { Join-Path $env:ProgramData 'AWatch-rus' } - if (-not (Test-Path -LiteralPath $stateRoot)) { - New-Item -Path $stateRoot -ItemType Directory -Force | Out-Null - } - - $lockPath = Join-Path $stateRoot 'recovery-loop.lock' - if (Test-Path -LiteralPath $lockPath) { - try { - $lockData = Get-Content -LiteralPath $lockPath -Raw | ConvertFrom-Json - $existingPid = [int]$lockData.pid - if ($existingPid -gt 0 -and (Get-Process -Id $existingPid -ErrorAction SilentlyContinue)) { - return $null - } - } - catch { - } - } - - $payload = @{ - pid = $PID - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath $lockPath -Value $payload -Encoding UTF8 - return $lockPath -} - -function Test-ActivityWatchCollectorRunningGlobal { - param([string]$ScriptPath) - - if ([string]::IsNullOrWhiteSpace($ScriptPath)) { - return $false - } - - return [bool]@( - Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - ($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and - $_.CommandLine -match [Regex]::Escape($ScriptPath) - } - ).Count -} - -function Start-ActivityWatchCollectorScriptGlobalIfNeeded { - param( - [string]$ScriptPath, - [string]$ConfigPath - ) - - if ([string]::IsNullOrWhiteSpace($ScriptPath)) { - return - } - - if (-not (Test-Path -LiteralPath $ScriptPath)) { - return - } - - if (Test-ActivityWatchCollectorRunningGlobal -ScriptPath $ScriptPath) { - return - } - - $powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' - $argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass', '-File', $ScriptPath, '-ConfigPath', $ConfigPath) - Start-Process -FilePath $powershellExe -ArgumentList $argumentList -WindowStyle Hidden -} - -function Start-ActivityWatchTaskIfNotRunning { - param( - [string]$TaskName, - [string]$UserId, - [object[]]$SessionRecords - ) - - if ([string]::IsNullOrWhiteSpace($TaskName) -or [string]::IsNullOrWhiteSpace($UserId)) { - return $false - } - - if (-not (Test-ActivityWatchUserHasLiveSession -UserId $UserId -SessionRecords $SessionRecords)) { - return $false - } - - try { - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if (-not $task) { - return $false - } - if ([string]$task.State -eq 'Running') { - return $true - } - Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - return $true - } - catch { - return $false - } -} - -function Get-ActivityWatchLiveInteractiveSessions { - param([object[]]$SessionRecords) - - return @( - $SessionRecords | - Where-Object { - $_.IsLive -and - $_.SessionId -gt 0 -and - -not [string]::IsNullOrWhiteSpace([string]$_.UserName) - } | - Sort-Object @{ Expression = { if ([string]$_.SessionName -ieq 'console') { 0 } else { 1 } } }, @{ Expression = { [int]$_.SessionId } } - ) -} - -function Resolve-ActivityWatchLiveSessionUserId { - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$SessionRecord, - [pscustomobject[]]$TaskDefinitions - ) - - $rawUser = [string]$SessionRecord.UserName - if ([string]::IsNullOrWhiteSpace($rawUser)) { - return $null - } - - foreach ($taskDef in @($TaskDefinitions)) { - foreach ($candidate in @(Resolve-ActivityWatchUserCandidates -UserId [string]$taskDef.userId)) { - if ($candidate -ieq $rawUser -or - $candidate -ieq ('{0}\{1}' -f $env:COMPUTERNAME, $rawUser) -or - ((-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) -and $candidate -ieq ('{0}\{1}' -f $env:USERDOMAIN, $rawUser))) { - return [string]$taskDef.userId - } - } - } - - if ($rawUser -match '^[^\\]+\\') { - return $rawUser - } - - return ('{0}\{1}' -f $env:COMPUTERNAME, $rawUser) -} - -function Get-ActivityWatchExplorerUsersBySession { - $map = @{} - - try { - Get-Process explorer -IncludeUserName -ErrorAction SilentlyContinue | - Where-Object { $_.SessionId -gt 0 -and -not [string]::IsNullOrWhiteSpace([string]$_.UserName) } | - Sort-Object SessionId, StartTime | - ForEach-Object { - if (-not $map.ContainsKey([int]$_.SessionId)) { - $map[[int]$_.SessionId] = [string]$_.UserName - } - } - } - catch { - } - - return $map -} - -function Get-ActivityWatchDisconnectedInteractiveSessions { - param([object[]]$SessionRecords) - - $explorerUsers = Get-ActivityWatchExplorerUsersBySession - $result = New-Object System.Collections.Generic.List[object] - - foreach ($session in @($SessionRecords | Where-Object { -not $_.IsLive -and $_.SessionId -gt 0 })) { - $resolvedUser = [string]$session.UserName - if ([string]::IsNullOrWhiteSpace($resolvedUser) -and $explorerUsers.ContainsKey([int]$session.SessionId)) { - $resolvedUser = [string]$explorerUsers[[int]$session.SessionId] - } - - if ([string]::IsNullOrWhiteSpace($resolvedUser)) { - continue - } - - $result.Add([pscustomobject]@{ - SessionName = [string]$session.SessionName - SessionId = [int]$session.SessionId - State = [string]$session.State - UserName = $resolvedUser - }) | Out-Null - } - - return @($result | Sort-Object SessionId -Unique) -} - -function Get-ActivityWatchMarkerDirectories { - param([string]$StateRoot) - - $roots = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - - if (-not [string]::IsNullOrWhiteSpace($StateRoot)) { - [void]$roots.Add((Join-Path $StateRoot 'markers')) - } - - $usersRoot = Join-Path $env:SystemDrive 'Users' - if (Test-Path -LiteralPath $usersRoot) { - foreach ($dir in @(Get-ChildItem -LiteralPath $usersRoot -Directory -ErrorAction SilentlyContinue)) { - [void]$roots.Add((Join-Path $dir.FullName 'AppData\Local\AWatch-rus\markers')) - } - } - - return @($roots) -} - -function Remove-ActivityWatchLogonMarkersForSession { - param( - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [int]$SessionId, - [string]$UserName - ) - - $userCandidates = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - if (-not [string]::IsNullOrWhiteSpace($UserName)) { - [void]$userCandidates.Add($UserName) - if ($UserName -match '^[^\\]+\\(.+)$') { - [void]$userCandidates.Add($Matches[1]) - } - } - - foreach ($markerDir in @(Get-ActivityWatchMarkerDirectories -StateRoot $StateRoot)) { - if (-not (Test-Path -LiteralPath $markerDir)) { - continue - } - - foreach ($marker in @(Get-ChildItem -LiteralPath $markerDir -Filter '*.marker' -File -ErrorAction SilentlyContinue)) { - $name = [string]$marker.BaseName - if ($name -notmatch '^logon-(.+)-(\d+)-') { - continue - } - - $markerUser = [string]$Matches[1] - $markerSessionId = [int]$Matches[2] - if ($markerSessionId -ne $SessionId) { - continue - } - - if ($userCandidates.Count -gt 0 -and -not $userCandidates.Contains($markerUser)) { - continue - } - - Remove-Item -LiteralPath $marker.FullName -Force -ErrorAction SilentlyContinue - } - } -} - -function Stop-ActivityWatchProcessesInNonLiveSessions { - param( - [Parameter(Mandatory = $true)] - [object[]]$SessionRecords, - [Parameter(Mandatory = $true)] - [pscustomobject]$Config, - [pscustomobject[]]$TaskDefinitions = @(), - [switch]$PreserveManagedSessions - ) - - $stateRoot = if ($Config.paths.PSObject.Properties.Name -contains 'stateRoot') { [string]$Config.paths.stateRoot } else { Join-Path $env:ProgramData 'AWatch-rus' } - $preservedSessionIds = @() - if ($PreserveManagedSessions.IsPresent) { - $preservedSessionIds = @( - Get-ActivityWatchManagedInteractiveSessions -TaskDefinitions $TaskDefinitions -SessionRecords $SessionRecords -IncludeDisconnected | - ForEach-Object { [int]$_.SessionId } | - Sort-Object -Unique - ) - } - - $sessionIds = @( - $SessionRecords | - Where-Object { -not $_.IsLive -and $_.SessionId -gt 0 -and ($preservedSessionIds -notcontains [int]$_.SessionId) } | - ForEach-Object { [int]$_.SessionId } | - Sort-Object -Unique - ) - - if (-not $sessionIds -or $sessionIds.Count -eq 0) { - return - } - - $sessionScopedScripts = New-Object System.Collections.Generic.List[string] - foreach ($propertyName in @('collectorScript', 'endpointCollectorScript', 'fileCollectorScript', 'emailCollectorScript', 'launchScript')) { - if ($Config.paths.PSObject.Properties.Name -contains $propertyName) { - $candidatePath = [string]$Config.paths.$propertyName - if (-not [string]::IsNullOrWhiteSpace($candidatePath)) { - $sessionScopedScripts.Add($candidatePath) | Out-Null - } - } - } - - foreach ($session in @($SessionRecords | Where-Object { -not $_.IsLive -and $_.SessionId -gt 0 -and ($preservedSessionIds -notcontains [int]$_.SessionId) })) { - Remove-ActivityWatchLogonMarkersForSession -StateRoot $stateRoot -SessionId ([int]$session.SessionId) -UserName ([string]$session.UserName) - } - - Get-Process -Name 'aw-watcher-afk','aw-watcher-window' -ErrorAction SilentlyContinue | - Where-Object { $sessionIds -contains [int]$_.SessionId } | - ForEach-Object { - Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue - } - - Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - ($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and - ($sessionIds -contains [int]$_.SessionId) - } | - ForEach-Object { - $commandLine = [string]$_.CommandLine - foreach ($scriptPath in $sessionScopedScripts) { - if (-not [string]::IsNullOrWhiteSpace($scriptPath) -and $commandLine -match [Regex]::Escape($scriptPath)) { - Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue - break - } - } - } -} - -function Promote-ActivityWatchDisconnectedSessionToConsole { - param( - [pscustomobject[]]$TaskDefinitions, - [object[]]$SessionRecords - ) - - $candidates = Get-ActivityWatchDisconnectedInteractiveSessions -SessionRecords $SessionRecords - if (-not $candidates -or $candidates.Count -eq 0) { - return $false - } - - $selected = $null - foreach ($taskDef in @($TaskDefinitions)) { - foreach ($candidate in @($candidates)) { - foreach ($knownUser in @(Resolve-ActivityWatchUserCandidates -UserId [string]$taskDef.userId)) { - if ($knownUser -ieq [string]$candidate.UserName -or - $knownUser -ieq ('{0}\{1}' -f $env:COMPUTERNAME, [string]$candidate.UserName) -or - ((-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) -and $knownUser -ieq ('{0}\{1}' -f $env:USERDOMAIN, [string]$candidate.UserName))) { - $selected = $candidate - break - } - } - if ($selected) { break } - } - if ($selected) { break } - } - - if (-not $selected) { - $selected = $candidates | Select-Object -First 1 - } - - if (-not $selected) { - return $false - } - - try { - & cmd.exe /c ("tscon {0} /dest:console" -f [int]$selected.SessionId) | Out-Null - return ($LASTEXITCODE -eq 0) - } - catch { - return $false - } -} - -function Ensure-ActivityWatchLaunchTaskForUser { - param( - [Parameter(Mandatory = $true)] - [string]$UserId, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - if ([string]::IsNullOrWhiteSpace($UserId) -or -not (Test-Path -LiteralPath $LaunchScriptPath)) { - return $null - } - - $taskName = "ActivityWatch Launch [$((Get-ActivityWatchTaskNameToken -UserId $UserId))]" - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $UserId - $principal = New-ScheduledTaskPrincipal -UserId $UserId -LogonType Interactive -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - - try { - $existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue - if ($existingTask) { - $existingUserId = [string]$existingTask.Principal.UserId - $existingArgs = @($existingTask.Actions | ForEach-Object { [string]$_.Arguments }) -join ' ' - if ($existingUserId -ieq $UserId -and $existingArgs -like "*$launcherPath*") { - return $taskName - } - - Remove-ActivityWatchScheduledTask -TaskName $taskName - } - - Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null - return $taskName - } - catch { - return $null - } -} - -function Start-ActivityWatchConsoleFallbackIfNeeded { - param( - [pscustomobject[]]$TaskDefinitions, - [object[]]$SessionRecords, - [pscustomobject]$Config, - [string]$ConfigPath, - [bool]$ConfiguredLiveTasksStarted - ) - - if ($ConfiguredLiveTasksStarted) { - return - } - - $liveSessions = Get-ActivityWatchLiveInteractiveSessions -SessionRecords $SessionRecords - if (-not $liveSessions -or $liveSessions.Count -eq 0) { - if (Promote-ActivityWatchDisconnectedSessionToConsole -TaskDefinitions $TaskDefinitions -SessionRecords $SessionRecords) { - Start-Sleep -Seconds 3 - $SessionRecords = Get-ActivityWatchSessionRecords - $liveSessions = Get-ActivityWatchLiveInteractiveSessions -SessionRecords $SessionRecords - } - } - if (-not $liveSessions -or $liveSessions.Count -eq 0) { - return - } - - $launchScriptPath = if ($Config.paths.PSObject.Properties.Name -contains 'launchScript') { [string]$Config.paths.launchScript } else { $null } - if ([string]::IsNullOrWhiteSpace($launchScriptPath)) { - return - } - - $preferredSession = $liveSessions | Select-Object -First 1 - $userId = Resolve-ActivityWatchLiveSessionUserId -SessionRecord $preferredSession -TaskDefinitions $TaskDefinitions - if ([string]::IsNullOrWhiteSpace($userId)) { - return - } - - $taskName = Ensure-ActivityWatchLaunchTaskForUser -UserId $userId -LaunchScriptPath $launchScriptPath -ConfigPath $ConfigPath - if ([string]::IsNullOrWhiteSpace($taskName)) { - return - } - - try { - $task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue - if ($task -and [string]$task.State -ne 'Running') { - Start-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue - } - } - catch { - } -} - -function Invoke-ActivityWatchRecoveryLoop { - param([string]$ConfigPath) - - $recoveryLockPath = New-ActivityWatchRecoveryLock -PrimaryConfigPath $ConfigPath - if (-not $recoveryLockPath) { - return - } - - try { - while ($true) { - $sleepSeconds = 180 - try { - $configPaths = Get-ActivityWatchRecoveryConfigPaths -PrimaryConfigPath $ConfigPath - $config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath - $taskDefs = Get-ActivityWatchRecoveryTaskDefinitions -ConfigPaths $configPaths - $sessionRecords = Get-ActivityWatchSessionRecords - Stop-ActivityWatchProcessesInNonLiveSessions -SessionRecords $sessionRecords -Config $config -TaskDefinitions $taskDefs -PreserveManagedSessions - $sessionRecords = Get-ActivityWatchSessionRecords - $stateRoot = [string]$config.paths.stateRoot - $sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' } - Start-ActivityWatchCollectorScriptGlobalIfNeeded -ScriptPath $sessionCollectorScript -ConfigPath $ConfigPath - - $configuredLiveTasksStarted = $false - foreach ($taskDef in $taskDefs) { - if (Start-ActivityWatchTaskIfNotRunning -TaskName $taskDef.taskName -UserId $taskDef.userId -SessionRecords $sessionRecords) { - $configuredLiveTasksStarted = $true - } - } - - Start-ActivityWatchConsoleFallbackIfNeeded -TaskDefinitions $taskDefs -SessionRecords $sessionRecords -Config $config -ConfigPath $ConfigPath -ConfiguredLiveTasksStarted $configuredLiveTasksStarted - - if ($config -and $config.recovery -and $config.recovery.intervalSeconds) { - $sleepSeconds = [Math]::Max([int]$config.recovery.intervalSeconds, 30) - } - } - catch { - } - - Start-Sleep -Seconds $sleepSeconds - } - } - finally { - if ($recoveryLockPath -and (Test-Path -LiteralPath $recoveryLockPath)) { - Remove-Item -LiteralPath $recoveryLockPath -Force -ErrorAction SilentlyContinue - } - } -} - -function Get-ActivityWatchHiddenLauncherPath { - param( - [Parameter(Mandatory = $true)] - [string]$ScriptPath - ) - - $directory = Split-Path -Path $ScriptPath -Parent - $baseName = [IO.Path]::GetFileNameWithoutExtension($ScriptPath) - return Join-Path $directory ("{0}-hidden.vbs" -f $baseName) -} - -function Write-ActivityWatchHiddenPowerShellWrapper { - param( - [Parameter(Mandatory = $true)] - [string]$Path, - [Parameter(Mandatory = $true)] - [string]$ScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory) { - New-ActivityWatchDirectory -Path $directory - } - - $powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' - $escapedPowerShellExe = $powershellExe.Replace('"', '""') - $escapedScriptPath = $ScriptPath.Replace('"', '""') - $escapedConfigPath = $ConfigPath.Replace('"', '""') - - $content = @" -On Error Resume Next -Set shell = CreateObject("WScript.Shell") -q = Chr(34) -command = q & "$escapedPowerShellExe" & q & " -NoProfile -ExecutionPolicy Bypass -File " & q & "$escapedScriptPath" & q & " -ConfigPath " & q & "$escapedConfigPath" & q -shell.Run command, 0, False -If Err.Number <> 0 Then - WScript.Quit 1 -End If -WScript.Quit 0 -"@ - - Set-Content -LiteralPath $Path -Value $content -Encoding ASCII -} - -function Remove-LegacyActivityWatchEntries { - $legacyTaskNames = @( - 'ActivityWatch Watchers', - 'ActivityWatch Guard', - 'ActivityWatch Heal', - 'AWatchRusStandaloneAgent', - 'AWatch Worktime Collector', - 'AW DLP Endpoint ADMIN', - 'AW DLP Endpoint USER1' - ) - - foreach ($taskName in $legacyTaskNames) { - Remove-ActivityWatchScheduledTask -TaskName $taskName - } - - $legacyTaskPatterns = @( - 'browser-domains-native-collector.ps1', - 'file-operations-collector.ps1', - 'dlp-endpoint-signals-collector.ps1', - 'worktime-session-collector.ps1', - 'aw-standalone-service.ps1' - ) - $managedTaskNames = @( - 'ActivityWatch Recovery', - 'ActivityWatch Hayabusa Upload', - 'ActivityWatch File1C Upload' - ) - - $scheduledTasks = @() - try { - $scheduledTasks = @(Get-ScheduledTask -ErrorAction Stop) - } - catch { - $scheduledTasks = @() - } - - foreach ($task in $scheduledTasks) { - $taskName = [string]$task.TaskName - if ([string]::IsNullOrWhiteSpace($taskName) -or $managedTaskNames -contains $taskName -or $taskName -like 'ActivityWatch Launch *') { - continue - } - - $isLegacyCollectorTask = $false - foreach ($action in @($task.Actions)) { - $execute = if ($action.PSObject.Properties.Name -contains 'Execute') { [string]$action.Execute } else { '' } - $arguments = if ($action.PSObject.Properties.Name -contains 'Arguments') { [string]$action.Arguments } else { '' } - $commandLine = ('{0} {1}' -f $execute, $arguments).Trim() - if ([string]::IsNullOrWhiteSpace($commandLine)) { - continue - } - foreach ($pattern in $legacyTaskPatterns) { - if ($commandLine -match [Regex]::Escape($pattern)) { - $isLegacyCollectorTask = $true - break - } - } - if ($isLegacyCollectorTask) { - break - } - } - - if ($isLegacyCollectorTask) { - Remove-ActivityWatchScheduledTask -TaskName $taskName - } - } - - $runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' - foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') { - Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue - } -} - -function Remove-ActivityWatchScheduledTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName - ) - - try { - Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction Stop - } - catch { - } - - & cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null - if ($LASTEXITCODE -eq 0) { - return - } - - for ($attempt = 0; $attempt -lt 10; $attempt++) { - $task = $null - try { - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop - } - catch { - & cmd.exe /c "schtasks /Query /TN `"$TaskName`" >nul 2>&1" | Out-Null - if ($LASTEXITCODE -ne 0) { - return - } - } - if (-not $task) { - return - } - - Start-Sleep -Milliseconds 300 - } -} - -function Set-ActivityWatchScheduledTaskAction { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$Execute, - [Parameter(Mandatory = $true)] - [string]$Arguments - ) - - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if (-not $task) { - return $false - } - - $newAction = New-ScheduledTaskAction -Execute $Execute -Argument $Arguments - try { - # Non-interactive update path. Avoids schtasks.exe /Change password prompt for user-bound tasks. - Set-ScheduledTask -TaskName $TaskName -Action $newAction -ErrorAction Stop | Out-Null - return $true - } - catch { - $taskCommand = ('"{0}" {1}' -f $Execute, $Arguments) - & schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null - if ($LASTEXITCODE -ne 0) { - Write-Host "skip task action update for $TaskName because the existing principal/action cannot be updated non-interactively: $($_.Exception.Message)" - return $false - } - return $true - } -} - -function Get-ActivityWatchScheduledTaskByCommand { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [string]$CommandMatch - ) - - $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue - if ($task) { - return $task - } - - if ([string]::IsNullOrWhiteSpace($CommandMatch)) { - return $null - } - - foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { - foreach ($action in @($candidate.Actions)) { - if ([string]$action.Arguments -like "*$CommandMatch*") { - return $candidate - } - } - } - - 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)] - [pscustomobject[]]$TaskDefinitions, - [Parameter(Mandatory = $true)] - [string]$LaunchScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $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`"" - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId - $principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType Interactive -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - $existingTask = Get-ActivityWatchScheduledTaskByCommand -TaskName $definition.LaunchTaskName -CommandMatch $ConfigPath - - if ($existingTask) { - $updated = Set-ActivityWatchScheduledTaskAction -TaskName $existingTask.TaskName -Execute $wscriptExe -Arguments $action.Arguments - if ($updated) { - continue - } - } - - Remove-ActivityWatchScheduledTask -TaskName $definition.LaunchTaskName - Register-ScheduledTask -TaskName $definition.LaunchTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null - } -} - -function Register-ActivityWatchRecoveryTask { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName, - [Parameter(Mandatory = $true)] - [string]$RecoveryScriptPath, - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - Remove-ActivityWatchScheduledTask -TaskName $TaskName - - $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' - $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath - Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath - $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" - $sessionRecords = @() - try { - $sessionRecords = @(Get-ActivityWatchSessionRecords) - } - catch { - $sessionRecords = @() - } - $liveSession = @(Get-ActivityWatchLiveInteractiveSessions -SessionRecords $sessionRecords) | Select-Object -First 1 - $interactiveUserId = $null - if ($liveSession -and -not [string]::IsNullOrWhiteSpace([string]$liveSession.UserName)) { - $rawUser = [string]$liveSession.UserName - $interactiveUserId = if ($rawUser -match '^[^\\]+\\') { $rawUser } else { ('{0}\{1}' -f $env:COMPUTERNAME, $rawUser) } - } - - if ($interactiveUserId) { - $trigger = New-ScheduledTaskTrigger -AtLogOn -User $interactiveUserId - $principal = New-ScheduledTaskPrincipal -UserId $interactiveUserId -LogonType Interactive -RunLevel Highest - } - else { - $trigger = New-ScheduledTaskTrigger -AtStartup - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - } - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) - - try { - Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -ErrorAction Stop | Out-Null - } - catch { - $taskCommand = ('"{0}" {1}' -f $wscriptExe, $action.Arguments) - if ($interactiveUserId) { - & schtasks.exe /Create /TN $TaskName /SC ONLOGON /RU $interactiveUserId /IT /RL HIGHEST /F /TR $taskCommand | Out-Null - } - else { - & schtasks.exe /Create /TN $TaskName /SC ONSTART /RU SYSTEM /RL HIGHEST /F /TR $taskCommand | Out-Null - } - if ($LASTEXITCODE -ne 0) { - throw - } - } -} - -function Register-ActivityWatchHayabusaAutoUploadTask { - param( - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath - $forensics = $config.forensics - if ($null -eq $forensics -or $forensics.PSObject.Properties.Name -notcontains 'hayabusaAutomation') { - return - } - - $automation = $forensics.hayabusaAutomation - $taskName = if ($automation.PSObject.Properties.Name -contains 'taskName' -and -not [string]::IsNullOrWhiteSpace([string]$automation.taskName)) { - [string]$automation.taskName - } else { - 'ActivityWatch Hayabusa Upload' - } - - if (-not [bool]$automation.enabled) { - Remove-ActivityWatchScheduledTask -TaskName $taskName - return - } - - $uploadScript = if ($config.paths.PSObject.Properties.Name -contains 'hayabusaUploadScript') { [string]$config.paths.hayabusaUploadScript } else { Join-Path $config.paths.stateRoot 'export-upload-hayabusa-to-aw-server.ps1' } - if (-not (Test-Path -LiteralPath $uploadScript)) { - throw "Не найден скрипт Hayabusa upload: $uploadScript" - } - - $intervalHours = [Math]::Max(1, [int]$automation.intervalHours) - $hoursBack = [Math]::Max(1, [int]$automation.hoursBack) - $mode = if ($automation.PSObject.Properties.Name -contains 'mode' -and -not [string]::IsNullOrWhiteSpace([string]$automation.mode)) { [string]$automation.mode } else { 'incident' } - $powerShellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' - $taskCommand = "`"$powerShellExe`" -NoProfile -ExecutionPolicy Bypass -File `"$uploadScript`" -ConfigPath `"$ConfigPath`" -HoursBack $hoursBack -Mode `"$mode`"" - - Remove-ActivityWatchScheduledTask -TaskName $taskName - & schtasks.exe /Create /TN $taskName /TR $taskCommand /SC HOURLY /MO $intervalHours /ST 00:00 /RU SYSTEM /RL HIGHEST /F | Out-Null - if ($LASTEXITCODE -ne 0) { - throw "Не удалось создать scheduled task $taskName через schtasks.exe" - } -} - -function Register-ActivityWatchFile1CAutoUploadTask { - param( - [Parameter(Mandatory = $true)] - [string]$ConfigPath - ) - - $config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath - if ($config.PSObject.Properties.Name -notcontains 'analytics' -or - $config.analytics.PSObject.Properties.Name -notcontains 'file1cAutomation') { - return - } - - $automation = $config.analytics.file1cAutomation - $taskName = if ($automation.PSObject.Properties.Name -contains 'taskName' -and -not [string]::IsNullOrWhiteSpace([string]$automation.taskName)) { - [string]$automation.taskName - } else { - 'ActivityWatch File1C Upload' - } - - if (-not [bool]$automation.enabled) { - Remove-ActivityWatchScheduledTask -TaskName $taskName - return - } - - $uploadScript = if ($config.paths.PSObject.Properties.Name -contains 'file1cTelemetryScript') { [string]$config.paths.file1cTelemetryScript } else { Join-Path $config.paths.stateRoot 'export-upload-file-1c-telemetry.ps1' } - if (-not (Test-Path -LiteralPath $uploadScript)) { - throw "Не найден скрипт file-1C telemetry upload: $uploadScript" - } - - $intervalHours = [Math]::Max(1, [int]$automation.intervalHours) - $powerShellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' - $taskCommand = "`"$powerShellExe`" -NoProfile -ExecutionPolicy Bypass -File `"$uploadScript`" -ConfigPath `"$ConfigPath`"" - $runnerUserId = $null - if ($automation.PSObject.Properties.Name -contains 'runAsUser' -and -not [string]::IsNullOrWhiteSpace([string]$automation.runAsUser)) { - $runnerUserId = [string]$automation.runAsUser - } - if ([string]::IsNullOrWhiteSpace($runnerUserId) -and $config.PSObject.Properties.Name -contains 'userTasks') { - $runnerUserId = @( - @($config.userTasks | ForEach-Object { [string]$_.userId }) | - Where-Object { $_ -match '(^|\\)(Администратор|Administrator)$' } | - Select-Object -First 1 - ) | Select-Object -First 1 - } - if ([string]::IsNullOrWhiteSpace($runnerUserId) -and $config.PSObject.Properties.Name -contains 'userTasks') { - $runnerUserId = @($config.userTasks | ForEach-Object { [string]$_.userId } | Select-Object -First 1) | Select-Object -First 1 - } - - Remove-ActivityWatchScheduledTask -TaskName $taskName - if (-not [string]::IsNullOrWhiteSpace($runnerUserId)) { - & schtasks.exe /Create /TN $taskName /TR $taskCommand /SC HOURLY /MO $intervalHours /ST 00:00 /RU $runnerUserId /RL HIGHEST /F | Out-Null - } - else { - & schtasks.exe /Create /TN $taskName /TR $taskCommand /SC HOURLY /MO $intervalHours /ST 00:00 /RU SYSTEM /RL HIGHEST /F | Out-Null - } - if ($LASTEXITCODE -ne 0) { - throw "Не удалось создать scheduled task $taskName через schtasks.exe" - } -} - -function Set-ActivityWatchAcl { - param( - [Parameter(Mandatory = $true)] - [string]$InstallRoot, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$LogsRoot - ) - - foreach ($path in $InstallRoot, $StateRoot, $LogsRoot) { - New-ActivityWatchDirectory -Path $path - } - - & 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 завершился с ошибкой для $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)(M)' | Out-Null - if ($LASTEXITCODE -ne 0) { - 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 завершился с ошибкой для $LogsRoot" - } -} - -function Start-ActivityWatchTasks { - param( - [Parameter(Mandatory = $true)] - [pscustomobject[]]$TaskDefinitions, - [string]$RecoveryTaskName = 'ActivityWatch Recovery' - ) - - $sessionRecords = Get-ActivityWatchSessionRecords - - foreach ($definition in $TaskDefinitions) { - if (Test-ActivityWatchUserHasLiveSession -UserId $definition.UserId -SessionRecords $sessionRecords) { - Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue - } - } - - Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue -} - -Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries diff --git a/install-kit-awindows-20260427-211240/windows/aw-collector-guard.ps1 b/install-kit-awindows-20260427-211240/windows/aw-collector-guard.ps1 deleted file mode 100644 index 559995e..0000000 --- a/install-kit-awindows-20260427-211240/windows/aw-collector-guard.ps1 +++ /dev/null @@ -1,681 +0,0 @@ -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [ValidateSet('shadow', 'enforce')] - [string]$Mode = 'shadow', - [int]$LoopSeconds = 60, - [int]$InteractiveMaxAgeSeconds = 900, - [int]$HeadlessMaxAgeSeconds = 900, - [int]$RestartWindowSeconds = 600, - [int]$MaxRestarts = 3, - [int]$ActionCooldownSeconds = 300, - [int]$InteractiveActionCooldownSeconds = 60, - [switch]$Once, - [switch]$HeadlessEndpointEnabled, - [switch]$HeadlessFileOpsEnabled, - [switch]$SelfTest -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' -Import-Module $modulePath -Force - -function New-GuardLock { - param([string]$StateRoot) - - if (-not (Test-Path -LiteralPath $StateRoot)) { - New-Item -Path $StateRoot -ItemType Directory -Force | Out-Null - } - - $lockPath = Join-Path $StateRoot 'collector-guard.lock' - if (Test-Path -LiteralPath $lockPath) { - try { - $lockData = Get-Content -LiteralPath $lockPath -Raw | ConvertFrom-Json - $existingPid = [int]$lockData.pid - if ($existingPid -gt 0 -and (Get-Process -Id $existingPid -ErrorAction SilentlyContinue)) { - return $null - } - } - catch { - } - } - - $payload = @{ - pid = $PID - createdAt = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Compress - Set-Content -LiteralPath $lockPath -Value $payload -Encoding UTF8 - return $lockPath -} - -function Write-GuardLog { - param( - [string]$LogPath, - [string]$Message - ) - - try { - $directory = Split-Path -Path $LogPath -Parent - if ($directory -and -not (Test-Path -LiteralPath $directory)) { - New-Item -Path $directory -ItemType Directory -Force | Out-Null - } - Add-Content -LiteralPath $LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Get-AwApiBase { - param([pscustomobject]$Config) - - $scheme = if ($Config.server.PSObject.Properties.Name -contains 'scheme') { [string]$Config.server.scheme } else { 'http' } - $hostName = [string]$Config.server.host - $port = [int]$Config.server.port - return ('{0}://{1}:{2}/api/0' -f $scheme, $hostName, $port) -} - -function Invoke-AwJson { - param( - [Parameter(Mandatory = $true)] - [string]$Method, - [Parameter(Mandatory = $true)] - [string]$Uri, - [object]$Body - ) - - $params = @{ - Method = $Method - Uri = $Uri - TimeoutSec = 15 - ErrorAction = 'Stop' - } - if ($null -ne $Body) { - $params.Body = ($Body | ConvertTo-Json -Depth 16 -Compress) - $params.ContentType = 'application/json' - } - return Invoke-RestMethod @params -} - -function Ensure-AwBucket { - param( - [string]$ApiBase, - [string]$BucketId, - [string]$ClientName, - [string]$BucketType, - [string]$Hostname - ) - - try { - Invoke-AwJson -Method 'GET' -Uri "$ApiBase/buckets/$BucketId" | Out-Null - return $true - } - catch { - } - - try { - $body = @{ - client = $ClientName - type = $BucketType - hostname = $Hostname - } - Invoke-AwJson -Method 'POST' -Uri "$ApiBase/buckets/$BucketId" -Body $body | Out-Null - return $true - } - catch { - return $false - } -} - -function Get-LatestBucketAge { - param( - [string]$ApiBase, - [string]$BucketId - ) - - try { - $events = Invoke-AwJson -Method 'GET' -Uri "$ApiBase/buckets/$BucketId/events?limit=20" - $latest = @($events | Where-Object { $null -ne $_.timestamp } | Sort-Object timestamp -Descending | Select-Object -First 1) - if (-not $latest) { - return [pscustomobject]@{ bucket = $BucketId; found = $false; timestamp = $null; ageSeconds = $null } - } - $ts = [DateTimeOffset]::Parse([string]$latest.timestamp).UtcDateTime - $age = [Math]::Max(0, [int]((Get-Date).ToUniversalTime() - $ts).TotalSeconds) - return [pscustomobject]@{ bucket = $BucketId; found = $true; timestamp = [string]$latest.timestamp; ageSeconds = $age } - } - catch { - return [pscustomobject]@{ bucket = $BucketId; found = $false; timestamp = $null; ageSeconds = $null; error = $_.Exception.Message } - } -} - -function Send-GuardHeartbeat { - param( - [string]$ApiBase, - [string]$Hostname, - [object]$State, - [int]$PulseSeconds - ) - - $bucketId = "aw-rus-collector-guard_$Hostname" - if (-not (Ensure-AwBucket -ApiBase $ApiBase -BucketId $bucketId -ClientName 'aw-rus-collector-guard' -BucketType 'aw.rus.collector.guard' -Hostname $Hostname)) { - return $false - } - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('o') - duration = 0 - data = $State - } - - try { - Invoke-AwJson -Method 'POST' -Uri "$ApiBase/buckets/$bucketId/heartbeat?pulsetime=$PulseSeconds" -Body $event | Out-Null - return $true - } - catch { - return $false - } -} - -function Read-GuardRuntime { - param([string]$Path) - - if (-not (Test-Path -LiteralPath $Path)) { - return [pscustomobject]@{ restartHistory = @{}; lastAction = @{}; quarantine = @{} } - } - try { - $state = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($null -eq $state.restartHistory) { $state | Add-Member -NotePropertyName restartHistory -NotePropertyValue @{} } - if ($null -eq $state.lastAction) { $state | Add-Member -NotePropertyName lastAction -NotePropertyValue @{} } - if ($null -eq $state.quarantine) { $state | Add-Member -NotePropertyName quarantine -NotePropertyValue @{} } - return $state - } - catch { - return [pscustomobject]@{ restartHistory = @{}; lastAction = @{}; quarantine = @{} } - } -} - -function Write-GuardRuntime { - param( - [string]$Path, - [object]$Runtime - ) - - $directory = Split-Path -Path $Path -Parent - if ($directory -and -not (Test-Path -LiteralPath $directory)) { - New-Item -Path $directory -ItemType Directory -Force | Out-Null - } - $Runtime | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $Path -Encoding UTF8 -} - -function Get-RuntimeMapValue { - param( - [object]$Map, - [string]$Key - ) - - if ($null -eq $Map) { - return $null - } - if ($Map -is [hashtable] -and $Map.ContainsKey($Key)) { - return $Map[$Key] - } - $propertyNames = @($Map.PSObject.Properties | ForEach-Object { $_.Name }) - if ($propertyNames -contains $Key) { - return $Map.$Key - } - return $null -} - -function Set-RuntimeMapValue { - param( - [object]$Map, - [string]$Key, - [object]$Value - ) - - if ($Map -is [hashtable]) { - $Map[$Key] = $Value - return - } - $propertyNames = @($Map.PSObject.Properties | ForEach-Object { $_.Name }) - if ($propertyNames -contains $Key) { - $Map.$Key = $Value - } - else { - $Map | Add-Member -NotePropertyName $Key -NotePropertyValue $Value -Force - } -} - -function Remove-RuntimeMapValue { - param( - [object]$Map, - [string]$Key - ) - - if ($null -eq $Map) { - return - } - if ($Map -is [hashtable]) { - if ($Map.ContainsKey($Key)) { - $Map.Remove($Key) - } - return - } - $property = $Map.PSObject.Properties[$Key] - if ($null -ne $property) { - $Map.PSObject.Properties.Remove($Key) - } -} - -function Reset-GuardActionBudget { - param( - [object]$Runtime, - [string]$Key - ) - - Remove-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key - Remove-RuntimeMapValue -Map $Runtime.lastAction -Key $Key - Remove-RuntimeMapValue -Map $Runtime.quarantine -Key $Key -} - -function Invoke-GuardSelfTest { - $emptyObject = [pscustomobject]@{} - if ($null -ne (Get-RuntimeMapValue -Map $emptyObject -Key 'missing')) { - throw 'empty PSCustomObject should not return a missing runtime-map value' - } - Set-RuntimeMapValue -Map $emptyObject -Key 'headless:worktime-session' -Value 123 - if ((Get-RuntimeMapValue -Map $emptyObject -Key 'headless:worktime-session') -ne 123) { - throw 'failed to set runtime-map value on empty PSCustomObject' - } - - $hash = @{} - Set-RuntimeMapValue -Map $hash -Key 'headless:worktime-session' -Value @(1, 2) - $hashValue = @(Get-RuntimeMapValue -Map $hash -Key 'headless:worktime-session') - if ($hashValue.Count -ne 2) { - throw 'failed to round-trip runtime-map value on hashtable' - } - - $runtime = [pscustomobject]@{ restartHistory = [pscustomobject]@{}; lastAction = [pscustomobject]@{}; quarantine = [pscustomobject]@{} } - $allowed = Test-ActionAllowed -Runtime $runtime -Key 'headless:worktime-session' -CooldownSeconds 1 -WindowSeconds 60 -MaxCount 3 - if (-not $allowed.allowed) { - throw "expected action to be allowed, got $($allowed.reason)" - } - Register-GuardAction -Runtime $runtime -Key 'headless:worktime-session' - $blocked = Test-ActionAllowed -Runtime $runtime -Key 'headless:worktime-session' -CooldownSeconds 300 -WindowSeconds 60 -MaxCount 3 - if ($blocked.allowed -or $blocked.reason -ne 'cooldown') { - throw 'expected cooldown after registering guard action' - } - $budgetRuntime = [pscustomobject]@{ restartHistory = [pscustomobject]@{}; lastAction = [pscustomobject]@{}; quarantine = [pscustomobject]@{} } - foreach ($i in 1..3) { - Register-GuardAction -Runtime $budgetRuntime -Key 'task:test' - } - $budgetBlocked = Test-ActionAllowed -Runtime $budgetRuntime -Key 'task:test' -CooldownSeconds 0 -WindowSeconds 600 -MaxCount 3 - if ($budgetBlocked.allowed -or $budgetBlocked.reason -ne 'quarantine') { - throw 'expected quarantine when restart budget is exhausted' - } - Reset-GuardActionBudget -Runtime $budgetRuntime -Key 'task:test' - $budgetAllowed = Test-ActionAllowed -Runtime $budgetRuntime -Key 'task:test' -CooldownSeconds 0 -WindowSeconds 600 -MaxCount 3 - if (-not $budgetAllowed.allowed) { - throw 'expected reset action budget to clear quarantine' - } - - $oldComputerName = $env:COMPUTERNAME - try { - $env:COMPUTERNAME = 'SHARKON2025' - $sessionRecords = @( - [pscustomobject]@{ SessionName = 'USER5'; UserName = 'USER5'; SessionId = 2; State = 'Disc'; IsLive = $false }, - [pscustomobject]@{ SessionName = 'console'; UserName = ''; SessionId = 1; State = 'Conn'; IsLive = $true } - ) - $taskDefs = @( - [pscustomobject]@{ taskName = 'ActivityWatch Launch [SHARKON2025_user5]'; userId = 'SHARKON2025\user5' } - ) - if (-not (Test-ActivityWatchUserHasManagedSession -UserId 'SHARKON2025\user5' -SessionRecords $sessionRecords -IncludeDisconnected)) { - throw 'expected disconnected managed session to match task user' - } - if (Test-ActivityWatchUserHasManagedSession -UserId 'SHARKON2025\user5' -SessionRecords $sessionRecords -IncludeLive) { - throw 'disconnected managed session should not match live-only filter' - } - $managed = @(Get-ActivityWatchManagedInteractiveSessions -TaskDefinitions $taskDefs -SessionRecords $sessionRecords -IncludeDisconnected) - if ($managed.Count -ne 1 -or [int]$managed[0].SessionId -ne 2) { - throw 'failed to enumerate disconnected managed session' - } - } - finally { - if ($null -eq $oldComputerName) { - Remove-Item Env:COMPUTERNAME -ErrorAction SilentlyContinue - } - else { - $env:COMPUTERNAME = $oldComputerName - } - } - - Write-Output 'collector guard self-test OK' -} - -function Test-ActionAllowed { - param( - [object]$Runtime, - [string]$Key, - [int]$CooldownSeconds, - [int]$WindowSeconds, - [int]$MaxCount - ) - - $now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - $last = Get-RuntimeMapValue -Map $Runtime.lastAction -Key $Key - if ($null -ne $last -and ($now - [int64]$last) -lt $CooldownSeconds) { - return [pscustomobject]@{ allowed = $false; reason = 'cooldown' } - } - - $history = @(Get-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key) - $history = @($history | Where-Object { ($now - [int64]$_) -le $WindowSeconds }) - Set-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key -Value @($history) - if ($history.Count -ge $MaxCount) { - Set-RuntimeMapValue -Map $Runtime.quarantine -Key $Key -Value @{ - since = (Get-Date).ToUniversalTime().ToString('o') - reason = 'restart-budget-exhausted' - count = $history.Count - } - return [pscustomobject]@{ allowed = $false; reason = 'quarantine' } - } - - Remove-RuntimeMapValue -Map $Runtime.quarantine -Key $Key - return [pscustomobject]@{ allowed = $true; reason = 'ok' } -} - -function Register-GuardAction { - param( - [object]$Runtime, - [string]$Key - ) - - $now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - $history = @(Get-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key) - $history += $now - Set-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key -Value @($history) - Set-RuntimeMapValue -Map $Runtime.lastAction -Key $Key -Value $now -} - -function Get-CollectorProcessSnapshot { - param([pscustomobject]$Config) - - $scriptPaths = [ordered]@{} - foreach ($name in @('collectorScript', 'endpointCollectorScript', 'fileCollectorScript', 'emailCollectorScript', 'sessionCollectorScript')) { - if ($Config.paths.PSObject.Properties.Name -contains $name) { - $value = [string]$Config.paths.$name - if (-not [string]::IsNullOrWhiteSpace($value)) { - $scriptPaths[$name] = $value - } - } - } - - $powershellCollectors = @() - try { - $processes = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { $_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe' }) - foreach ($proc in $processes) { - $commandLine = [string]$proc.CommandLine - foreach ($entry in $scriptPaths.GetEnumerator()) { - if ($commandLine -match [Regex]::Escape([string]$entry.Value)) { - $powershellCollectors += [pscustomobject]@{ - name = [string]$entry.Key - processId = [int]$proc.ProcessId - sessionId = [int]$proc.SessionId - scriptPath = [string]$entry.Value - } - } - } - } - } - catch { - } - - $watchers = @() - try { - $watchers = @(Get-Process -Name 'aw-watcher-afk','aw-watcher-window' -ErrorAction SilentlyContinue | - Select-Object @{Name='name'; Expression={$_.Name}}, @{Name='processId'; Expression={$_.Id}}, @{Name='sessionId'; Expression={$_.SessionId}}) - } - catch { - $watchers = @() - } - - return [pscustomobject]@{ - watchers = @($watchers) - collectors = @($powershellCollectors) - } -} - -function Invoke-ExactTaskRun { - param([string]$TaskName) - - & schtasks.exe /Run /TN $TaskName | Out-Null - return ($LASTEXITCODE -eq 0) -} - -function Invoke-GuardCycle { - param( - [object]$Runtime, - [string]$RuntimePath, - [string]$LogPath - ) - - $config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath - $stateRoot = if ($config.paths.PSObject.Properties.Name -contains 'stateRoot') { [string]$config.paths.stateRoot } else { Split-Path -Path $ConfigPath -Parent } - $hostname = if ($config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$config.awHostname)) { [string]$config.awHostname } else { [string]$env:COMPUTERNAME } - $apiBase = Get-AwApiBase -Config $config - $configPaths = Get-ActivityWatchRecoveryConfigPaths -PrimaryConfigPath $ConfigPath - $taskDefs = @(Get-ActivityWatchRecoveryTaskDefinitions -ConfigPaths $configPaths) - $sessionRecords = @(Get-ActivityWatchSessionRecords) - $liveSessions = @(Get-ActivityWatchLiveInteractiveSessions -SessionRecords $sessionRecords) - $managedInteractiveSessions = @(Get-ActivityWatchManagedInteractiveSessions -TaskDefinitions $taskDefs -SessionRecords $sessionRecords -IncludeLive -IncludeDisconnected) - $processSnapshot = Get-CollectorProcessSnapshot -Config $config - $liveSessionIds = @($liveSessions | ForEach-Object { [int]$_.SessionId }) - $managedSessionIds = @($managedInteractiveSessions | ForEach-Object { [int]$_.SessionId } | Sort-Object -Unique) - - $bucketChecks = [ordered]@{} - foreach ($bucket in @( - "aw-worktime-sessions_$hostname", - "aw-watcher-afk_$hostname", - "aw-watcher-window_$hostname", - "aw-dlp-endpoint-signals_$hostname" - )) { - $bucketChecks[$bucket] = Get-LatestBucketAge -ApiBase $apiBase -BucketId $bucket - } - - $actions = New-Object System.Collections.Generic.List[object] - $problems = New-Object System.Collections.Generic.List[string] - - $worktimeAge = $bucketChecks["aw-worktime-sessions_$hostname"].ageSeconds - $sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' } - $sessionCollectorRunning = Test-ActivityWatchCollectorRunningGlobal -ScriptPath $sessionCollectorScript - $headlessKey = 'headless:worktime-session' - $needsHeadlessAction = (-not $sessionCollectorRunning -or $null -eq $worktimeAge -or [int]$worktimeAge -gt $HeadlessMaxAgeSeconds) - if ($needsHeadlessAction) { - $key = $headlessKey - $allowed = Test-ActionAllowed -Runtime $Runtime -Key $key -CooldownSeconds $ActionCooldownSeconds -WindowSeconds $RestartWindowSeconds -MaxCount $MaxRestarts - if ($allowed.allowed) { - if ($Mode -eq 'enforce') { - Start-ActivityWatchCollectorScriptGlobalIfNeeded -ScriptPath $sessionCollectorScript -ConfigPath $ConfigPath - Register-GuardAction -Runtime $Runtime -Key $key - Write-GuardLog -LogPath $LogPath -Message "started $key" - $actions.Add([pscustomobject]@{ action = 'start'; target = $key; applied = $true }) | Out-Null - } - else { - $actions.Add([pscustomobject]@{ action = 'start'; target = $key; applied = $false; mode = 'shadow' }) | Out-Null - } - } - else { - $problems.Add("$key action blocked: $($allowed.reason)") | Out-Null - } - } - else { - Reset-GuardActionBudget -Runtime $Runtime -Key $headlessKey - } - - if ($Mode -eq 'enforce') { - Stop-ActivityWatchProcessesInNonLiveSessions -SessionRecords $sessionRecords -Config $config -TaskDefinitions $taskDefs -PreserveManagedSessions - } - - $interactiveStale = $false - foreach ($bucket in @("aw-watcher-afk_$hostname", "aw-watcher-window_$hostname", "aw-dlp-endpoint-signals_$hostname")) { - $age = $bucketChecks[$bucket].ageSeconds - if ($null -eq $age -or [int]$age -gt $InteractiveMaxAgeSeconds) { - $interactiveStale = $true - } - } - - $watchersInLiveSessions = @($processSnapshot.watchers | Where-Object { $liveSessionIds -contains [int]$_.sessionId }) - $watchersInManagedSessions = @($processSnapshot.watchers | Where-Object { $managedSessionIds -contains [int]$_.sessionId }) - $liveWatcherMissing = $false - if ($liveSessions.Count -gt 0) { - $hasAfk = @($watchersInLiveSessions | Where-Object { [string]$_.name -ieq 'aw-watcher-afk' }).Count -gt 0 - $hasWindow = @($watchersInLiveSessions | Where-Object { [string]$_.name -ieq 'aw-watcher-window' }).Count -gt 0 - $liveWatcherMissing = (-not $hasAfk) -or (-not $hasWindow) - } - $managedWatcherMissing = $false - if ($managedInteractiveSessions.Count -gt 0) { - $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 } - foreach ($managedSession in @($managedInteractiveSessions)) { - $sessionId = [int]$managedSession.SessionId - $sessionWatchers = @($watchersInManagedSessions | Where-Object { [int]$_.sessionId -eq $sessionId }) - $hasManagedAfk = @($sessionWatchers | Where-Object { [string]$_.name -ieq 'aw-watcher-afk' }).Count -gt 0 - $hasManagedWindow = @($sessionWatchers | Where-Object { [string]$_.name -ieq 'aw-watcher-window' }).Count -gt 0 - if (($afkEnabled -and -not $hasManagedAfk) -or ($windowEnabled -and -not $hasManagedWindow)) { - $managedWatcherMissing = $true - break - } - } - } - - if ($managedInteractiveSessions.Count -eq 0 -and $liveSessions.Count -gt 0 -and $interactiveStale) { - $problems.Add('interactive buckets stale but no managed interactive sessions found') | Out-Null - } - - $needsInteractiveTaskAction = $managedInteractiveSessions.Count -gt 0 -and ( - $liveWatcherMissing -or - $managedWatcherMissing -or - ($interactiveStale -and $liveSessions.Count -gt 0) - ) - - if ($needsInteractiveTaskAction) { - foreach ($taskDef in $taskDefs) { - if (-not (Test-ActivityWatchUserHasManagedSession -UserId ([string]$taskDef.userId) -SessionRecords $sessionRecords -IncludeLive -IncludeDisconnected)) { - continue - } - $key = "task:$($taskDef.taskName)" - $allowed = Test-ActionAllowed -Runtime $Runtime -Key $key -CooldownSeconds $InteractiveActionCooldownSeconds -WindowSeconds $RestartWindowSeconds -MaxCount $MaxRestarts - if (-not $allowed.allowed) { - $problems.Add("$key action blocked: $($allowed.reason)") | Out-Null - continue - } - - if ($Mode -eq 'enforce') { - $ok = Invoke-ExactTaskRun -TaskName ([string]$taskDef.taskName) - if ($ok) { - Register-GuardAction -Runtime $Runtime -Key $key - } - Write-GuardLog -LogPath $LogPath -Message ("run {0} ok={1}" -f $key, $ok) - $actions.Add([pscustomobject]@{ action = 'run-task'; target = [string]$taskDef.taskName; applied = $true; ok = $ok }) | Out-Null - } - else { - $actions.Add([pscustomobject]@{ action = 'run-task'; target = [string]$taskDef.taskName; applied = $false; mode = 'shadow' }) | Out-Null - } - } - } - else { - foreach ($taskDef in $taskDefs) { - if (Test-ActivityWatchUserHasManagedSession -UserId ([string]$taskDef.userId) -SessionRecords $sessionRecords -IncludeLive -IncludeDisconnected) { - Reset-GuardActionBudget -Runtime $Runtime -Key "task:$($taskDef.taskName)" - } - } - } - - $status = 'ok' - if ($problems.Count -gt 0) { - $status = 'warn' - } - if ($managedInteractiveSessions.Count -gt 0 -and $interactiveStale -and $Mode -eq 'shadow') { - $status = 'warn' - } - - $sessionState = @( - foreach ($session in @($sessionRecords)) { - [pscustomobject]@{ - SessionName = [string]$session.SessionName - UserName = [string]$session.UserName - SessionId = [int]$session.SessionId - State = [string]$session.State - IsLive = [bool]$session.IsLive - } - } - ) - - $state = @{} - $state['status'] = $status - $state['mode'] = $Mode - $state['host'] = $hostname - $state['generatedAtUtc'] = (Get-Date).ToUniversalTime().ToString('o') - $state['pid'] = $PID - $state['sessions'] = @($sessionState) - $state['liveSessionCount'] = $liveSessions.Count - $state['managedSessionCount'] = $managedInteractiveSessions.Count - $state['managedSessions'] = @($managedInteractiveSessions) - $bucketState = @{} - foreach ($key in $bucketChecks.Keys) { - $bucketState[$key] = $bucketChecks[$key] - } - - $state['processes'] = $processSnapshot - $state['buckets'] = $bucketState - $state['actions'] = @($actions.ToArray()) - $state['problems'] = @($problems.ToArray()) - $state['quarantine'] = $Runtime.quarantine - - $statePath = Join-Path $stateRoot 'collector-guard-state.json' - $state | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $statePath -Encoding UTF8 - Write-GuardRuntime -Path $RuntimePath -Runtime $Runtime - [void](Send-GuardHeartbeat -ApiBase $apiBase -Hostname $hostname -State $state -PulseSeconds ([Math]::Max($LoopSeconds * 2, 60))) - return $state -} - -if ($SelfTest) { - Invoke-GuardSelfTest - exit 0 -} - -$initialConfig = Read-ActivityWatchDeploymentConfig -Path $ConfigPath -$initialStateRoot = if ($initialConfig.paths.PSObject.Properties.Name -contains 'stateRoot') { [string]$initialConfig.paths.stateRoot } else { Split-Path -Path $ConfigPath -Parent } -$initialLogsRoot = if ($initialConfig.paths.PSObject.Properties.Name -contains 'logsRoot') { [string]$initialConfig.paths.logsRoot } else { Join-Path $initialStateRoot 'logs' } -$logPath = Join-Path $initialLogsRoot 'collector-guard.log' -$runtimePath = Join-Path $initialStateRoot 'collector-guard-runtime.json' -$lockPath = New-GuardLock -StateRoot $initialStateRoot -if (-not $lockPath) { - Write-GuardLog -LogPath $logPath -Message 'another collector guard instance is already running' - exit 0 -} - -try { - $runtime = Read-GuardRuntime -Path $runtimePath - Write-GuardLog -LogPath $logPath -Message "collector guard started mode=$Mode loop=$LoopSeconds once=$($Once.IsPresent)" - while ($true) { - try { - Invoke-GuardCycle -Runtime $runtime -RuntimePath $runtimePath -LogPath $logPath | Out-Null - } - catch { - Write-GuardLog -LogPath $logPath -Message ("cycle error: {0}; at {1}" -f $_.Exception.Message, $_.ScriptStackTrace) - } - - if ($Once) { - break - } - Start-Sleep -Seconds ([Math]::Max($LoopSeconds, 15)) - } -} -finally { - if ($lockPath -and (Test-Path -LiteralPath $lockPath)) { - Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue - } - Write-GuardLog -LogPath $logPath -Message 'collector guard stopped' -} diff --git a/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 b/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 deleted file mode 100755 index 9308e5e..0000000 --- a/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 +++ /dev/null @@ -1,953 +0,0 @@ -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$RulesPath, - [string]$PolicyPath, - [string]$LogPath, - [string]$IncidentLogPath, - [int]$PollSeconds, - [int]$PulseSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -Add-Type -AssemblyName UIAutomationClient -Add-Type -AssemblyName UIAutomationTypes -Add-Type -AssemblyName System.Net.Http - -Add-Type @" -using System; -using System.Runtime.InteropServices; -using System.Text; - -public static class NativeAwMethods { - [DllImport("user32.dll")] - public static extern IntPtr GetForegroundWindow(); - - [DllImport("user32.dll")] - public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); - - [DllImport("user32.dll")] - public static extern int GetWindowTextLength(IntPtr hWnd); -} -"@ - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - - return $null -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $httpClient = $null - $content = $null - 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 - if (-not $response.IsSuccessStatusCode) { - $status = [int]$response.StatusCode - $reason = [string]$response.ReasonPhrase - $body = $response.Content.ReadAsStringAsync().Result - throw "HTTP POST failed status=$status reason=$reason body=$body" - } - } - finally { - if ($null -ne $content) { - $content.Dispose() - } - if ($null -ne $httpClient) { - $httpClient.Dispose() - } - } -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$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\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\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 '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 } -$resolvedHostname = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$deploymentConfig.awHostname)) { [string]$deploymentConfig.awHostname } else { [string]$env:COMPUTERNAME } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $resolvedHostname -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentLogPath = $resolvedIncidentLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false -$script:IncidentState = @{} -$script:DlpRules = @() -$script:DlpDefaults = [ordered]@{ - enabled = $false - cooldownSeconds = 300 - action = 'log' - severity = 'low' -} -$script:BrowserMap = @{ - msedge = 'edge' - chrome = 'chrome' - brave = 'brave' - vivaldi = 'vivaldi' - opera = 'opera' - firefox = 'firefox' -} -$script:CategoryRules = @( - @{ Name = 'work_business_systems'; Group = 'work'; Domains = @('bitrix24.ru', '1c.ru', 'sbis.ru', 'kontur.ru', 'diadoc.ru', 'nalog.gov.ru', 'gosuslugi.ru') } - @{ Name = 'work_docs_collab'; Group = 'work'; Domains = @('office.com', 'sharepoint.com', 'docs.google.com', 'drive.google.com', 'notion.so', 'miro.com') } - @{ Name = 'work_dev'; Group = 'work'; Domains = @('github.com', 'gitlab.com', 'bitbucket.org', 'youtrack.cloud', 'atlassian.net') } - @{ Name = 'work_communication'; Group = 'work'; Domains = @('teams.microsoft.com', 'outlook.office.com', 'web.telegram.org', 'slack.com', 'zoom.us') } - @{ Name = 'neutral_search_reference'; Group = 'neutral'; Domains = @('google.com', 'google.ru', 'yandex.ru', 'bing.com', 'duckduckgo.com', 'wikipedia.org') } - @{ Name = 'neutral_news'; Group = 'neutral'; Domains = @('rbc.ru', 'tass.ru', 'ria.ru', 'kommersant.ru', 'vedomosti.ru') } - @{ Name = 'personal_social'; Group = 'personal'; Domains = @('vk.com', 'ok.ru', 'facebook.com', 'instagram.com', 'tiktok.com', 'x.com', 'twitter.com') } - @{ Name = 'personal_video'; Group = 'personal'; Domains = @('youtube.com', 'youtu.be', 'rutube.ru', 'twitch.tv', 'kinopoisk.ru') } - @{ Name = 'personal_marketplace'; Group = 'personal'; Domains = @('ozon.ru', 'wildberries.ru', 'avito.ru', 'aliexpress.com', 'market.yandex.ru') } - @{ Name = 'personal_entertainment'; Group = 'personal'; Domains = @('dzen.ru', 'pikabu.ru', 'dtf.ru', 'playground.ru') } -) - -function Write-CollectorLog { - param([string]$Message) - - if (-not $script:LocalAgentLogsEnabled) { - return - } - - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Write-DlpIncidentLog { - param([string]$Message) - - if (-not $script:LocalAgentLogsEnabled) { - return - } - - try { - Add-Content -LiteralPath $script:IncidentLogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Test-DomainMatch { - param( - [string]$DomainHost, - [string]$RuleDomain - ) - - if ([string]::IsNullOrWhiteSpace($DomainHost) -or [string]::IsNullOrWhiteSpace($RuleDomain)) { - return $false - } - - $left = $DomainHost.ToLowerInvariant() - $right = $RuleDomain.ToLowerInvariant() - return $left -eq $right -or $left.EndsWith('.' + $right) -} - -function Get-HostFromUrl { - param([string]$Url) - - if ([string]::IsNullOrWhiteSpace($Url)) { - return $null - } - - try { - $uri = [Uri]$Url - $uriHost = $uri.Host.ToLowerInvariant() - if ($uriHost.StartsWith('www.')) { - return $uriHost.Substring(4) - } - - return $uriHost - } - catch { - return $null - } -} - -function Get-RootDomain { - param([string]$DomainHost) - - if ([string]::IsNullOrWhiteSpace($DomainHost)) { - return $null - } - - $parts = $DomainHost.Split('.') - if ($parts.Count -le 2) { - return $DomainHost - } - - $suffix = ('{0}.{1}' -f $parts[$parts.Count - 2], $parts[$parts.Count - 1]).ToLowerInvariant() - $compoundTlds = @('co.uk', 'com.au', 'co.jp', 'com.br', 'co.in', 'com.tr', 'com.cn') - if (($compoundTlds -contains $suffix) -and $parts.Count -ge 3) { - return ('{0}.{1}' -f $parts[$parts.Count - 3], $suffix).ToLowerInvariant() - } - - return $suffix -} - -function ConvertTo-NormalizedUrl { - param([AllowNull()][string]$Value) - - if ([string]::IsNullOrWhiteSpace($Value)) { - return $null - } - - $candidate = $Value.Trim() - if ($candidate.Length -lt 4) { - return $null - } - - if ($candidate -match '^(?i)(search|find|address and search|search with|новая вкладка|new tab)') { - return $null - } - - if ($candidate -match '^(?i)(https?|file|ftp|chrome|edge|about|view-source)://') { - return $candidate - } - - if ($candidate -match '^(?i)localhost([/:]|$)') { - return "http://$candidate" - } - - if ($candidate -match '^[a-z0-9.-]+\.[a-z]{2,}([/:?#].*)?$') { - return "https://$candidate" - } - - return $null -} - -function Load-CustomCategoryRules { - param([string]$Path) - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - return - } - - try { - $parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - $rules = @() - - if ($parsed.rules) { - $sourceRules = @($parsed.rules) - } - elseif ($parsed -is [System.Collections.IEnumerable]) { - $sourceRules = @($parsed) - } - else { - $sourceRules = @() - } - - foreach ($rule in $sourceRules) { - if (-not $rule) { - continue - } - - $name = [string]$rule.name - $group = [string]$rule.group - $domains = @($rule.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) - - if ($name -and $group -and $domains.Count -gt 0) { - $rules += @{ - Name = $name - Group = $group - Domains = $domains - } - } - } - - if ($rules.Count -gt 0) { - $script:CategoryRules = @($rules) + @($script:CategoryRules) - Write-CollectorLog ("пользовательские правила загружены: {0}" -f $rules.Count) - } - } - catch { - Write-CollectorLog ("не удалось загрузить пользовательские правила: {0}" -f $_.Exception.Message) - } -} - -function Get-WebCategory { - param([string]$DomainHost) - - foreach ($rule in $script:CategoryRules) { - foreach ($domain in $rule.Domains) { - if (Test-DomainMatch -DomainHost $DomainHost -RuleDomain $domain) { - return [pscustomobject]@{ - Name = [string]$rule.Name - Group = [string]$rule.Group - Rule = [string]$domain - } - } - } - } - - return [pscustomobject]@{ - Name = 'uncategorized' - Group = 'neutral' - Rule = 'none' - } -} - -function Test-DomainListMatch { - param( - [string]$DomainHost, - [string[]]$Domains - ) - - if (-not $Domains -or $Domains.Count -eq 0) { - return $false - } - - foreach ($domain in $Domains) { - if (Test-DomainMatch -DomainHost $DomainHost -RuleDomain $domain) { - return $true - } - } - - return $false -} - -function Test-DlpRuleTimeWindow { - param( - [int]$CurrentHour, - [AllowNull()][int]$HourFrom, - [AllowNull()][int]$HourTo - ) - - if ($null -eq $HourFrom -or $null -eq $HourTo) { - return $true - } - - if ($HourFrom -eq $HourTo) { - return $true - } - - if ($HourFrom -lt $HourTo) { - return ($CurrentHour -ge $HourFrom -and $CurrentHour -lt $HourTo) - } - - return ($CurrentHour -ge $HourFrom -or $CurrentHour -lt $HourTo) -} - -function Load-DlpPolicy { - param([string]$Path) - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-CollectorLog ("DLP-политика не найдена, DLP отключен: {0}" -f $Path) - return - } - - try { - $parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - $defaults = $parsed.defaults - if ($defaults) { - if ($defaults.PSObject.Properties.Name -contains 'enabled') { - $script:DlpDefaults.enabled = [bool]$defaults.enabled - } - if ($defaults.cooldownSeconds) { - $script:DlpDefaults.cooldownSeconds = [int]$defaults.cooldownSeconds - } - if ($defaults.action) { - $script:DlpDefaults.action = [string]$defaults.action - } - if ($defaults.severity) { - $script:DlpDefaults.severity = [string]$defaults.severity - } - } - - $loaded = @() - foreach ($rule in @($parsed.rules)) { - if (-not $rule) { continue } - $when = $rule.when - if (-not $when) { - $when = [pscustomobject]@{} - } - $loaded += [pscustomobject]@{ - id = [string]$rule.id - 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.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 { @() } - categoryGroups = if ($when.PSObject.Properties.Name -contains 'categoryGroups') { @($when.categoryGroups | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - categories = if ($when.PSObject.Properties.Name -contains 'categories') { @($when.categories | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - browsers = if ($when.PSObject.Properties.Name -contains 'browsers') { @($when.browsers | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - urlRegex = if ($when.PSObject.Properties.Name -contains 'urlRegex' -and $when.urlRegex) { [string]$when.urlRegex } else { $null } - titleRegex = if ($when.PSObject.Properties.Name -contains 'titleRegex' -and $when.titleRegex) { [string]$when.titleRegex } else { $null } - hourFrom = if ($when.PSObject.Properties.Name -contains 'hourFrom') { [int]$when.hourFrom } else { $null } - hourTo = if ($when.PSObject.Properties.Name -contains 'hourTo') { [int]$when.hourTo } else { $null } - } - } - } - - $script:DlpRules = @($loaded) - Write-CollectorLog ("DLP-политика загружена: включена={0}, правил={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count) - } - catch { - Write-CollectorLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message) - } -} - -function Test-DlpRuleMatch { - param( - [pscustomobject]$Rule, - [string]$Domain, - [string]$RootDomain, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$Category, - [string]$CategoryGroup - ) - - if (-not $Rule.enabled) { - return $false - } - - $when = $Rule.when - $currentHour = (Get-Date).Hour - if (-not (Test-DlpRuleTimeWindow -CurrentHour $currentHour -HourFrom $when.hourFrom -HourTo $when.hourTo)) { - return $false - } - - if ($when.domains.Count -gt 0) { - $domainMatched = (Test-DomainListMatch -DomainHost $Domain -Domains $when.domains) -or (Test-DomainListMatch -DomainHost $RootDomain -Domains $when.domains) - if (-not $domainMatched) { - return $false - } - } - - if ($when.categoryGroups.Count -gt 0 -and ($when.categoryGroups -notcontains $CategoryGroup.ToLowerInvariant())) { - return $false - } - - if ($when.categories.Count -gt 0 -and ($when.categories -notcontains $Category.ToLowerInvariant())) { - return $false - } - - if ($when.browsers.Count -gt 0 -and ($when.browsers -notcontains $BrowserKey.ToLowerInvariant())) { - return $false - } - - if ($when.urlRegex) { - if (-not ($Url -match $when.urlRegex)) { - return $false - } - } - - if ($when.titleRegex) { - if (-not ($Title -match $when.titleRegex)) { - return $false - } - } - - return $true -} - -function Get-DlpDecision { - param( - [string]$Domain, - [string]$RootDomain, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$Category, - [string]$CategoryGroup - ) - - if (-not $script:DlpDefaults.enabled) { - return $null - } - - foreach ($rule in $script:DlpRules) { - if (Test-DlpRuleMatch -Rule $rule -Domain $Domain -RootDomain $RootDomain -Url $Url -Title $Title -BrowserKey $BrowserKey -Category $Category -CategoryGroup $CategoryGroup) { - return $rule - } - } - - return $null -} - -function Should-EmitIncident { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:IncidentState.ContainsKey($Fingerprint)) { - $last = [datetime]$script:IncidentState[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:IncidentState[$Fingerprint] = $now - return $true -} - -function Send-DlpIncidentHeartbeat { - param( - [pscustomobject]$Decision, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName, - [string]$Domain, - [string]$RootDomain, - [string]$Category, - [string]$CategoryGroup - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId ([string]$Decision.id) -SignalType 'web' - } - catch { - } - } - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = [string]$Decision.id - action = [string]$Decision.action - severity = [string]$Decision.severity - message = [string]$Decision.message - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - domain = $Domain - rootDomain = $RootDomain - category = $Category - categoryGroup = $CategoryGroup - username = $env:USERNAME - hostname = $script:Hostname - sessionId = $script:SessionId - source = 'uia-native-dlp' - } + $captureData - } | ConvertTo-Json -Depth 5 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -Json $event -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-CollectorLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message) - return @{} - } -} - -function Get-ForegroundWindowContext { - $handle = [NativeAwMethods]::GetForegroundWindow() - if ($handle -eq [IntPtr]::Zero) { - return $null - } - - $processId = [uint32]0 - [void][NativeAwMethods]::GetWindowThreadProcessId($handle, [ref]$processId) - if (-not $processId) { - return $null - } - - $process = Get-Process -Id ([int]$processId) -ErrorAction SilentlyContinue - if (-not $process) { - return $null - } - - $textLength = [NativeAwMethods]::GetWindowTextLength($handle) - $builder = [Text.StringBuilder]::new([Math]::Max($textLength + 1, 260)) - [void][NativeAwMethods]::GetWindowText($handle, $builder, $builder.Capacity) - - return [pscustomobject]@{ - Handle = $handle - ProcessName = $process.ProcessName.ToLowerInvariant() - Title = $builder.ToString() - } -} - -function Get-BrowserUrlFromWindow { - param([IntPtr]$Handle) - - $root = [System.Windows.Automation.AutomationElement]::FromHandle($Handle) - if (-not $root) { - return $null - } - - $editCondition = [System.Windows.Automation.PropertyCondition]::new( - [System.Windows.Automation.AutomationElement]::ControlTypeProperty, - [System.Windows.Automation.ControlType]::Edit - ) - - $edits = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $editCondition) - foreach ($edit in $edits) { - $valuePattern = $null - if ($edit.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$valuePattern)) { - $candidate = ConvertTo-NormalizedUrl -Value $valuePattern.Current.Value - if ($candidate) { - return $candidate - } - } - - $candidateFromName = ConvertTo-NormalizedUrl -Value $edit.Current.Name - if ($candidateFromName) { - return $candidateFromName - } - } - - return $null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType = 'web.tab.current' - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - $script:KnownBuckets[$BucketId] = $true - return - } - catch { - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - try { - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - } - catch { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - } - $script:KnownBuckets[$BucketId] = $true -} - -function Send-Heartbeat { - param( - [string]$BucketId, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName - ) - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - source = 'uia-native' - sessionId = $script:SessionId - } - } | ConvertTo-Json -Depth 4 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -Json $event -} - -function Send-WindowHeartbeat { - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$Context - ) - - if (-not $Context) { - return - } - - $processName = [string]$Context.ProcessName - $title = [string]$Context.Title - if ([string]::IsNullOrWhiteSpace($processName) -and [string]::IsNullOrWhiteSpace($title)) { - return - } - - $bucketId = 'aw-watcher-window_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-watcher-window' -BucketType 'currentwindow' - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - app = if ([string]::IsNullOrWhiteSpace($processName)) { 'unknown.exe' } else { "$processName.exe" } - title = $title - source = 'uia-native' - sessionId = $script:SessionId - } - } | ConvertTo-Json -Depth 4 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -Json $event -} - -function Send-CategoryHeartbeat { - param( - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName, - [string]$Domain, - [string]$RootDomain, - [string]$Category, - [string]$CategoryGroup, - [string]$CategoryRule - ) - - $bucketId = 'aw-detmir-web-category_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-detmir-web-category' -BucketType 'aw.web.category' - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - domain = $Domain - rootDomain = $RootDomain - category = $Category - categoryGroup = $CategoryGroup - categoryRule = $CategoryRule - source = 'uia-native' - sessionId = $script:SessionId - } - } | ConvertTo-Json -Depth 4 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -Json $event -} - -function Send-CollectorHealthHeartbeat { - param( - $Context = $null, - [string]$DetectedUrl = $null - ) - - $bucketId = 'aw-detmir-web-category_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-detmir-web-category' -BucketType 'aw.web.category' - - $foregroundProcess = '' - $foregroundTitle = '' - $browserDetected = $false - if ($Context) { - $foregroundProcess = [string]$Context.ProcessName - $foregroundTitle = [string]$Context.Title - $browserDetected = $script:BrowserMap.ContainsKey($foregroundProcess) - } - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = 'collector_health' - username = $env:USERNAME - hostname = $script:Hostname - sessionId = $script:SessionId - foregroundProcess = $foregroundProcess - foregroundTitle = $foregroundTitle - browserDetected = $browserDetected - urlDetected = -not [string]::IsNullOrWhiteSpace($DetectedUrl) - } - } | ConvertTo-Json -Depth 5 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -Json $event -} - -Load-CustomCategoryRules -Path $resolvedRulesPath -Load-DlpPolicy -Path $resolvedPolicyPath -Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase) - -$lastHealth = [datetime]::MinValue -while ($true) { - $context = $null - $detectedUrl = $null - try { - $context = Get-ForegroundWindowContext - if ($context) { - Send-WindowHeartbeat -Context $context - } - if ($context -and $script:BrowserMap.ContainsKey($context.ProcessName)) { - $url = Get-BrowserUrlFromWindow -Handle $context.Handle - if ($url) { - $detectedUrl = $url - $browserKey = $script:BrowserMap[$context.ProcessName] - $domain = Get-HostFromUrl -Url $url - if (-not $domain) { - $domain = 'unknown' - } - - $rootDomain = Get-RootDomain -DomainHost $domain - if (-not $rootDomain) { - $rootDomain = $domain - } - - $category = Get-WebCategory -DomainHost $domain - $bucketId = 'aw-watcher-web-{0}_{1}' -f $browserKey, $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey) - Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName - Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule - - $decision = Get-DlpDecision -Domain $domain -RootDomain $rootDomain -Url $url -Title $context.Title -BrowserKey $browserKey -Category $category.Name -CategoryGroup $category.Group - if ($decision) { - $fingerprint = '{0}|{1}|{2}|{3}' -f $decision.id, $browserKey, $rootDomain, $env:USERNAME - $cooldown = [Math]::Max([int]$decision.cooldownSeconds, 30) - if (Should-EmitIncident -Fingerprint $fingerprint -CooldownSeconds $cooldown) { - Write-DlpIncidentLog ("{0} {1} {2} {3}" -f $decision.severity, $decision.action, $decision.id, $url) - if (@('alert', 'block', 'quarantine') -contains ([string]$decision.action).ToLowerInvariant()) { - Send-DlpIncidentHeartbeat -Decision $decision -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group - } - } - } - } - } - } - catch { - Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message) - } - - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $lastHealth).TotalSeconds -ge [Math]::Max($resolvedPulseSeconds, $resolvedPollSeconds)) { - try { - Send-CollectorHealthHeartbeat -Context $context -DetectedUrl $detectedUrl - $lastHealth = $nowUtc - } - catch { - Write-CollectorLog ("ошибка heartbeat: {0}" -f $_.Exception.Message) - } - } - - Start-Sleep -Seconds $resolvedPollSeconds -} diff --git a/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 deleted file mode 100755 index 4370b43..0000000 --- a/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 +++ /dev/null @@ -1,183 +0,0 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [string[]]$Users, - [string]$UserListPath, - [string]$Domain, - [int]$ServerPort = 5600, - [ValidateSet('http', 'https')] - [string]$ServerScheme = 'http', - [string]$Version = 'v0.13.2', - [string]$PackageUrl, - [string]$PackageZipPath, - [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, - [string]$IncidentArtifactsRoot, - [string]$EvtxExportRoot, - [int]$EvtxRetentionDays = 14, - [string[]]$EvtxChannels = @(), - [bool]$LogonMarkerEnabled = $true, - [bool]$ProcessEventsEnabled = $false, - [string]$AwHostname, - [string]$CustomRulesPath, - [string]$CustomPolicyPath, - [ValidateSet('local', 'server')] - [string]$PolicyMode = 'local', - [bool]$PolicyEngineEnabled = $false, - [string]$PolicyEngineHost, - [int]$PolicyEnginePort = 5601, - [ValidateSet('http', 'https')] - [string]$PolicyEngineScheme = 'http', - [int]$PolicyRefreshSeconds = 300, - [string]$PolicyCachePath, - [bool]$HayabusaAutoUploadEnabled = $true, - [int]$HayabusaAutoUploadIntervalHours = 6, - [int]$HayabusaAutoUploadHoursBack = 6, - [string]$HayabusaAutoUploadMode = 'incident', - [string]$HayabusaAutoUploadTaskName = 'ActivityWatch Hayabusa Upload', - [bool]$File1CAutoUploadEnabled = $true, - [int]$File1CAutoUploadIntervalHours = 6, - [string]$File1CAutoUploadTaskName = 'ActivityWatch File1C Upload', - [string]$File1CTargetHost, - [string]$File1CTargetUser = 'igor', - [string]$File1CRegistryWorkbookPath = 'E:\USER1\СПИСОК ПРЕДПРИЯТИЙ И ИХ РАСПРЕДЕЛЕНИЕ.xlsx', - [switch]$IntegrationTestEnabled -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' -Import-Module $modulePath -Force - -Assert-Administrator - -$targetUsers = Normalize-ActivityWatchUsers -Users $Users -UserListPath $UserListPath -Domain $Domain -$workingRoot = Join-Path $env:TEMP 'activitywatch-windows-deploy' -$backupRoot = Join-Path $StateRoot 'backups' -$logsRoot = Join-Path $StateRoot 'logs' -$configPath = Join-Path $StateRoot 'deployment-config.json' -$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' -$policyClientSource = Join-Path $PSScriptRoot 'dlp-policy-client.ps1' -$emailCollectorSource = Join-Path $PSScriptRoot 'email-outbound-collector.ps1' -$fileCollectorSource = Join-Path $PSScriptRoot 'file-operations-collector.ps1' -$sessionCollectorSource = Join-Path $PSScriptRoot 'worktime-session-collector.ps1' -$evtxExportScriptSource = Join-Path $PSScriptRoot 'export-evtx-for-hayabusa.ps1' -$hayabusaUploadScriptSource = Join-Path $PSScriptRoot 'export-upload-hayabusa-to-aw-server.ps1' -$file1cTelemetryScriptSource = Join-Path $PSScriptRoot 'export-upload-file-1c-telemetry.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 -Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null - -$assetResult = Copy-ActivityWatchCollectorAssets ` - -CollectorScriptSource $collectorSource ` - -EndpointCollectorScriptSource $endpointCollectorSource ` - -PolicyClientScriptSource $policyClientSource ` - -EmailCollectorScriptSource $emailCollectorSource ` - -FileCollectorScriptSource $fileCollectorSource ` - -SessionCollectorScriptSource $sessionCollectorSource ` - -EvtxExportScriptSource $evtxExportScriptSource ` - -HayabusaUploadScriptSource $hayabusaUploadScriptSource ` - -File1CTelemetryScriptSource $file1cTelemetryScriptSource ` - -ExampleRulesSource $exampleRulesSource ` - -ExamplePolicySource $examplePolicySource ` - -StateRoot $StateRoot ` - -CustomRulesSource $CustomRulesPath ` - -CustomPolicySource $CustomPolicyPath -$taskDefinitions = New-ActivityWatchUserTaskDefinitions -Users $targetUsers - -Write-ActivityWatchLaunchScript -Path $launchScriptPath -ConfigPath $configPath -Write-ActivityWatchRecoveryScript -Path $recoveryScriptPath -ConfigPath $configPath - -$config = New-ActivityWatchDeploymentConfig ` - -ServerHost $ServerHost ` - -ServerPort $ServerPort ` - -ServerScheme $ServerScheme ` - -InstallRoot $InstallRoot ` - -StateRoot $StateRoot ` - -LogsRoot $logsRoot ` - -CollectorScript $assetResult.CollectorScript ` - -EndpointCollectorScript $assetResult.EndpointCollectorScript ` - -PolicyClientScript $assetResult.PolicyClientScript ` - -EmailCollectorScript $assetResult.EmailCollectorScript ` - -FileCollectorScript $assetResult.FileCollectorScript ` - -SessionCollectorScript $assetResult.SessionCollectorScript ` - -EvtxExportScript $assetResult.EvtxExportScript ` - -HayabusaUploadScript $assetResult.HayabusaUploadScript ` - -File1CTelemetryScript $assetResult.File1CTelemetryScript ` - -RulesPath $assetResult.ActiveRules ` - -PolicyPath $assetResult.ActivePolicy ` - -PollSeconds $PollSeconds ` - -PulseSeconds $PulseSeconds ` - -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` - -AfkEnabled $AfkEnabled ` - -WindowEnabled $WindowEnabled ` - -FileOpsEnabled $FileOpsEnabled ` - -LocalAgentLogsEnabled $LocalAgentLogsEnabled ` - -IncidentCaptureEnabled $IncidentCaptureEnabled ` - -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` - -IncidentArtifactsRoot $IncidentArtifactsRoot ` - -EvtxExportRoot $EvtxExportRoot ` - -EvtxRetentionDays $EvtxRetentionDays ` - -EvtxChannels $EvtxChannels ` - -LogonMarkerEnabled $LogonMarkerEnabled ` - -ProcessEventsEnabled $ProcessEventsEnabled ` - -AwHostname $AwHostname ` - -PolicyMode $PolicyMode ` - -PolicyEngineEnabled $PolicyEngineEnabled ` - -PolicyEngineHost $PolicyEngineHost ` - -PolicyEnginePort $PolicyEnginePort ` - -PolicyEngineScheme $PolicyEngineScheme ` - -PolicyRefreshSeconds $PolicyRefreshSeconds ` - -PolicyCachePath $PolicyCachePath ` - -HayabusaAutoUploadEnabled $HayabusaAutoUploadEnabled ` - -HayabusaAutoUploadIntervalHours $HayabusaAutoUploadIntervalHours ` - -HayabusaAutoUploadHoursBack $HayabusaAutoUploadHoursBack ` - -HayabusaAutoUploadMode $HayabusaAutoUploadMode ` - -HayabusaAutoUploadTaskName $HayabusaAutoUploadTaskName ` - -File1CAutoUploadEnabled $File1CAutoUploadEnabled ` - -File1CAutoUploadIntervalHours $File1CAutoUploadIntervalHours ` - -File1CAutoUploadTaskName $File1CAutoUploadTaskName ` - -File1CTargetHost $File1CTargetHost ` - -File1CTargetUser $File1CTargetUser ` - -File1CRegistryWorkbookPath $File1CRegistryWorkbookPath ` - -LaunchScriptPath $launchScriptPath ` - -RecoveryScriptPath $recoveryScriptPath ` - -UserTasks $taskDefinitions ` - -PackageVersion $Version ` - -IntegrationTestEnabled:$IntegrationTestEnabled - -Write-ActivityWatchDeploymentConfig -Config $config -Path $configPath -Remove-LegacyActivityWatchEntries -Set-ActivityWatchAcl -InstallRoot $InstallRoot -StateRoot $StateRoot -LogsRoot $logsRoot -Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $launchScriptPath -ConfigPath $configPath -Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath -Register-ActivityWatchHayabusaAutoUploadTask -ConfigPath $configPath -Register-ActivityWatchFile1CAutoUploadTask -ConfigPath $configPath -Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName - -Write-Host 'ActivityWatch развёрнут для пользователей:' -$targetUsers | ForEach-Object { Write-Host " - $_" } -Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" -Write-Host "Каталог данных: $StateRoot" -Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)" diff --git a/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 deleted file mode 100644 index e21413a..0000000 --- a/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 +++ /dev/null @@ -1,203 +0,0 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [string[]]$Users, - [string]$UserListPath, - [string]$Domain, - [int]$ServerPort = 5600, - [ValidateSet('http', 'https')] - [string]$ServerScheme = 'http', - [string]$Version = 'v0.13.2', - [string]$PackageUrl, - [string]$PackageZipPath, - [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, - [string]$IncidentArtifactsRoot, - [string]$EvtxExportRoot, - [int]$EvtxRetentionDays = 14, - [string[]]$EvtxChannels = @(), - [bool]$LogonMarkerEnabled = $true, - [bool]$ProcessEventsEnabled = $false, - [string]$AwHostname, - [string]$CustomRulesPath, - [string]$CustomPolicyPath, - [ValidateSet('local', 'server')] - [string]$PolicyMode = 'local', - [bool]$PolicyEngineEnabled = $false, - [string]$PolicyEngineHost, - [int]$PolicyEnginePort = 5601, - [ValidateSet('http', 'https')] - [string]$PolicyEngineScheme = 'http', - [int]$PolicyRefreshSeconds = 300, - [string]$PolicyCachePath, - [string]$ReportPath, - [bool]$HayabusaAutoUploadEnabled = $true, - [int]$HayabusaAutoUploadIntervalHours = 6, - [int]$HayabusaAutoUploadHoursBack = 6, - [string]$HayabusaAutoUploadMode = 'incident', - [string]$HayabusaAutoUploadTaskName = 'ActivityWatch Hayabusa Upload', - [bool]$File1CAutoUploadEnabled = $true, - [int]$File1CAutoUploadIntervalHours = 6, - [string]$File1CAutoUploadTaskName = 'ActivityWatch File1C Upload', - [string]$File1CTargetHost, - [string]$File1CTargetUser = 'igor', - [string]$File1CRegistryWorkbookPath = 'E:\USER1\СПИСОК ПРЕДПРИЯТИЙ И ИХ РАСПРЕДЕЛЕНИЕ.xlsx', - [switch]$SkipHardening, - [switch]$ValidateAfterDeploy, - [switch]$IntegrationTestEnabled -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' -Import-Module $modulePath -Force - -Assert-Administrator - -$resolvedUsers = Normalize-ActivityWatchUsers -Users $Users -UserListPath $UserListPath -Domain $Domain -$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' -$effectiveReportPath = if ($ReportPath) { $ReportPath } else { Join-Path $StateRoot "ensemble-report-$timestamp.json" } -$deployScript = Join-Path $PSScriptRoot 'deploy-domain-users.ps1' -$hardeningScript = Join-Path $PSScriptRoot 'hardening-recovery.ps1' -$validationScript = Join-Path $PSScriptRoot 'validate-deployment.ps1' - -if (-not (Test-Path -LiteralPath $deployScript)) { - throw "Не найден скрипт: $deployScript" -} - -& $deployScript ` - -ServerHost $ServerHost ` - -Users $resolvedUsers ` - -ServerPort $ServerPort ` - -ServerScheme $ServerScheme ` - -Version $Version ` - -PackageUrl $PackageUrl ` - -PackageZipPath $PackageZipPath ` - -InstallRoot $InstallRoot ` - -StateRoot $StateRoot ` - -PollSeconds $PollSeconds ` - -PulseSeconds $PulseSeconds ` - -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` - -AfkEnabled $AfkEnabled ` - -WindowEnabled $WindowEnabled ` - -FileOpsEnabled $FileOpsEnabled ` - -LocalAgentLogsEnabled $LocalAgentLogsEnabled ` - -IncidentCaptureEnabled $IncidentCaptureEnabled ` - -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` - -IncidentArtifactsRoot $IncidentArtifactsRoot ` - -EvtxExportRoot $EvtxExportRoot ` - -EvtxRetentionDays $EvtxRetentionDays ` - -EvtxChannels $EvtxChannels ` - -LogonMarkerEnabled $LogonMarkerEnabled ` - -ProcessEventsEnabled $ProcessEventsEnabled ` - -AwHostname $AwHostname ` - -CustomRulesPath $CustomRulesPath ` - -CustomPolicyPath $CustomPolicyPath ` - -PolicyMode $PolicyMode ` - -PolicyEngineEnabled $PolicyEngineEnabled ` - -PolicyEngineHost $PolicyEngineHost ` - -PolicyEnginePort $PolicyEnginePort ` - -PolicyEngineScheme $PolicyEngineScheme ` - -PolicyRefreshSeconds $PolicyRefreshSeconds ` - -PolicyCachePath $PolicyCachePath ` - -HayabusaAutoUploadEnabled $HayabusaAutoUploadEnabled ` - -HayabusaAutoUploadIntervalHours $HayabusaAutoUploadIntervalHours ` - -HayabusaAutoUploadHoursBack $HayabusaAutoUploadHoursBack ` - -HayabusaAutoUploadMode $HayabusaAutoUploadMode ` - -HayabusaAutoUploadTaskName $HayabusaAutoUploadTaskName ` - -File1CAutoUploadEnabled $File1CAutoUploadEnabled ` - -File1CAutoUploadIntervalHours $File1CAutoUploadIntervalHours ` - -File1CAutoUploadTaskName $File1CAutoUploadTaskName ` - -File1CTargetHost $File1CTargetHost ` - -File1CTargetUser $File1CTargetUser ` - -File1CRegistryWorkbookPath $File1CRegistryWorkbookPath ` - -IntegrationTestEnabled:$IntegrationTestEnabled - -if (-not $SkipHardening) { - & $hardeningScript ` - -ConfigPath (Join-Path $StateRoot 'deployment-config.json') ` - -ServerHost $ServerHost ` - -ServerPort $ServerPort ` - -ServerScheme $ServerScheme ` - -Users $resolvedUsers ` - -InstallRoot $InstallRoot ` - -StateRoot $StateRoot ` - -PollSeconds $PollSeconds ` - -PulseSeconds $PulseSeconds ` - -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` - -AfkEnabled $AfkEnabled ` - -WindowEnabled $WindowEnabled ` - -FileOpsEnabled $FileOpsEnabled ` - -LocalAgentLogsEnabled $LocalAgentLogsEnabled ` - -IncidentCaptureEnabled $IncidentCaptureEnabled ` - -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` - -IncidentArtifactsRoot $IncidentArtifactsRoot ` - -EvtxExportRoot $EvtxExportRoot ` - -EvtxRetentionDays $EvtxRetentionDays ` - -EvtxChannels $EvtxChannels ` - -LogonMarkerEnabled $LogonMarkerEnabled ` - -ProcessEventsEnabled $ProcessEventsEnabled ` - -AwHostname $AwHostname ` - -CustomRulesPath $CustomRulesPath ` - -CustomPolicyPath $CustomPolicyPath ` - -PolicyMode $PolicyMode ` - -PolicyEngineEnabled $PolicyEngineEnabled ` - -PolicyEngineHost $PolicyEngineHost ` - -PolicyEnginePort $PolicyEnginePort ` - -PolicyEngineScheme $PolicyEngineScheme ` - -PolicyRefreshSeconds $PolicyRefreshSeconds ` - -PolicyCachePath $PolicyCachePath -} - -$report = [ordered]@{ - generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - server = [ordered]@{ - host = $ServerHost - port = $ServerPort - scheme = $ServerScheme - } - packageVersion = $Version - users = @($resolvedUsers) - paths = [ordered]@{ - installRoot = $InstallRoot - stateRoot = $StateRoot - configPath = Join-Path $StateRoot 'deployment-config.json' - } - collectors = [ordered]@{ - afkEnabled = $AfkEnabled - windowEnabled = $WindowEnabled - fileOpsEnabled = $FileOpsEnabled - } - hardeningApplied = (-not $SkipHardening) -} - -if ($ValidateAfterDeploy) { - if (-not (Test-Path -LiteralPath $validationScript)) { - throw "Не найден скрипт: $validationScript" - } - - $validation = & $validationScript -ConfigPath (Join-Path $StateRoot 'deployment-config.json') - $report.validation = $validation -} - -$reportDirectory = Split-Path -Path $effectiveReportPath -Parent -if ($reportDirectory) { - New-ActivityWatchDirectory -Path $reportDirectory -} - -$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8 - -Write-Host 'Комплексное развёртывание ActivityWatch завершено.' -Write-Host "Пользователи: $($resolvedUsers -join ', ')" -Write-Host "Отчёт: $effectiveReportPath" diff --git a/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 deleted file mode 100755 index eb6c739..0000000 --- a/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 +++ /dev/null @@ -1,125 +0,0 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [Parameter(Mandatory = $true)] - [string]$TargetUser, - [int]$ServerPort = 5600, - [ValidateSet('http', 'https')] - [string]$ServerScheme = 'http', - [string]$Version = 'v0.13.2', - [string]$PackageUrl, - [string]$PackageZipPath, - [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]$LocalAgentLogsEnabled = $false, - [bool]$IncidentCaptureEnabled = $true, - [bool]$IncidentScreenshotEnabled = $true, - [string]$IncidentArtifactsRoot, - [string]$EvtxExportRoot, - [int]$EvtxRetentionDays = 14, - [string[]]$EvtxChannels = @(), - [bool]$LogonMarkerEnabled = $true, - [bool]$ProcessEventsEnabled = $false, - [string]$AwHostname, - [string]$CustomRulesPath, - [string]$CustomPolicyPath -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' -Import-Module $modulePath -Force - -Assert-Administrator - -$workingRoot = Join-Path $env:TEMP 'activitywatch-windows-deploy' -$backupRoot = Join-Path $StateRoot 'backups' -$logsRoot = Join-Path $StateRoot 'logs' -$configPath = Join-Path $StateRoot 'deployment-config.json' -$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' -$emailCollectorSource = Join-Path $PSScriptRoot 'email-outbound-collector.ps1' -$sessionCollectorSource = Join-Path $PSScriptRoot 'worktime-session-collector.ps1' -$evtxExportScriptSource = Join-Path $PSScriptRoot 'export-evtx-for-hayabusa.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 - -$archivePath = Get-ActivityWatchArchive -PackageZipPath $PackageZipPath -PackageUrl $PackageUrl -Version $Version -WorkingRoot $workingRoot -Install-ActivityWatchPackage -ArchivePath $archivePath -InstallRoot $InstallRoot -WorkingRoot $workingRoot -BackupRoot $backupRoot | Out-Null -Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null - -$assetResult = Copy-ActivityWatchCollectorAssets ` - -CollectorScriptSource $collectorSource ` - -EndpointCollectorScriptSource $endpointCollectorSource ` - -EmailCollectorScriptSource $emailCollectorSource ` - -SessionCollectorScriptSource $sessionCollectorSource ` - -EvtxExportScriptSource $evtxExportScriptSource ` - -ExampleRulesSource $exampleRulesSource ` - -ExamplePolicySource $examplePolicySource ` - -StateRoot $StateRoot ` - -CustomRulesSource $CustomRulesPath ` - -CustomPolicySource $CustomPolicyPath -$taskDefinitions = New-ActivityWatchUserTaskDefinitions -Users @($TargetUser) - -Write-ActivityWatchLaunchScript -Path $launchScriptPath -ConfigPath $configPath -Write-ActivityWatchRecoveryScript -Path $recoveryScriptPath -ConfigPath $configPath - -$config = New-ActivityWatchDeploymentConfig ` - -ServerHost $ServerHost ` - -ServerPort $ServerPort ` - -ServerScheme $ServerScheme ` - -InstallRoot $InstallRoot ` - -StateRoot $StateRoot ` - -LogsRoot $logsRoot ` - -CollectorScript $assetResult.CollectorScript ` - -EndpointCollectorScript $assetResult.EndpointCollectorScript ` - -EmailCollectorScript $assetResult.EmailCollectorScript ` - -SessionCollectorScript $assetResult.SessionCollectorScript ` - -EvtxExportScript $assetResult.EvtxExportScript ` - -RulesPath $assetResult.ActiveRules ` - -PolicyPath $assetResult.ActivePolicy ` - -PollSeconds $PollSeconds ` - -PulseSeconds $PulseSeconds ` - -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` - -AfkEnabled $AfkEnabled ` - -WindowEnabled $WindowEnabled ` - -LocalAgentLogsEnabled $LocalAgentLogsEnabled ` - -IncidentCaptureEnabled $IncidentCaptureEnabled ` - -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` - -IncidentArtifactsRoot $IncidentArtifactsRoot ` - -EvtxExportRoot $EvtxExportRoot ` - -EvtxRetentionDays $EvtxRetentionDays ` - -EvtxChannels $EvtxChannels ` - -LogonMarkerEnabled $LogonMarkerEnabled ` - -ProcessEventsEnabled $ProcessEventsEnabled ` - -AwHostname $AwHostname ` - -LaunchScriptPath $launchScriptPath ` - -RecoveryScriptPath $recoveryScriptPath ` - -UserTasks $taskDefinitions ` - -PackageVersion $Version - -Write-ActivityWatchDeploymentConfig -Config $config -Path $configPath -Remove-LegacyActivityWatchEntries -Set-ActivityWatchAcl -InstallRoot $InstallRoot -StateRoot $StateRoot -LogsRoot $logsRoot -Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $launchScriptPath -ConfigPath $configPath -Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath -Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName - -Write-Host "ActivityWatch развёрнут для пользователя: $TargetUser" -Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" -Write-Host "Каталог установки: $InstallRoot" -Write-Host "Каталог данных: $StateRoot" -Write-Host "Файл правил: $($assetResult.ActiveRules)" -Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)" diff --git a/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 b/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 deleted file mode 100644 index 8fe1831..0000000 --- a/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 +++ /dev/null @@ -1,1684 +0,0 @@ -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyEngineHost, - [int]$PolicyEnginePort, - [ValidateSet('http', 'https')] - [string]$PolicyEngineScheme, - [string]$PolicyPath, - [ValidateSet('local', 'server')] - [string]$PolicyMode, - [int]$PolicyRefreshSeconds, - [string]$PolicyCachePath, - [string]$LogPath, - [int]$PollSeconds, - [switch]$SelfTestSuppressedBlock -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# Ensure HttpClient is available (Windows PowerShell 5 may not auto-load it) -try { - Add-Type -AssemblyName System.Net.Http -} -catch { -} - -$script:TransportQueuePath = $null -$script:TransportQueueLockPath = $null -$script:SessionId = [System.Diagnostics.Process]::GetCurrentProcess().SessionId -$script:TransportMetrics = @{ - eventsEnqueued = 0 - eventsFlushed = 0 - sendFailures = 0 - queueDepth = 0 -} - -$policyClientModulePath = Join-Path $PSScriptRoot 'dlp-policy-client.ps1' -if (Test-Path -LiteralPath $policyClientModulePath) { - try { - Import-Module $policyClientModulePath -Force -DisableNameChecking - $script:PolicyClientAvailable = $true - } - catch { - $script:PolicyClientAvailable = $false - } -} -else { - $script:PolicyClientAvailable = $false -} - -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-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Get-QueueNameToken { - param( - [string]$UserName, - [int]$SessionId - ) - $token = ('{0}-s{1}' -f $UserName, $SessionId) - foreach ($ch in [System.IO.Path]::GetInvalidFileNameChars()) { - $token = $token.Replace([string]$ch, '_') - } - if ([string]::IsNullOrWhiteSpace($token)) { - return ('session-{0}' -f $SessionId) - } - return $token -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - try { - $bytes = [System.Text.Encoding]::UTF8.GetBytes($Json) - $req = [System.Net.HttpWebRequest]::Create($Uri) - $req.Method = 'POST' - $req.ContentType = 'application/json' - $req.Accept = 'application/json' - $req.KeepAlive = $false - $req.Timeout = 15000 - $req.ReadWriteTimeout = 15000 - $req.ContentLength = $bytes.Length - - $stream = $req.GetRequestStream() - try { $stream.Write($bytes, 0, $bytes.Length) } finally { $stream.Close() } - - $resp = $req.GetResponse() - try { - # read body for debugging, but discard on success - $rs = $resp.GetResponseStream() - if ($rs) { $sr = New-Object System.IO.StreamReader($rs); $null = $sr.ReadToEnd(); $sr.Close() } - } finally { - $resp.Close() - } - return - } - catch [System.Net.WebException] { - $status = $null - $body = '' - try { - if ($_.Exception.Response) { - try { $status = [int]$_.Exception.Response.StatusCode } catch {} - $rs = $_.Exception.Response.GetResponseStream() - if ($rs) { $sr = New-Object System.IO.StreamReader($rs); $body = $sr.ReadToEnd(); $sr.Close() } - } - } catch {} - - # aw-server-rust may return 304 for idempotent bucket create. Treat it as OK. - if ($status -eq 304) { - Write-EndpointLog ("POST bucket exists (304): uri={0}" -f $Uri) - return - } - - Write-EndpointLog ("POST failed: uri={0} status={1} err={2} body={3}" -f $Uri, $status, $_.Exception.Message, $body) - throw - } - catch { - Write-EndpointLog ("POST error: uri={0} err={1}" -f $Uri, $_.Exception.Message) - throw - } -} - -function Initialize-TransportQueue { - param([Parameter(Mandatory = $true)][string]$StateRoot) - $queueToken = Get-QueueNameToken -UserName $env:USERNAME -SessionId $script:SessionId - $script:TransportQueuePath = Join-Path $StateRoot ("dlp-endpoint-signals-queue-{0}.jsonl" -f $queueToken) - $script:TransportQueueLockPath = Join-Path $StateRoot ("dlp-endpoint-signals-queue-{0}.lock" -f $queueToken) - if (-not (Test-Path -LiteralPath $script:TransportQueuePath)) { - New-Item -Path $script:TransportQueuePath -ItemType File -Force | Out-Null - } - $legacyQueuePath = Join-Path $StateRoot 'dlp-endpoint-signals-queue.jsonl' - if (Test-Path -LiteralPath $legacyQueuePath) { - $legacyItems = @(Get-Content -LiteralPath $legacyQueuePath -ErrorAction SilentlyContinue | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) - if ($legacyItems.Count -gt 0) { - Add-Content -LiteralPath $script:TransportQueuePath -Value $legacyItems -Encoding UTF8 - Clear-Content -LiteralPath $legacyQueuePath -ErrorAction SilentlyContinue - } - } -} - -function Get-TransportQueueLock { - $tries = 0 - while ($tries -lt 50) { - try { - return [System.IO.File]::Open($script:TransportQueueLockPath, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) - } - catch { - Start-Sleep -Milliseconds 50 - $tries++ - } - } - throw "Failed to acquire transport queue lock: $script:TransportQueueLockPath" -} - -function Add-TransportQueueItem { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Payload, - [string]$Kind = 'endpoint' - ) - $lock = Get-TransportQueueLock - try { - $line = @{ - ts = (Get-Date).ToUniversalTime().ToString('o') - uri = $Uri - payload = $Payload - kind = $Kind - } | ConvertTo-Json -Compress - Add-Content -LiteralPath $script:TransportQueuePath -Value $line -Encoding UTF8 - $script:TransportMetrics.eventsEnqueued++ - } - finally { - $lock.Dispose() - } -} - -function Read-TransportQueueItems { - if (-not (Test-Path -LiteralPath $script:TransportQueuePath)) { return @() } - $items = @() - foreach ($line in @(Get-Content -LiteralPath $script:TransportQueuePath -ErrorAction SilentlyContinue)) { - if ([string]::IsNullOrWhiteSpace($line)) { continue } - try { $items += ($line | ConvertFrom-Json) } catch {} - } - return $items -} - -function Write-TransportQueueItems { - param([object[]]$Items = @()) - - $lines = @() - foreach ($item in @($Items)) { - if ($null -eq $item) { continue } - $lines += ($item | ConvertTo-Json -Compress) - } - - Set-Content -LiteralPath $script:TransportQueuePath -Value $lines -Encoding UTF8 - $script:TransportMetrics.queueDepth = @($Items).Count -} - -function Flush-TransportQueue { - param([int]$MaxItems = 200) - if (-not (Test-Path -LiteralPath $script:TransportQueuePath)) { return } - - $items = @() - $lock = Get-TransportQueueLock - try { - $items = @(Read-TransportQueueItems) - $itemCount = @($items).Count - $script:TransportMetrics.queueDepth = $itemCount - if ($itemCount -eq 0) { return } - - # Drain the on-disk queue under lock, then release the lock before network I/O. - # This prevents one stalled POST from blocking every concurrent enqueue/flush attempt. - Write-TransportQueueItems -Items @() - } - finally { - $lock.Dispose() - } - - $retryItems = @() - $sent = 0 - foreach ($item in @($items)) { - if ($null -eq $item) { continue } - if ($sent -ge $MaxItems) { - $retryItems += $item - continue - } - try { - Invoke-AwJsonPost -Uri ([string]$item.uri) -Json ([string]$item.payload) - $sent++ - $script:TransportMetrics.eventsFlushed++ - } - catch { - $script:TransportMetrics.sendFailures++ - $retryItems += $item - } - } - - $lock = Get-TransportQueueLock - try { - $concurrentItems = @(Read-TransportQueueItems) - Write-TransportQueueItems -Items (@($retryItems) + @($concurrentItems)) - } - finally { - $lock.Dispose() - } -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - # Fast-path: if bucket already exists, don't POST. - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" -TimeoutSec 10 -DisableKeepAlive -ErrorAction Stop | Out-Null - Write-EndpointLog ("bucket ok (GET): {0}" -f $BucketId) - $script:KnownBuckets[$BucketId] = $true - return - } - catch { - Write-EndpointLog ("bucket GET failed: {0} err={1}" -f $BucketId, $_.Exception.Message) - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - try { - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - } - catch { - # If create failed (race), verify it exists now. - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" -TimeoutSec 10 -DisableKeepAlive | Out-Null - } - catch { - throw - } - } - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Add-TransportQueueItem -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Payload $payload -Kind 'endpoint_signal' - Flush-TransportQueue -MaxItems 50 -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { - } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Add-TransportQueueItem -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Payload $payload -Kind 'dlp_incident' - Flush-TransportQueue -MaxItems 100 -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Clipboard is not reliably accessible from Session 0 (SYSTEM). Avoid noisy thread hacks there. - if ($script:SessionId -eq 0) { - return $null - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $script:__aw_clip = $null - $threadStart = [System.Threading.ThreadStart]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread = New-Object System.Threading.Thread($threadStart) - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { - try { $thread.Abort() } catch {} - } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - contentAnalysis = [ordered]@{ - dictionaryPack = $null - regexPack = $null - ocrEnabled = $false - } - nativeControls = [ordered]@{ - mode = 'monitor' - rollout = [ordered]@{ - allowGlobalBlock = $false - } - channels = [ordered]@{ - clipboard = [ordered]@{ action = 'audit' } - usb = [ordered]@{ action = 'audit' } - print = [ordered]@{ action = 'audit' } - } - } - } - - $script:PolicySource = 'defaults' - $script:PolicyVersion = $null - $script:PolicyChecksum = $null - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - $props = @() - try { $props = @($raw.endpoint.PSObject.Properties.Name) } catch { $props = @() } - if ($props -contains 'clipboard' -and $raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($props -contains 'usb' -and $raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($props -contains 'print' -and $raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - - if ($raw.contentAnalysis) { - if ($raw.contentAnalysis.PSObject.Properties.Name -contains 'dictionaryPack' -and $raw.contentAnalysis.dictionaryPack) { - $script:Policy.contentAnalysis.dictionaryPack = [string]$raw.contentAnalysis.dictionaryPack - } - if ($raw.contentAnalysis.PSObject.Properties.Name -contains 'regexPack' -and $raw.contentAnalysis.regexPack) { - $script:Policy.contentAnalysis.regexPack = [string]$raw.contentAnalysis.regexPack - } - if ($raw.contentAnalysis.PSObject.Properties.Name -contains 'ocrEnabled') { - $script:Policy.contentAnalysis.ocrEnabled = [bool]$raw.contentAnalysis.ocrEnabled - } - } - if ($raw.nativeControls) { - $nativeProps = @($raw.nativeControls.PSObject.Properties.Name) - if ($nativeProps -contains 'mode' -and $raw.nativeControls.mode) { - $script:Policy.nativeControls.mode = ([string]$raw.nativeControls.mode).ToLowerInvariant() - } - if ($nativeProps -contains 'rollout' -and $raw.nativeControls.rollout) { - $rolloutProps = @($raw.nativeControls.rollout.PSObject.Properties.Name) - if ($rolloutProps -contains 'allowGlobalBlock') { - $script:Policy.nativeControls.rollout.allowGlobalBlock = [bool]$raw.nativeControls.rollout.allowGlobalBlock - } - } - if ($nativeProps -contains 'channels' -and $raw.nativeControls.channels) { - foreach ($channelName in @('clipboard', 'usb', 'print')) { - if (@($raw.nativeControls.channels.PSObject.Properties.Name) -contains $channelName) { - $channel = $raw.nativeControls.channels.$channelName - if ($channel -and (@($channel.PSObject.Properties.Name) -contains 'action') -and $channel.action) { - $script:Policy.nativeControls.channels[$channelName].action = ([string]$channel.action).ToLowerInvariant() - } - } - } - } - } - $script:PolicySource = 'local' - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Resolve-DlpEffectiveAction { - param( - [Parameter(Mandatory = $true)][string]$RequestedAction, - [Parameter(Mandatory = $true)][ValidateSet('clipboard', 'usb', 'print')][string]$Channel - ) - - $requested = $RequestedAction.ToLowerInvariant() - $mode = ([string]$script:Policy.nativeControls.mode).ToLowerInvariant() - $allowGlobalBlock = [bool]$script:Policy.nativeControls.rollout.allowGlobalBlock - $channelAction = 'audit' - try { - $channelAction = ([string]$script:Policy.nativeControls.channels[$Channel].action).ToLowerInvariant() - } - catch { - $channelAction = 'audit' - } - - $suppressed = $false - $effective = $requested - if ($requested -eq 'block') { - $channelAllowsBlock = $channelAction -in @('block', 'blockwithoverride') - if ($mode -ne 'enforce' -or -not $allowGlobalBlock -or -not $channelAllowsBlock) { - $effective = 'alert' - $suppressed = $true - } - } - - return [pscustomobject]@{ - requestedAction = $requested - action = $effective - enforcementMode = $mode - nativeChannelAction = $channelAction - enforcementSuppressed = $suppressed - } -} - -function Invoke-SuppressedBlockSelfTest { - $decisions = @() - foreach ($channel in @('clipboard', 'usb', 'print')) { - $decisions += (Resolve-DlpEffectiveAction -RequestedAction 'block' -Channel $channel) - } - - $failed = @( - $decisions | - Where-Object { $_.action -eq 'block' -or -not [bool]$_.enforcementSuppressed } - ) - - $result = [ordered]@{ - ok = (@($failed).Count -eq 0) - test = 'suppressed-block-in-monitor' - policySource = $script:PolicySource - policyMode = $script:PolicyMode - decisions = @($decisions) - } - - $result | ConvertTo-Json -Depth 6 - if (-not $result.ok) { - exit 2 - } - exit 0 -} - -function Test-ValidInn { - param([string]$Value) - $digits = ($Value -replace '\D', '') - if ($digits.Length -eq 10) { - $coef = @(2, 4, 10, 3, 5, 9, 4, 6, 8) - $sum = 0 - for ($i = 0; $i -lt 9; $i++) { $sum += ([int][string]$digits[$i]) * $coef[$i] } - $chk = ($sum % 11) % 10 - return $chk -eq ([int][string]$digits[9]) - } - if ($digits.Length -eq 12) { - $c11 = @(7, 2, 4, 10, 3, 5, 9, 4, 6, 8) - $c12 = @(3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8) - $sum11 = 0 - for ($i = 0; $i -lt 10; $i++) { $sum11 += ([int][string]$digits[$i]) * $c11[$i] } - $sum12 = 0 - for ($i = 0; $i -lt 11; $i++) { $sum12 += ([int][string]$digits[$i]) * $c12[$i] } - return ((($sum11 % 11) % 10) -eq ([int][string]$digits[10])) -and ((($sum12 % 11) % 10) -eq ([int][string]$digits[11])) - } - return $false -} - -function Test-ValidSnils { - param([string]$Value) - $digits = ($Value -replace '\D', '') - if ($digits.Length -ne 11) { return $false } - $num = $digits.Substring(0, 9) - $checksum = [int]$digits.Substring(9, 2) - $sum = 0 - for ($i = 0; $i -lt 9; $i++) { $sum += ([int][string]$num[$i]) * (9 - $i) } - if ($sum -lt 100) { $expected = $sum } - elseif ($sum -eq 100 -or $sum -eq 101) { $expected = 0 } - else { - $expected = $sum % 101 - if ($expected -eq 100) { $expected = 0 } - } - return $checksum -eq $expected -} - -function Test-ValidPassport { - param([string]$Value) - $digits = ($Value -replace '\D', '') - if ($digits.Length -ne 10) { return $false } - if ($digits -eq '0000000000') { return $false } - return ($digits.ToCharArray() | Select-Object -Unique).Count -gt 1 -} - -function Get-AdvancedContentMatches { - param( - [string]$Text, - [string]$DictionaryPack, - [string]$RegexPack - ) - - $result = @{ - dictionaryMatches = @() - regexMatches = @() - } - if ([string]::IsNullOrWhiteSpace($Text)) { return $result } - - if ($DictionaryPack -eq '152-fz-pdn') { - $m = [regex]::Matches($Text, '\b\d{10}\b|\b\d{12}\b') - foreach ($item in $m) { - if (Test-ValidInn -Value $item.Value) { - $result.dictionaryMatches += @{ name = 'inn'; value = $item.Value; severity = 'high' } - } - } - $m = [regex]::Matches($Text, '\b\d{3}-\d{3}-\d{3}\s?\d{2}\b') - foreach ($item in $m) { - if (Test-ValidSnils -Value $item.Value) { - $result.dictionaryMatches += @{ name = 'snils'; value = $item.Value; severity = 'high' } - } - } - $m = [regex]::Matches($Text, '\b\d{4}\s?\d{6}\b') - foreach ($item in $m) { - if (Test-ValidPassport -Value $item.Value) { - $result.dictionaryMatches += @{ name = 'passport'; value = $item.Value; severity = 'high' } - } - } - } - - $regexRules = @() - switch ($RegexPack) { - 'financial' { - $regexRules = @( - @{ id = 'card-pan'; regex = '\b(?:\d[ -]*?){13,19}\b'; severity = 'high' }, - @{ id = 'iban'; regex = '\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b'; severity = 'medium' } - ) - } - 'contacts' { - $regexRules = @( - @{ id = 'email'; regex = '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'; severity = 'low' }, - @{ id = 'phone-ru'; regex = '(?:\+7|8)\s*\(?\d{3}\)?\s*\d{3}[- ]?\d{2}[- ]?\d{2}'; severity = 'low' } - ) - } - 'secrets' { - $regexRules = @( - @{ id = 'aws-access-key'; regex = 'AKIA[0-9A-Z]{16}'; severity = 'high' }, - @{ id = 'generic-password'; regex = '(?i)(password|пароль)\s*[:=]\s*\S{6,}'; severity = 'medium' } - ) - } - } - foreach ($rule in $regexRules) { - $m = [regex]::Matches($Text, [string]$rule.regex) - foreach ($item in $m) { - $result.regexMatches += @{ name = [string]$rule.id; value = $item.Value; severity = [string]$rule.severity } - } - } - - return $result -} - -function Apply-PolicyFromBundle { - param( - [Parameter(Mandatory = $true)]$Bundle, - [Parameter(Mandatory = $true)][string]$Source - ) - - if (-not $Bundle.policy) { - throw 'Policy bundle has no policy payload.' - } - - $tempPath = [System.IO.Path]::GetTempFileName() - try { - $Bundle.policy | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $tempPath -Encoding UTF8 - Load-DlpPolicy -Path $tempPath - $script:PolicySource = $Source - $script:PolicyVersion = if ($Bundle.PSObject.Properties.Name -contains 'version') { [string]$Bundle.version } else { $null } - $script:PolicyChecksum = if ($Bundle.PSObject.Properties.Name -contains 'checksum') { [string]$Bundle.checksum } else { $null } - } - finally { - Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue - } -} - -function Refresh-DlpPolicyFromServer { - if (-not $script:PolicyEngineEnabled) { - return $false - } - if (-not $script:PolicyClientAvailable) { - Write-EndpointLog 'policy client module unavailable, cannot use server mode' - return $false - } - - try { - $bundle = Get-RemoteDlpPolicyBundle -ApiBase $script:PolicyApiBase -TimeoutSec 10 - Save-CachedDlpPolicyBundle -Bundle $bundle -CachePath $script:PolicyCachePath - Apply-PolicyFromBundle -Bundle $bundle -Source 'server' - $script:LastPolicyRefreshAt = (Get-Date).ToUniversalTime() - Write-EndpointLog ("policy refreshed from server version={0} checksum={1}" -f $script:PolicyVersion, $script:PolicyChecksum) - return $true - } - catch { - Write-EndpointLog ("policy refresh failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Sync-DlpPolicyDesiredState { - if (-not $script:PolicyEngineEnabled -or -not $script:PolicyClientAvailable) { - return $false - } - if (-not $script:PolicyAgentId) { - return $false - } - - try { - [void](Send-DlpPolicyAgentHeartbeat -ApiBase $script:PolicyApiBase -AgentId $script:PolicyAgentId -Hostname $script:Hostname -Version $script:PolicyVersion -Checksum $script:PolicyChecksum -TimeoutSec 10) - $desired = Get-RemoteDlpPolicyDesired -ApiBase $script:PolicyApiBase -AgentId $script:PolicyAgentId -TimeoutSec 10 - if ($desired -and $desired.refreshNow -eq $true) { - Write-EndpointLog ("policy desired refresh requested: reason={0}" -f $desired.reason) - return (Refresh-DlpPolicyFromServer) - } - return $true - } - catch { - Write-EndpointLog ("policy desired sync failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Initialize-DlpPolicy { - if ($script:PolicyMode -eq 'server') { - if (Refresh-DlpPolicyFromServer) { - return - } - - if ($script:PolicyClientAvailable) { - $cached = Read-CachedDlpPolicyBundle -CachePath $script:PolicyCachePath - if ($cached) { - try { - Apply-PolicyFromBundle -Bundle $cached -Source 'cache' - Write-EndpointLog ("policy loaded from cache version={0} checksum={1}" -f $script:PolicyVersion, $script:PolicyChecksum) - return - } - catch { - Write-EndpointLog ("cached policy load failed: {0}" -f $_.Exception.Message) - } - } - } - - Load-DlpPolicy -Path $script:LocalPolicyPath - $script:PolicySource = 'local-fallback' - return - } - - Load-DlpPolicy -Path $script:LocalPolicyPath -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - $dictionaryPack = if ($rule.dictionaryPack) { [string]$rule.dictionaryPack } elseif ($script:Policy.contentAnalysis.dictionaryPack) { [string]$script:Policy.contentAnalysis.dictionaryPack } else { $null } - $regexPack = if ($rule.regexPack) { [string]$rule.regexPack } elseif ($script:Policy.contentAnalysis.regexPack) { [string]$script:Policy.contentAnalysis.regexPack } else { $null } - $ocrEnabled = if ($rule.PSObject.Properties.Name -contains 'ocrEnabled') { [bool]$rule.ocrEnabled } else { [bool]$script:Policy.contentAnalysis.ocrEnabled } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - $advanced = Get-AdvancedContentMatches -Text $ClipboardText -DictionaryPack $dictionaryPack -RegexPack $regexPack - $advancedMatched = (@($advanced.dictionaryMatches).Count -gt 0) -or (@($advanced.regexMatches).Count -gt 0) - if ($advancedMatched) { $matched = $true } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $requestedAction = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $actionDecision = Resolve-DlpEffectiveAction -RequestedAction $requestedAction -Channel 'clipboard' - $action = [string]$actionDecision.action - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - requestedAction = [string]$actionDecision.requestedAction - enforcementMode = [string]$actionDecision.enforcementMode - nativeChannelAction = [string]$actionDecision.nativeChannelAction - enforcementSuppressed = [bool]$actionDecision.enforcementSuppressed - dictionaryPack = $dictionaryPack - regexPack = $regexPack - dictionaryMatches = @($advanced.dictionaryMatches) - regexMatches = @($advanced.regexMatches) - ocrRequested = $ocrEnabled - } - Write-EndpointLog ("incident clipboard rule={0} requested={1} action={2} severity={3} enforced={4} suppressed={5}" -f $ruleId, $requestedAction, $action, $severity, $enforced, [bool]$actionDecision.enforcementSuppressed) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $requestedAction = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $actionDecision = Resolve-DlpEffectiveAction -RequestedAction $requestedAction -Channel 'usb' - $action = [string]$actionDecision.action - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - requestedAction = [string]$actionDecision.requestedAction - enforcementMode = [string]$actionDecision.enforcementMode - nativeChannelAction = [string]$actionDecision.nativeChannelAction - enforcementSuppressed = [bool]$actionDecision.enforcementSuppressed - } - Write-EndpointLog ("incident usb rule={0} requested={1} action={2} severity={3} drive={4} enforced={5} suppressed={6}" -f $ruleId, $requestedAction, $action, $severity, $DriveLetter, $enforced, [bool]$actionDecision.enforcementSuppressed) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - $dictionaryPack = if ($rule.dictionaryPack) { [string]$rule.dictionaryPack } elseif ($script:Policy.contentAnalysis.dictionaryPack) { [string]$script:Policy.contentAnalysis.dictionaryPack } else { $null } - $regexPack = if ($rule.regexPack) { [string]$rule.regexPack } elseif ($script:Policy.contentAnalysis.regexPack) { [string]$script:Policy.contentAnalysis.regexPack } else { $null } - $ocrEnabled = if ($rule.PSObject.Properties.Name -contains 'ocrEnabled') { [bool]$rule.ocrEnabled } else { [bool]$script:Policy.contentAnalysis.ocrEnabled } - $advanced = Get-AdvancedContentMatches -Text $DocumentName -DictionaryPack $dictionaryPack -RegexPack $regexPack - $advancedMatched = (@($advanced.dictionaryMatches).Count -gt 0) -or (@($advanced.regexMatches).Count -gt 0) - if ($advancedMatched) { $match = $true } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $requestedAction = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $actionDecision = Resolve-DlpEffectiveAction -RequestedAction $requestedAction -Channel 'print' - $action = [string]$actionDecision.action - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - requestedAction = [string]$actionDecision.requestedAction - enforcementMode = [string]$actionDecision.enforcementMode - nativeChannelAction = [string]$actionDecision.nativeChannelAction - enforcementSuppressed = [bool]$actionDecision.enforcementSuppressed - dictionaryPack = $dictionaryPack - regexPack = $regexPack - dictionaryMatches = @($advanced.dictionaryMatches) - regexMatches = @($advanced.regexMatches) - ocrRequested = $ocrEnabled - } - Write-EndpointLog ("incident print rule={0} requested={1} action={2} severity={3} printer={4} enforced={5} suppressed={6}" -f $ruleId, $requestedAction, $action, $severity, $PrinterName, $enforced, [bool]$actionDecision.enforcementSuppressed) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedStateRoot = if ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'stateRoot') { [string]$deploymentConfig.paths.stateRoot } else { Split-Path -Path $resolvedPolicyPath -Parent } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA '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 } -$resolvedHostname = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$deploymentConfig.awHostname)) { [string]$deploymentConfig.awHostname } else { [string]$env:COMPUTERNAME } -$resolvedPolicyMode = if ($PolicyMode) { [string]$PolicyMode } elseif ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'policyEngine' -and $deploymentConfig.policyEngine.PSObject.Properties.Name -contains 'mode') { [string]$deploymentConfig.policyEngine.mode } else { 'local' } -$resolvedPolicyEngineEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'policyEngine' -and $deploymentConfig.policyEngine.PSObject.Properties.Name -contains 'enabled') { [bool]$deploymentConfig.policyEngine.enabled } else { $false } -$resolvedPolicyEngineHost = if ($PolicyEngineHost) { [string]$PolicyEngineHost } elseif ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'policyEngine' -and $deploymentConfig.policyEngine.PSObject.Properties.Name -contains 'host') { [string]$deploymentConfig.policyEngine.host } else { $resolvedServerHost } -$resolvedPolicyEnginePort = if ($PSBoundParameters.ContainsKey('PolicyEnginePort')) { $PolicyEnginePort } elseif ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'policyEngine' -and $deploymentConfig.policyEngine.PSObject.Properties.Name -contains 'port') { [int]$deploymentConfig.policyEngine.port } else { $resolvedServerPort } -$resolvedPolicyEngineScheme = if ($PolicyEngineScheme) { [string]$PolicyEngineScheme } elseif ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'policyEngine' -and $deploymentConfig.policyEngine.PSObject.Properties.Name -contains 'scheme') { [string]$deploymentConfig.policyEngine.scheme } else { $resolvedServerScheme } -$resolvedPolicyRefreshSeconds = if ($PSBoundParameters.ContainsKey('PolicyRefreshSeconds')) { $PolicyRefreshSeconds } elseif ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'policyEngine' -and $deploymentConfig.policyEngine.PSObject.Properties.Name -contains 'refreshSeconds') { [int]$deploymentConfig.policyEngine.refreshSeconds } else { 300 } -$resolvedPolicyCachePath = if ($PolicyCachePath) { [string]$PolicyCachePath } elseif ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'policyEngine' -and $deploymentConfig.policyEngine.PSObject.Properties.Name -contains 'cachePath') { [string]$deploymentConfig.policyEngine.cachePath } else { Join-Path $resolvedStateRoot 'dlp-policy-cache.json' } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:PolicyApiBase = '{0}://{1}:{2}/api/0' -f $resolvedPolicyEngineScheme, $resolvedPolicyEngineHost, $resolvedPolicyEnginePort -$script:Hostname = $resolvedHostname -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false -$script:PolicyMode = $resolvedPolicyMode -$script:PolicyEngineEnabled = $resolvedPolicyEngineEnabled -$script:PolicyRefreshSeconds = [Math]::Max($resolvedPolicyRefreshSeconds, 60) -$script:PolicyCachePath = $resolvedPolicyCachePath -$script:LocalPolicyPath = $resolvedPolicyPath -$script:LastPolicyRefreshAt = [datetime]::MinValue -$script:PolicyAgentId = $resolvedHostname -$script:TransportBackoffSeconds = 1 -# Integration test flag (backward compatible - defaults to false) -$script:IntegrationTestEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'integrationTestEnabled') { [bool]$deploymentConfig.integrationTestEnabled } else { $false } - -# Integration metadata tracking (backward compatible) -$script:TotalEventsProcessed = 0 -$script:LastEventTime = $null - -Initialize-TransportQueue -StateRoot $resolvedStateRoot -Initialize-DlpPolicy -if ($SelfTestSuppressedBlock) { - Invoke-SuppressedBlockSelfTest -} -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - try { - Flush-TransportQueue -MaxItems 200 - $script:TransportBackoffSeconds = 1 - } - catch { - $script:TransportBackoffSeconds = [Math]::Min($script:TransportBackoffSeconds * 2, 60) - Write-EndpointLog ("transport flush failed, backoff={0}s err={1}" -f $script:TransportBackoffSeconds, $_.Exception.Message) - } - - if ($script:PolicyMode -eq 'server') { - $policyAge = ((Get-Date).ToUniversalTime() - $script:LastPolicyRefreshAt).TotalSeconds - if ($policyAge -ge $script:PolicyRefreshSeconds) { - [void](Refresh-DlpPolicyFromServer) - } - else { - [void](Sync-DlpPolicyDesiredState) - } - } - - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - policyMode = $script:PolicyMode - policySource = $script:PolicySource - policyVersion = $script:PolicyVersion - policyChecksum = $script:PolicyChecksum - queueDepth = [int]$script:TransportMetrics.queueDepth - eventsEnqueued = [int]$script:TransportMetrics.eventsEnqueued - eventsFlushed = [int]$script:TransportMetrics.eventsFlushed - sendFailures = [int]$script:TransportMetrics.sendFailures - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - $script:TotalEventsProcessed++ - $script:LastEventTime = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { - } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - $script:TotalEventsProcessed++ - $script:LastEventTime = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { - } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - $script:TotalEventsProcessed++ - $script:LastEventTime = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { - } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - $script:TotalEventsProcessed++ - $script:LastEventTime = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { - } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - # Integration metadata self-test (backward compatible) - if ($script:IntegrationTestEnabled -and (Get-Date).Minute -eq 0) { - try { - $testMetadata = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - collector = 'dlp-endpoint-signals' - version = '1.0.0' - hostname = $env:COMPUTERNAME - username = $env:USERNAME - status = 'healthy' - checks = @{ - eventsProcessed = $script:TotalEventsProcessed - lastEventTime = $script:LastEventTime - iocRulesLoaded = if ($script:IocRules) { @($script:IocRules).Count } else { 0 } - policyRulesLoaded = if ($script:Policy -and $script:Policy.endpoint) { (@($script:Policy.endpoint.clipboard).Count + @($script:Policy.endpoint.usb).Count + @($script:Policy.endpoint.print).Count) } else { 0 } - } - } - Send-EndpointSignalHeartbeat -SignalType 'integration_test' -Data $testMetadata - Write-EndpointLog "Integration metadata test sent" - } - catch { - Write-EndpointLog "Integration test failed: $($_.Exception.Message)" - } - } - - if ($script:TransportBackoffSeconds -gt $resolvedPollSeconds) { - Start-Sleep -Seconds $script:TransportBackoffSeconds - } - else { - Start-Sleep -Seconds $resolvedPollSeconds - } -} diff --git a/install-kit-awindows-20260427-211240/windows/dlp-policy.example.json b/install-kit-awindows-20260427-211240/windows/dlp-policy.example.json deleted file mode 100644 index e8cae21..0000000 --- a/install-kit-awindows-20260427-211240/windows/dlp-policy.example.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "version": 1, - "defaults": { - "enabled": true, - "cooldownSeconds": 300, - "action": "log", - "severity": "low" - }, - "nativeControls": { - "mode": "monitor", - "rollout": { - "baselineDays": 7, - "requireGuardHeartbeat": true, - "allowGlobalBlock": false - }, - "channels": { - "clipboard": {"action": "audit"}, - "usb": {"action": "audit"}, - "print": {"action": "audit"} - } - }, - "rules": [ - { - "id": "personal-web-during-workhours", - "enabled": true, - "cooldownSeconds": 600, - "action": "alert", - "severity": "medium", - "message": "Личные ресурсы в рабочее время", - "when": { - "categoryGroups": ["personal"], - "hourFrom": 9, - "hourTo": 19 - } - }, - { - "id": "high-risk-cloud-storage", - "enabled": true, - "cooldownSeconds": 900, - "action": "alert", - "severity": "high", - "message": "Подозрительный доступ к облачному хранилищу", - "when": { - "domains": [ - "dropbox.com", - "drive.google.com", - "mega.nz", - "onedrive.live.com", - "disk.yandex.ru" - ] - } - }, - { - "id": "anonymizer-and-vpn-web", - "enabled": true, - "cooldownSeconds": 900, - "action": "alert", - "severity": "high", - "message": "Использование веб-анонимайзеров / VPN-сервисов", - "when": { - "domains": [ - "hidemy.name", - "2ip.ru", - "whoer.net", - "protonvpn.com", - "nordvpn.com" - ] - } - } - ], - "endpoint": { - "clipboard": [ - { - "id": "clipboard-sensitive-keywords", - "enabled": true, - "cooldownSeconds": 300, - "action": "alert", - "severity": "high", - "message": "В буфере обнаружены чувствительные ключевые слова", - "minLength": 20, - "regexPatterns": [ - "(?i)парол(ь|и)", - "(?i)password", - "(?i)secret", - "(?i)cvv", - "(?i)паспорт" - ] - } - ], - "usb": [ - { - "id": "usb-media-connected", - "enabled": true, - "cooldownSeconds": 300, - "action": "alert", - "severity": "medium", - "message": "Подключен съемный носитель" - } - ], - "print": [ - { - "id": "print-sensitive-docs", - "enabled": true, - "cooldownSeconds": 300, - "action": "alert", - "severity": "high", - "message": "Печать документа с признаками чувствительных данных", - "documentRegex": "(?i)(salary|зарплат|passport|паспорт|договор|contract)" - } - ] - }, - "contentAnalysis": { - "dictionaryPack": "152-fz-pdn", - "regexPack": "secrets", - "ocrEnabled": true - }, - "ioc": { - "enabled": true, - "source": "http://aw-server.example.local:5610/dlp-ioc/ioc_blacklist.json", - "format": "hayabusa_sigma_v1", - "refreshMinutes": 360 - } -} diff --git a/install-kit-awindows-20260427-211240/windows/dlp-policy.native-cross-os.example.json b/install-kit-awindows-20260427-211240/windows/dlp-policy.native-cross-os.example.json deleted file mode 100644 index b8710b0..0000000 --- a/install-kit-awindows-20260427-211240/windows/dlp-policy.native-cross-os.example.json +++ /dev/null @@ -1,145 +0,0 @@ -{ - "version": 1, - "defaults": { - "enabled": true, - "cooldownSeconds": 300, - "action": "alert", - "severity": "medium" - }, - "nativeControls": { - "mode": "monitor", - "rollout": { - "baselineDays": 7, - "requireGuardHeartbeat": true, - "allowGlobalBlock": false - }, - "channels": { - "removableStorage": { - "action": "audit", - "windows": { - "mechanisms": ["gpo-device-restrictions", "set-disk-readonly"], - "target": "write" - }, - "linux": { - "mechanisms": ["fanotify", "auditd"], - "target": "mountpoints" - }, - "macos": { - "mechanisms": ["mdm-restrictions", "endpoint-security"], - "target": "managed-devices" - }, - "chromeos": { - "mechanisms": ["data-controls"], - "target": "removable-storage" - } - }, - "print": { - "action": "audit", - "windows": { - "mechanisms": ["printservice-operational-log", "spooler-cancel-job"] - }, - "linux": { - "mechanisms": ["cups-logs"], - "enforcement": "monitor-only" - }, - "macos": { - "mechanisms": ["mdm-printing-restrictions"], - "enforcement": "managed-only" - }, - "chromeos": { - "mechanisms": ["data-controls"] - } - }, - "clipboard": { - "action": "audit", - "windows": { - "mechanisms": ["clipboard-monitor", "clear-clipboard"], - "blockScope": "high-confidence-only" - }, - "linux": { - "mechanisms": ["desktop-clipboard-monitor"], - "enforcement": "monitor-only" - }, - "macos": { - "mechanisms": ["endpoint-monitor", "mdm-restrictions"], - "enforcement": "monitor-first" - }, - "chromeos": { - "mechanisms": ["data-controls"] - } - }, - "browserUpload": { - "action": "audit", - "windows": { - "mechanisms": ["managed-browser-policy", "browser-extension"] - }, - "linux": { - "mechanisms": ["managed-browser-policy", "proxy-logs"] - }, - "macos": { - "mechanisms": ["managed-browser-policy", "network-extension"] - }, - "chromeos": { - "mechanisms": ["data-controls"] - } - }, - "appExecution": { - "action": "audit", - "windows": { - "mechanisms": ["applocker", "wdac"] - }, - "linux": { - "mechanisms": ["auditd", "fanotify", "bpf-lsm"], - "enforcement": "fanotify-or-lsm-only" - }, - "macos": { - "mechanisms": ["mdm-restrictions", "endpoint-security"] - }, - "chromeos": { - "mechanisms": ["admin-console-app-policy"] - } - } - } - }, - "endpoint": { - "clipboard": [ - { - "id": "clipboard-sensitive-keywords", - "enabled": true, - "cooldownSeconds": 300, - "action": "alert", - "severity": "high", - "message": "В буфере обнаружены чувствительные ключевые слова", - "minLength": 20, - "regexPatterns": [ - "(?i)парол(ь|и)", - "(?i)password", - "(?i)secret", - "(?i)cvv", - "(?i)паспорт" - ] - } - ], - "usb": [ - { - "id": "usb-media-connected", - "enabled": true, - "cooldownSeconds": 300, - "action": "alert", - "severity": "medium", - "message": "Подключен съемный носитель" - } - ], - "print": [ - { - "id": "print-sensitive-docs", - "enabled": true, - "cooldownSeconds": 300, - "action": "alert", - "severity": "high", - "message": "Печать документа с признаками чувствительных данных", - "documentRegex": "(?i)(salary|зарплат|passport|паспорт|договор|contract)" - } - ] - } -} diff --git a/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1 b/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1 deleted file mode 100644 index 96c6461..0000000 --- a/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1 +++ /dev/null @@ -1,624 +0,0 @@ -<# -.SYNOPSIS - DLP email outbound collector for AWatch-rus (Phase 2.5). - Monitors outgoing email via Outlook COM Sent Items polling - and/or SMTP network connection detection. - -.DESCRIPTION - Two collection modes (configurable, can run simultaneously): - - outlook : Polls Outlook Sent Items via COM for new messages. - - smtp : Monitors SMTP connections (ports 25/587/465) via - Get-NetTCPConnection for any process sending mail. - - Sends heartbeats to AW bucket `aw-email-monitor_`. - Evaluates DLP policy rules from `endpoint.email[]` section. - Supports enforcement: action="block" moves the email to Drafts - (Outlook mode) or logs with enforced=false (SMTP mode). -#> -[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, - [ValidateSet('outlook', 'smtp', 'both')] - [string]$Mode = 'smtp' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# --------------------------------------------------------------------------- -# Shared infrastructure (mirrors other collectors) -# --------------------------------------------------------------------------- - -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-CollectorLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { return } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { } -} - -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( - [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 Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { $sha.Dispose() } -} - -function Send-EmailHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - $bucketId = 'aw-email-monitor_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-email-monitor' -BucketType 'aw.dlp.email' - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'email-outbound-collector' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-EmailIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [hashtable]$Data - ) - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = 'email_outbound' - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'email-outbound-collector' - } + $Data - } | ConvertTo-Json -Depth 7 -Compress - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { } -} - -# --------------------------------------------------------------------------- -# DLP Policy -# --------------------------------------------------------------------------- - -function Load-EmailPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - email = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-CollectorLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - if ($raw.endpoint -and $raw.endpoint.email) { - $script:Policy.endpoint.email = @($raw.endpoint.email) - } - } - catch { - Write-CollectorLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - $script:Cooldown[$Fingerprint] = $now - return $true -} - -# --------------------------------------------------------------------------- -# Email DLP rule evaluation -# --------------------------------------------------------------------------- - -function Evaluate-EmailRules { - param( - [string]$Subject, - [string]$RecipientsJoined, - [string]$SenderAddress, - [int]$AttachmentCount, - [string]$AttachmentNames, - [int]$BodyLength, - [string]$MessageId, - $OutlookMailItem - ) - - foreach ($rule in @($script:Policy.endpoint.email)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $matched = $true - - if ($rule.subjectRegex) { - $matched = $matched -and ($Subject -match [string]$rule.subjectRegex) - } - if ($rule.recipientRegex) { - $matched = $matched -and ($RecipientsJoined -match [string]$rule.recipientRegex) - } - if ($rule.senderRegex) { - $matched = $matched -and ($SenderAddress -match [string]$rule.senderRegex) - } - if ($rule.attachmentRegex -and $AttachmentNames) { - $matched = $matched -and ($AttachmentNames -match [string]$rule.attachmentRegex) - } - if ($rule.minAttachments) { - $matched = $matched -and ($AttachmentCount -ge [int]$rule.minAttachments) - } - if ($rule.minBodyLength) { - $matched = $matched -and ($BodyLength -ge [int]$rule.minBodyLength) - } - if ($rule.externalOnly -and [bool]$rule.externalOnly) { - $internalDomain = if ($rule.internalDomain) { [string]$rule.internalDomain } else { '' } - if ($internalDomain -and $RecipientsJoined -notmatch [regex]::Escape($internalDomain)) { - # all recipients are external — continue matching - } - elseif ($internalDomain) { - $matched = $false - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "email|$ruleId|$MessageId|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Email rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block' -and $null -ne $OutlookMailItem) { - $enforced = Invoke-EmailEnforcement -MailItem $OutlookMailItem -RuleId $ruleId - Show-EnforcementNotification -Title 'DLP: письмо перемещено в черновики' -Body $message - } - elseif ($action -eq 'block') { - Show-EnforcementNotification -Title 'DLP: обнаружена отправка письма' -Body $message - } - - Send-EmailIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -Data @{ - subject = (Get-StringHash -Value $Subject) - recipients = (Get-StringHash -Value $RecipientsJoined) - sender = $SenderAddress - attachmentCount = $AttachmentCount - attachmentNames = $AttachmentNames - bodyLength = $BodyLength - enforced = $enforced - } - Write-CollectorLog ("incident email rule={0} action={1} severity={2} enforced={3} subject_hash={4}" -f $ruleId, $action, $severity, $enforced, (Get-StringHash -Value $Subject)) - } -} - -function Invoke-EmailEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)]$MailItem, - [string]$RuleId - ) - try { - $draftsFolder = $script:OutlookNamespace.GetDefaultFolder(16) # olFolderDrafts - $MailItem.Move($draftsFolder) | Out-Null - Write-CollectorLog ("enforcement: email moved to Drafts rule={0} subject_hash={1}" -f $RuleId, (Get-StringHash -Value $MailItem.Subject)) - return $true - } - catch { - Write-CollectorLog ("enforcement: email move to Drafts failed rule={0}: {1}" -f $RuleId, $_.Exception.Message) - return $false - } -} - -# --------------------------------------------------------------------------- -# Outlook Sent Items polling -# --------------------------------------------------------------------------- - -function Initialize-OutlookCom { - if ($script:OutlookDisabled) { - return $false - } - - if (-not (Test-OutlookProfileConfigured)) { - Write-CollectorLog "Outlook profile not configured for current user, Outlook mode disabled" - $script:OutlookDisabled = $true - return $false - } - - if (-not (Get-Process -Name OUTLOOK -ErrorAction SilentlyContinue | Select-Object -First 1)) { - Write-CollectorLog "Outlook process not running, skipping COM initialization" - return $false - } - - try { - $script:OutlookApp = [Runtime.InteropServices.Marshal]::GetActiveObject('Outlook.Application') - $script:OutlookNamespace = $script:OutlookApp.GetNamespace('MAPI') - $script:SentFolder = $script:OutlookNamespace.GetDefaultFolder(5) # olFolderSentMail - Write-CollectorLog "Outlook COM initialized, Sent Items folder opened" - return $true - } - catch { - Write-CollectorLog ("Outlook COM init failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Test-OutlookProfileConfigured { - [OutputType([bool])] - $officeRoots = @( - 'HKCU:\Software\Microsoft\Office', - 'HKCU:\Software\WOW6432Node\Microsoft\Office' - ) - - foreach ($root in $officeRoots) { - if (-not (Test-Path -LiteralPath $root)) { continue } - $versions = Get-ChildItem -LiteralPath $root -ErrorAction SilentlyContinue | - Where-Object { $_.PSChildName -match '^\d+\.\d+$' } | - Sort-Object { [version]$_.PSChildName } -Descending - foreach ($ver in $versions) { - $profilesPath = Join-Path $ver.PSPath 'Outlook\Profiles' - if (Test-Path -LiteralPath $profilesPath) { - $profiles = Get-ChildItem -LiteralPath $profilesPath -ErrorAction SilentlyContinue - if ($profiles -and $profiles.Count -gt 0) { - return $true - } - } - } - } - - return $false -} - -function Get-OutlookSentItems { - param([datetime]$Since) - - $results = @() - try { - $items = $script:SentFolder.Items - $items.Sort('[SentOn]', $true) - - $filter = "[SentOn] >= '{0}'" -f $Since.ToString('MM/dd/yyyy HH:mm') - $restricted = $items.Restrict($filter) - - foreach ($item in $restricted) { - try { - if ($item.Class -ne 43) { continue } # olMail = 43 - - $recipients = @() - for ($i = 1; $i -le $item.Recipients.Count; $i++) { - $recip = $item.Recipients.Item($i) - $recipients += [string]$recip.Address - } - - $attachmentNames = @() - for ($i = 1; $i -le $item.Attachments.Count; $i++) { - $attachmentNames += [string]$item.Attachments.Item($i).FileName - } - - $results += [pscustomobject]@{ - EntryID = [string]$item.EntryID - Subject = [string]$item.Subject - SenderAddress = [string]$item.SenderEmailAddress - SenderName = [string]$item.SenderName - Recipients = $recipients - RecipientsJoined = ($recipients -join '; ') - AttachmentCount = [int]$item.Attachments.Count - AttachmentNames = ($attachmentNames -join '; ') - BodyLength = if ($item.Body) { $item.Body.Length } else { 0 } - SentOn = $item.SentOn - MailItem = $item - } - } - catch { } - } - } - catch { - Write-CollectorLog ("Outlook Sent Items scan failed: {0}" -f $_.Exception.Message) - } - return $results -} - -function Poll-OutlookSentItems { - $items = Get-OutlookSentItems -Since $script:OutlookLastPoll - - foreach ($item in $items) { - $entryId = $item.EntryID - if ($script:SeenEntryIds.ContainsKey($entryId)) { continue } - $script:SeenEntryIds[$entryId] = (Get-Date).ToUniversalTime() - - $subjectHash = Get-StringHash -Value $item.Subject - - Send-EmailHeartbeat -SignalType 'email_sent' -Data @{ - subject = $subjectHash - sender = [string]$item.SenderAddress - senderName = [string]$item.SenderName - recipientCount = $item.Recipients.Count - recipients = (Get-StringHash -Value $item.RecipientsJoined) - attachmentCount = [int]$item.AttachmentCount - attachmentNames = [string]$item.AttachmentNames - bodyLength = [int]$item.BodyLength - sentOn = if ($item.SentOn) { $item.SentOn.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') } else { '' } - collectionMode = 'outlook' - } - Write-CollectorLog ("email_sent outlook subject_hash={0} to={1} attachments={2}" -f $subjectHash, $item.Recipients.Count, $item.AttachmentCount) - - Evaluate-EmailRules ` - -Subject $item.Subject ` - -RecipientsJoined $item.RecipientsJoined ` - -SenderAddress $item.SenderAddress ` - -AttachmentCount $item.AttachmentCount ` - -AttachmentNames $item.AttachmentNames ` - -BodyLength $item.BodyLength ` - -MessageId $entryId ` - -OutlookMailItem $item.MailItem - } - - $script:OutlookLastPoll = (Get-Date).AddSeconds(-10) - - # Cleanup old entry IDs (keep last 24h) - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-24) - foreach ($k in @($script:SeenEntryIds.Keys)) { - if ([datetime]$script:SeenEntryIds[$k] -lt $cleanupBefore) { - $script:SeenEntryIds.Remove($k) - } - } -} - -# --------------------------------------------------------------------------- -# SMTP network connection monitoring -# --------------------------------------------------------------------------- - -function Poll-SmtpConnections { - try { - $smtpPorts = @(25, 587, 465, 2525) - $connections = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | - Where-Object { $smtpPorts -contains $_.RemotePort } - - foreach ($conn in @($connections)) { - $processId = [int]$conn.OwningProcess - $remoteAddr = [string]$conn.RemoteAddress - $remotePort = [int]$conn.RemotePort - $fingerprint = "{0}:{1}:{2}" -f $processId, $remoteAddr, $remotePort - if ($script:SeenSmtpConnections.ContainsKey($fingerprint)) { continue } - $script:SeenSmtpConnections[$fingerprint] = (Get-Date).ToUniversalTime() - - $processName = '' - try { - $proc = Get-Process -Id $processId -ErrorAction SilentlyContinue - $processName = [string]$proc.ProcessName - } - catch { } - - Send-EmailHeartbeat -SignalType 'smtp_connection' -Data @{ - remoteAddress = $remoteAddr - remotePort = $remotePort - processId = $processId - processName = $processName - localPort = [int]$conn.LocalPort - collectionMode = 'smtp' - } - Write-CollectorLog ("smtp_connection process={0}({1}) remote={2}:{3}" -f $processName, $processId, $remoteAddr, $remotePort) - - Evaluate-EmailRules ` - -Subject '' ` - -RecipientsJoined $remoteAddr ` - -SenderAddress $env:USERNAME ` - -AttachmentCount 0 ` - -AttachmentNames '' ` - -BodyLength 0 ` - -MessageId $fingerprint ` - -OutlookMailItem $null - } - - # Cleanup old SMTP connections (keep last 8h) - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenSmtpConnections.Keys)) { - if ([datetime]$script:SeenSmtpConnections[$k] -lt $cleanupBefore) { - $script:SeenSmtpConnections.Remove($k) - } - } - } - catch { - Write-CollectorLog ("SMTP poll error: {0}" -f $_.Exception.Message) - } -} - -# --------------------------------------------------------------------------- -# Initialization -# --------------------------------------------------------------------------- - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 10 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("email-outbound-{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 } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenEntryIds = @{} -$script:SeenSmtpConnections = @{} -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:OutlookApp = $null -$script:OutlookNamespace = $null -$script:SentFolder = $null -$script:OutlookLastPoll = (Get-Date).AddMinutes(-5) -$script:OutlookDisabled = $false - -Load-EmailPolicy -Path $resolvedPolicyPath -Write-CollectorLog ("email collector started mode={0} against {1}" -f $Mode, $script:ApiBase) - -$useOutlook = ($Mode -eq 'outlook' -or $Mode -eq 'both') -$useSmtp = ($Mode -eq 'smtp' -or $Mode -eq 'both') -$outlookReady = $false - -if ($useOutlook) { - $outlookReady = Initialize-OutlookCom - if (-not $outlookReady -and $Mode -eq 'outlook') { - Write-CollectorLog "Outlook COM not available, collector will retry" - } -} - -# --------------------------------------------------------------------------- -# Main loop -# --------------------------------------------------------------------------- - -while ($true) { - try { - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - if ($useOutlook) { - if (-not $outlookReady) { - $outlookReady = Initialize-OutlookCom - } - if ($outlookReady) { - try { - Poll-OutlookSentItems - } - catch { - Write-CollectorLog ("outlook poll error: {0}" -f $_.Exception.Message) - $outlookReady = $false - $script:OutlookApp = $null - $script:OutlookNamespace = $null - $script:SentFolder = $null - } - } - } - - if ($useSmtp) { - try { - Poll-SmtpConnections - } - catch { - Write-CollectorLog ("smtp poll error: {0}" -f $_.Exception.Message) - } - } - } - catch { - Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} diff --git a/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 b/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 deleted file mode 100755 index fb4b6ef..0000000 --- a/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 +++ /dev/null @@ -1,242 +0,0 @@ -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string[]]$Users, - [string]$UserListPath, - [string]$Domain, - [string]$InstallRoot, - [string]$StateRoot, - [int]$PollSeconds, - [int]$PulseSeconds, - [int]$RecoveryIntervalSeconds, - [bool]$AfkEnabled, - [bool]$WindowEnabled, - [bool]$FileOpsEnabled, - [bool]$LocalAgentLogsEnabled, - [bool]$IncidentCaptureEnabled, - [bool]$IncidentScreenshotEnabled, - [string]$IncidentArtifactsRoot, - [string]$EvtxExportRoot, - [int]$EvtxRetentionDays, - [string[]]$EvtxChannels, - [bool]$LogonMarkerEnabled, - [bool]$ProcessEventsEnabled, - [string]$AwHostname, - [string]$CustomRulesPath, - [string]$CustomPolicyPath, - [ValidateSet('local', 'server')] - [string]$PolicyMode, - [bool]$PolicyEngineEnabled, - [string]$PolicyEngineHost, - [int]$PolicyEnginePort, - [ValidateSet('http', 'https')] - [string]$PolicyEngineScheme, - [int]$PolicyRefreshSeconds, - [string]$PolicyCachePath, - [switch]$RepairPackage, - [string]$Version, - [string]$PackageUrl, - [string]$PackageZipPath -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' -Import-Module $modulePath -Force - -Assert-Administrator - -$existingConfig = $null -if (Test-Path -LiteralPath $ConfigPath) { - $existingConfig = Read-ActivityWatchDeploymentConfig -Path $ConfigPath -} - -if (-not $existingConfig -and (-not $ServerHost)) { - throw 'deployment-config.json отсутствует. Укажите -ServerHost и параметры пользователей либо сначала выполните скрипт развёртывания.' -} - -$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' } -$effectiveEvtxExportScript = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'evtxExportScript') { [string]$existingConfig.paths.evtxExportScript } else { Join-Path $effectiveStateRoot 'export-evtx-for-hayabusa.ps1' } -$effectiveHayabusaUploadScript = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'hayabusaUploadScript') { [string]$existingConfig.paths.hayabusaUploadScript } else { Join-Path $effectiveStateRoot 'export-upload-hayabusa-to-aw-server.ps1' } -$effectiveFile1CTelemetryScript = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'file1cTelemetryScript') { [string]$existingConfig.paths.file1cTelemetryScript } else { Join-Path $effectiveStateRoot 'export-upload-file-1c-telemetry.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' } -$effectivePolicyClientScript = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'policyClientScript') { [string]$existingConfig.paths.policyClientScript } else { Join-Path $effectiveStateRoot 'dlp-policy-client.ps1' } - -$effectiveServerHost = if ($ServerHost) { $ServerHost } elseif ($existingConfig) { [string]$existingConfig.server.host } else { $null } -$effectiveServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($existingConfig) { [int]$existingConfig.server.port } else { 5600 } -$effectiveServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($existingConfig) { [string]$existingConfig.server.scheme } else { 'http' } -$effectivePollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($existingConfig) { [int]$existingConfig.collector.pollSeconds } else { 5 } -$effectivePulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($existingConfig) { [int]$existingConfig.collector.pulseSeconds } else { 30 } -$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 } -$effectiveIncidentArtifactsRoot = if ($PSBoundParameters.ContainsKey('IncidentArtifactsRoot') -and $IncidentArtifactsRoot) { $IncidentArtifactsRoot } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$existingConfig.incidentCapture.artifactsRoot } else { Join-Path $effectiveStateRoot 'incident-artifacts' } -$effectiveEvtxExportRoot = if ($PSBoundParameters.ContainsKey('EvtxExportRoot') -and $EvtxExportRoot) { $EvtxExportRoot } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'evtxExportRoot') { [string]$existingConfig.forensics.evtxExportRoot } else { Join-Path $effectiveStateRoot 'forensics\evtx-exports' } -$effectiveEvtxRetentionDays = if ($PSBoundParameters.ContainsKey('EvtxRetentionDays')) { [int]$EvtxRetentionDays } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'retentionDays') { [int]$existingConfig.forensics.retentionDays } else { 14 } -$effectiveEvtxChannels = if ($PSBoundParameters.ContainsKey('EvtxChannels')) { @($EvtxChannels) } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'evtxChannels') { @($existingConfig.forensics.evtxChannels) } else { @() } -$effectiveLogonMarkerEnabled = if ($PSBoundParameters.ContainsKey('LogonMarkerEnabled')) { [bool]$LogonMarkerEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'sessionEvents' -and $existingConfig.sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$existingConfig.sessionEvents.logonEnabled } else { $true } -$effectiveProcessEventsEnabled = if ($PSBoundParameters.ContainsKey('ProcessEventsEnabled')) { [bool]$ProcessEventsEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'sessionEvents' -and $existingConfig.sessionEvents.PSObject.Properties.Name -contains 'processEventsEnabled') { [bool]$existingConfig.sessionEvents.processEventsEnabled } else { $false } -$effectiveAwHostname = if ($PSBoundParameters.ContainsKey('AwHostname') -and -not [string]::IsNullOrWhiteSpace($AwHostname)) { [string]$AwHostname } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$existingConfig.awHostname)) { [string]$existingConfig.awHostname } else { [string]$env:COMPUTERNAME } -$effectiveVersion = if ($Version) { $Version } elseif ($existingConfig) { [string]$existingConfig.package.version } else { 'v0.13.2' } -$effectivePolicyMode = if ($PSBoundParameters.ContainsKey('PolicyMode') -and $PolicyMode) { [string]$PolicyMode } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'policyEngine' -and $existingConfig.policyEngine.PSObject.Properties.Name -contains 'mode') { [string]$existingConfig.policyEngine.mode } else { 'local' } -$effectivePolicyEngineEnabled = if ($PSBoundParameters.ContainsKey('PolicyEngineEnabled')) { [bool]$PolicyEngineEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'policyEngine' -and $existingConfig.policyEngine.PSObject.Properties.Name -contains 'enabled') { [bool]$existingConfig.policyEngine.enabled } else { $false } -$effectivePolicyEngineHost = if ($PSBoundParameters.ContainsKey('PolicyEngineHost') -and -not [string]::IsNullOrWhiteSpace($PolicyEngineHost)) { [string]$PolicyEngineHost } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'policyEngine' -and $existingConfig.policyEngine.PSObject.Properties.Name -contains 'host') { [string]$existingConfig.policyEngine.host } else { [string]$effectiveServerHost } -$effectivePolicyEnginePort = if ($PSBoundParameters.ContainsKey('PolicyEnginePort')) { [int]$PolicyEnginePort } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'policyEngine' -and $existingConfig.policyEngine.PSObject.Properties.Name -contains 'port') { [int]$existingConfig.policyEngine.port } else { 5601 } -$effectivePolicyEngineScheme = if ($PSBoundParameters.ContainsKey('PolicyEngineScheme') -and $PolicyEngineScheme) { [string]$PolicyEngineScheme } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'policyEngine' -and $existingConfig.policyEngine.PSObject.Properties.Name -contains 'scheme') { [string]$existingConfig.policyEngine.scheme } else { 'http' } -$effectivePolicyRefreshSeconds = if ($PSBoundParameters.ContainsKey('PolicyRefreshSeconds')) { [int]$PolicyRefreshSeconds } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'policyEngine' -and $existingConfig.policyEngine.PSObject.Properties.Name -contains 'refreshSeconds') { [int]$existingConfig.policyEngine.refreshSeconds } else { 300 } -$effectivePolicyCachePath = if ($PSBoundParameters.ContainsKey('PolicyCachePath') -and $PolicyCachePath) { [string]$PolicyCachePath } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'policyEngine' -and $existingConfig.policyEngine.PSObject.Properties.Name -contains 'cachePath') { [string]$existingConfig.policyEngine.cachePath } else { Join-Path $effectiveStateRoot 'dlp-policy-cache.json' } -$effectiveHayabusaAutoUploadEnabled = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'hayabusaAutomation' -and $existingConfig.forensics.hayabusaAutomation.PSObject.Properties.Name -contains 'enabled') { [bool]$existingConfig.forensics.hayabusaAutomation.enabled } else { $true } -$effectiveHayabusaAutoUploadIntervalHours = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'hayabusaAutomation' -and $existingConfig.forensics.hayabusaAutomation.PSObject.Properties.Name -contains 'intervalHours') { [int]$existingConfig.forensics.hayabusaAutomation.intervalHours } else { 6 } -$effectiveHayabusaAutoUploadHoursBack = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'hayabusaAutomation' -and $existingConfig.forensics.hayabusaAutomation.PSObject.Properties.Name -contains 'hoursBack') { [int]$existingConfig.forensics.hayabusaAutomation.hoursBack } else { 6 } -$effectiveHayabusaAutoUploadMode = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'hayabusaAutomation' -and $existingConfig.forensics.hayabusaAutomation.PSObject.Properties.Name -contains 'mode') { [string]$existingConfig.forensics.hayabusaAutomation.mode } else { 'incident' } -$effectiveHayabusaAutoUploadTaskName = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'hayabusaAutomation' -and $existingConfig.forensics.hayabusaAutomation.PSObject.Properties.Name -contains 'taskName') { [string]$existingConfig.forensics.hayabusaAutomation.taskName } else { 'ActivityWatch Hayabusa Upload' } -$effectiveFile1CAutoUploadEnabled = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'enabled') { [bool]$existingConfig.analytics.file1cAutomation.enabled } else { $true } -$effectiveFile1CAutoUploadIntervalHours = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'intervalHours') { [int]$existingConfig.analytics.file1cAutomation.intervalHours } else { 6 } -$effectiveFile1CAutoUploadTaskName = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'taskName') { [string]$existingConfig.analytics.file1cAutomation.taskName } else { 'ActivityWatch File1C Upload' } -$effectiveFile1CTargetHost = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'targetHost') { [string]$existingConfig.analytics.file1cAutomation.targetHost } else { '' } -$effectiveFile1CTargetUser = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'targetUser') { [string]$existingConfig.analytics.file1cAutomation.targetUser } else { 'igor' } - -if ([string]::IsNullOrWhiteSpace($effectiveFile1CTargetHost)) { - $file1cLogPath = Join-Path $effectiveLogsRoot 'file1c-telemetry.log' - if (Test-Path -LiteralPath $file1cLogPath) { - $recoveredFile1CHost = '' - foreach ($line in Get-Content -LiteralPath $file1cLogPath -Encoding UTF8) { - if ([string]$line -match 'upload complete analyticsHost=([^\s]+)') { - $recoveredFile1CHost = [string]$Matches[1] - } - } - if (-not [string]::IsNullOrWhiteSpace($recoveredFile1CHost)) { - $effectiveFile1CTargetHost = $recoveredFile1CHost - } - } -} - -$effectiveUsers = if ($Users -or $UserListPath) { - Normalize-ActivityWatchUsers -Users $Users -UserListPath $UserListPath -Domain $Domain -} -elseif ($existingConfig) { - @($existingConfig.userTasks | ForEach-Object { [string]$_.userId }) -} -else { - throw 'Не указаны целевые пользователи.' -} - -New-ActivityWatchDirectory -Path $effectiveStateRoot -New-ActivityWatchDirectory -Path $effectiveLogsRoot -Enable-ActivityWatchPrintTelemetry - -if ($RepairPackage) { - $workingRoot = Join-Path $env:TEMP 'activitywatch-windows-deploy' - $backupRoot = Join-Path $effectiveStateRoot 'backups' - $archivePath = Get-ActivityWatchArchive -PackageZipPath $PackageZipPath -PackageUrl $PackageUrl -Version $effectiveVersion -WorkingRoot $workingRoot - Install-ActivityWatchPackage -ArchivePath $archivePath -InstallRoot $effectiveInstallRoot -WorkingRoot $workingRoot -BackupRoot $backupRoot | Out-Null -} - -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') ` - -EmailCollectorScriptSource (Join-Path $PSScriptRoot 'email-outbound-collector.ps1') ` - -FileCollectorScriptSource (Join-Path $PSScriptRoot 'file-operations-collector.ps1') ` - -SessionCollectorScriptSource (Join-Path $PSScriptRoot 'worktime-session-collector.ps1') ` - -EvtxExportScriptSource (Join-Path $PSScriptRoot 'export-evtx-for-hayabusa.ps1') ` - -HayabusaUploadScriptSource (Join-Path $PSScriptRoot 'export-upload-hayabusa-to-aw-server.ps1') ` - -File1CTelemetryScriptSource (Join-Path $PSScriptRoot 'export-upload-file-1c-telemetry.ps1') ` - -ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') ` - -ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') ` - -StateRoot $effectiveStateRoot ` - -CustomRulesSource $CustomRulesPath ` - -CustomPolicySource $CustomPolicyPath - -$taskDefinitions = New-ActivityWatchUserTaskDefinitions -Users $effectiveUsers -Write-ActivityWatchLaunchScript -Path $effectiveLaunchScript -ConfigPath $effectiveConfigPath -Write-ActivityWatchRecoveryScript -Path $effectiveRecoveryScript -ConfigPath $effectiveConfigPath - -$config = New-ActivityWatchDeploymentConfig ` - -ServerHost $effectiveServerHost ` - -ServerPort $effectiveServerPort ` - -ServerScheme $effectiveServerScheme ` - -InstallRoot $effectiveInstallRoot ` - -StateRoot $effectiveStateRoot ` - -LogsRoot $effectiveLogsRoot ` - -CollectorScript $effectiveCollector ` - -EndpointCollectorScript $effectiveEndpointCollector ` - -PolicyClientScript $effectivePolicyClientScript ` - -EmailCollectorScript $assetResult.EmailCollectorScript ` - -FileCollectorScript $effectiveFileCollector ` - -SessionCollectorScript $effectiveSessionCollector ` - -EvtxExportScript $effectiveEvtxExportScript ` - -HayabusaUploadScript $effectiveHayabusaUploadScript ` - -File1CTelemetryScript $effectiveFile1CTelemetryScript ` - -RulesPath $effectiveRules ` - -PolicyPath $effectivePolicy ` - -PollSeconds $effectivePollSeconds ` - -PulseSeconds $effectivePulseSeconds ` - -RecoveryIntervalSeconds $effectiveRecoveryInterval ` - -AfkEnabled $effectiveAfkEnabled ` - -WindowEnabled $effectiveWindowEnabled ` - -FileOpsEnabled $effectiveFileOpsEnabled ` - -LocalAgentLogsEnabled $effectiveLocalAgentLogsEnabled ` - -IncidentCaptureEnabled $effectiveIncidentCaptureEnabled ` - -IncidentScreenshotEnabled $effectiveIncidentScreenshotEnabled ` - -IncidentArtifactsRoot $effectiveIncidentArtifactsRoot ` - -EvtxExportRoot $effectiveEvtxExportRoot ` - -EvtxRetentionDays $effectiveEvtxRetentionDays ` - -EvtxChannels $effectiveEvtxChannels ` - -LogonMarkerEnabled $effectiveLogonMarkerEnabled ` - -ProcessEventsEnabled $effectiveProcessEventsEnabled ` - -AwHostname $effectiveAwHostname ` - -PolicyMode $effectivePolicyMode ` - -PolicyEngineEnabled $effectivePolicyEngineEnabled ` - -PolicyEngineHost $effectivePolicyEngineHost ` - -PolicyEnginePort $effectivePolicyEnginePort ` - -PolicyEngineScheme $effectivePolicyEngineScheme ` - -PolicyRefreshSeconds $effectivePolicyRefreshSeconds ` - -PolicyCachePath $effectivePolicyCachePath ` - -HayabusaAutoUploadEnabled $effectiveHayabusaAutoUploadEnabled ` - -HayabusaAutoUploadIntervalHours $effectiveHayabusaAutoUploadIntervalHours ` - -HayabusaAutoUploadHoursBack $effectiveHayabusaAutoUploadHoursBack ` - -HayabusaAutoUploadMode $effectiveHayabusaAutoUploadMode ` - -HayabusaAutoUploadTaskName $effectiveHayabusaAutoUploadTaskName ` - -File1CAutoUploadEnabled $effectiveFile1CAutoUploadEnabled ` - -File1CAutoUploadIntervalHours $effectiveFile1CAutoUploadIntervalHours ` - -File1CAutoUploadTaskName $effectiveFile1CAutoUploadTaskName ` - -File1CTargetHost $effectiveFile1CTargetHost ` - -File1CTargetUser $effectiveFile1CTargetUser ` - -LaunchScriptPath $effectiveLaunchScript ` - -RecoveryScriptPath $effectiveRecoveryScript ` - -UserTasks $taskDefinitions ` - -PackageVersion $effectiveVersion - -Write-ActivityWatchDeploymentConfig -Config $config -Path $effectiveConfigPath -Remove-LegacyActivityWatchEntries -Set-ActivityWatchAcl -InstallRoot $effectiveInstallRoot -StateRoot $effectiveStateRoot -LogsRoot $effectiveLogsRoot -Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $effectiveLaunchScript -ConfigPath $effectiveConfigPath -Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath -Register-ActivityWatchHayabusaAutoUploadTask -ConfigPath $effectiveConfigPath -Register-ActivityWatchFile1CAutoUploadTask -ConfigPath $effectiveConfigPath -Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName - -Write-Host 'Укрепление и восстановление ActivityWatch завершены.' -Write-Host "Конфигурация: $effectiveConfigPath" -Write-Host "Пользователи восстановлены: $($effectiveUsers -join ', ')" diff --git a/install-kit-awindows-20260427-211240/windows/install-collector-guard-service.ps1 b/install-kit-awindows-20260427-211240/windows/install-collector-guard-service.ps1 deleted file mode 100644 index a6e270c..0000000 --- a/install-kit-awindows-20260427-211240/windows/install-collector-guard-service.ps1 +++ /dev/null @@ -1,88 +0,0 @@ -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [ValidateSet('shadow', 'enforce')] - [string]$Mode = 'shadow', - [string]$ServiceName = 'AWatchRusCollectorGuard', - [int]$LoopSeconds = 60, - [switch]$DisableRecoveryTask -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Assert-Admin { - $id = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($id) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Run as Administrator.' - } -} - -Assert-Admin - -$guardScriptPath = Join-Path $PSScriptRoot 'aw-collector-guard.ps1' -$serviceSourcePath = Join-Path $PSScriptRoot 'AWatchRusCollectorGuardService.cs' -$serviceExePath = Join-Path $PSScriptRoot 'AWatchRusCollectorGuardService.exe' -if (-not (Test-Path -LiteralPath $guardScriptPath)) { - throw "Collector guard script not found: $guardScriptPath" -} -if (-not (Test-Path -LiteralPath $serviceSourcePath)) { - throw "Collector guard service source not found: $serviceSourcePath" -} -if (-not (Test-Path -LiteralPath $ConfigPath)) { - throw "Config not found: $ConfigPath" -} - -$cscCandidates = @( - (Join-Path $env:WINDIR 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'), - (Join-Path $env:WINDIR 'Microsoft.NET\Framework\v4.0.30319\csc.exe') -) -$csc = @($cscCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1) -if (-not $csc) { - throw 'C# compiler not found. Install .NET Framework build tools or provide AWatchRusCollectorGuardService.exe.' -} - -$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue -if ($existing) { - if ($existing.Status -ne 'Stopped') { - Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue - try { - $existing.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(20)) - } - catch { - } - } - sc.exe delete $ServiceName | Out-Null - Start-Sleep -Seconds 2 -} - -& $csc /nologo /target:exe /optimize+ /out:$serviceExePath /reference:System.ServiceProcess.dll $serviceSourcePath | Out-Null -if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $serviceExePath)) { - throw "Failed to compile $serviceExePath" -} - -$logsRoot = Join-Path (Split-Path -Path $ConfigPath -Parent) 'logs' -$serviceLogPath = Join-Path $logsRoot 'collector-guard-service.log' -$binPath = "`"$serviceExePath`" --service-name `"$ServiceName`" --script `"$guardScriptPath`" --config `"$ConfigPath`" --mode $Mode --loop $LoopSeconds --log `"$serviceLogPath`"" - -New-Service -Name $ServiceName -BinaryPathName $binPath -DisplayName 'AWatch-rus Collector Guard' -StartupType Automatic | Out-Null -sc.exe description $ServiceName "Session-aware ActivityWatch collector guard for AWatch-rus" | Out-Null -sc.exe failure $ServiceName reset= 300 actions= restart/5000/restart/15000/restart/60000 | Out-Null - -if ($DisableRecoveryTask) { - Write-Warning 'DisableRecoveryTask is deprecated and ignored: ActivityWatch Recovery must remain enabled as collector guard fallback.' -} -else { - try { - Enable-ScheduledTask -TaskName 'ActivityWatch Recovery' -ErrorAction SilentlyContinue | Out-Null - } - catch { - } -} - -sc.exe start $ServiceName | Out-Null - -Write-Output "Collector guard service installed: $ServiceName" -Write-Output "Mode: $Mode" -Write-Output "Config: $ConfigPath" diff --git a/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 b/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 deleted file mode 100644 index c588050..0000000 --- a/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 +++ /dev/null @@ -1,243 +0,0 @@ -[CmdletBinding(SupportsShouldProcess = $true)] -param( - [string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2', - [string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2', - [string]$NewInstallRoot = 'C:\Program Files\AWatch-rus\bin', - [string]$NewStateRoot = 'C:\ProgramData\AWatch-rus', - [string]$ToolkitRoot = 'C:\Program Files\AWatch-rus\windows', - [switch]$SkipValidation -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' -Import-Module $modulePath -Force - -Assert-Administrator - -function Copy-DirectoryContents { - param( - [Parameter(Mandatory = $true)] - [string]$Source, - [Parameter(Mandatory = $true)] - [string]$Destination - ) - - if (-not (Test-Path -LiteralPath $Source)) { - return - } - - New-ActivityWatchDirectory -Path $Destination - Copy-Item -Path (Join-Path $Source '*') -Destination $Destination -Recurse -Force -} - -function Copy-IfExists { - param( - [Parameter(Mandatory = $true)] - [string]$Source, - [Parameter(Mandatory = $true)] - [string]$Destination - ) - - if (Test-Path -LiteralPath $Source) { - Copy-Item -LiteralPath $Source -Destination $Destination -Force - } -} - -function Convert-PathValue { - param( - [AllowNull()] - [string]$Value - ) - - if ([string]::IsNullOrWhiteSpace($Value)) { - return $Value - } - - return $Value.Replace($OldInstallRoot, $NewInstallRoot).Replace($OldStateRoot, $NewStateRoot) -} - -function Stop-AWatchTaskSet { - foreach ($task in @(Get-ScheduledTask | Where-Object { $_.TaskName -eq 'ActivityWatch Recovery' -or $_.TaskName -like 'ActivityWatch Launch *' })) { - Stop-ScheduledTask -TaskName $task.TaskName -ErrorAction SilentlyContinue - } -} - -function Get-ExistingAWatchConfig { - $newConfigPath = Join-Path $NewStateRoot 'deployment-config.json' - $oldConfigPath = Join-Path $OldStateRoot 'deployment-config.json' - - if (Test-Path -LiteralPath $oldConfigPath) { - return [pscustomobject]@{ - Path = $oldConfigPath - Config = Read-ActivityWatchDeploymentConfig -Path $oldConfigPath - } - } - - if (Test-Path -LiteralPath $newConfigPath) { - return [pscustomobject]@{ - Path = $newConfigPath - Config = Read-ActivityWatchDeploymentConfig -Path $newConfigPath - } - } - - throw "Не найден deployment-config.json ни в $OldStateRoot, ни в $NewStateRoot." -} - -function Update-AWatchConfigPaths { - param( - [Parameter(Mandatory = $true)] - [pscustomobject]$Config - ) - - $logsRoot = Join-Path $NewStateRoot 'logs' - $Config.paths.installRoot = $NewInstallRoot - $Config.paths.stateRoot = $NewStateRoot - $Config.paths.logsRoot = $logsRoot - $Config.paths.collectorScript = Join-Path $NewStateRoot 'browser-domains-native-collector.ps1' - $Config.paths.endpointCollectorScript = Join-Path $NewStateRoot 'dlp-endpoint-signals-collector.ps1' - if ($Config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { - $Config.paths.sessionCollectorScript = Join-Path $NewStateRoot 'worktime-session-collector.ps1' - } - $Config.paths.rulesPath = Join-Path $NewStateRoot 'web-category-rules.json' - if ($Config.paths.PSObject.Properties.Name -contains 'policyPath') { - $Config.paths.policyPath = Join-Path $NewStateRoot 'dlp-policy.json' - } - $Config.paths.launchScript = Join-Path $NewStateRoot 'launch-watchers.ps1' - $Config.paths.recoveryScript = Join-Path $NewStateRoot 'recovery-loop.ps1' - - if ($Config.PSObject.Properties.Name -contains 'incidentCapture' -and $Config.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { - $Config.incidentCapture.artifactsRoot = Convert-PathValue -Value ([string]$Config.incidentCapture.artifactsRoot) - } - - return $Config -} - -$existing = Get-ExistingAWatchConfig -$backupRoot = Join-Path $NewStateRoot ('migration-backups\' + (Get-Date -Format 'yyyyMMdd-HHmmss')) -$newConfigPath = Join-Path $NewStateRoot 'deployment-config.json' -$newLogsRoot = Join-Path $NewStateRoot 'logs' - -$summary = [ordered]@{ - sourceConfig = $existing.Path - oldInstallRoot = $OldInstallRoot - oldStateRoot = $OldStateRoot - newInstallRoot = $NewInstallRoot - newStateRoot = $NewStateRoot - backupRoot = $backupRoot - actions = @( - 'stop ActivityWatch scheduled tasks', - 'backup old/new install and state directories', - 'copy old install/state contents to AWatch-rus paths', - 'rewrite deployment-config.json paths', - 'regenerate launcher/recovery scripts', - 're-register scheduled tasks', - 'run validate-deployment.ps1' - ) -} - -if ($WhatIfPreference) { - return [pscustomobject]$summary -} - -if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Миграция ActivityWatch Windows/RDP путей в AWatch-rus')) { - New-ActivityWatchDirectory -Path $NewStateRoot - New-ActivityWatchDirectory -Path $backupRoot - - Stop-AWatchTaskSet - - foreach ($item in @( - @{ Source = $OldInstallRoot; Name = 'old-install' }, - @{ Source = $OldStateRoot; Name = 'old-state' }, - @{ Source = $NewInstallRoot; Name = 'new-install' }, - @{ Source = $NewStateRoot; Name = 'new-state' } - )) { - if (Test-Path -LiteralPath $item.Source) { - $backupDest = Join-Path $backupRoot $item.Name - New-ActivityWatchDirectory -Path $backupDest - - $excludeDirs = @() - if ($item.Source -eq $NewStateRoot) { - # Avoid infinite recursion: backupRoot is inside NewStateRoot by default. - $excludeDirs += $backupRoot - } - - $robocopyArgs = @( - $item.Source, - $backupDest, - '/E', - '/R:1', - '/W:1', - '/NFL', - '/NDL', - '/NJH', - '/NJS', - '/NP' - ) - if ($excludeDirs.Count -gt 0) { - $robocopyArgs += '/XD' - $robocopyArgs += $excludeDirs - } - - & robocopy @robocopyArgs | Out-Null - if ($LASTEXITCODE -ge 8) { - throw "Backup robocopy failed (exit=$LASTEXITCODE) for source '$($item.Source)' to '$backupDest'" - } - } - } - - Copy-DirectoryContents -Source $OldInstallRoot -Destination $NewInstallRoot - Copy-DirectoryContents -Source $OldStateRoot -Destination $NewStateRoot - New-ActivityWatchDirectory -Path $newLogsRoot - - foreach ($file in @( - 'browser-domains-native-collector.ps1', - 'dlp-endpoint-signals-collector.ps1', - 'worktime-session-collector.ps1', - 'web-category-rules.example.json', - 'dlp-policy.example.json' - )) { - Copy-IfExists -Source (Join-Path $ToolkitRoot $file) -Destination (Join-Path $NewStateRoot $file) - } - - Copy-IfExists -Source (Join-Path $OldStateRoot 'web-category-rules.json') -Destination (Join-Path $NewStateRoot 'web-category-rules.json') - Copy-IfExists -Source (Join-Path $OldStateRoot 'dlp-policy.json') -Destination (Join-Path $NewStateRoot 'dlp-policy.json') - if (-not (Test-Path -LiteralPath (Join-Path $NewStateRoot 'web-category-rules.json'))) { - Copy-IfExists -Source (Join-Path $NewStateRoot 'web-category-rules.example.json') -Destination (Join-Path $NewStateRoot 'web-category-rules.json') - } - if (-not (Test-Path -LiteralPath (Join-Path $NewStateRoot 'dlp-policy.json'))) { - Copy-IfExists -Source (Join-Path $NewStateRoot 'dlp-policy.example.json') -Destination (Join-Path $NewStateRoot 'dlp-policy.json') - } - - $config = Update-AWatchConfigPaths -Config $existing.Config - Write-ActivityWatchDeploymentConfig -Config $config -Path $newConfigPath - Write-ActivityWatchLaunchScript -Path $config.paths.launchScript -ConfigPath $newConfigPath - Write-ActivityWatchRecoveryScript -Path $config.paths.recoveryScript -ConfigPath $newConfigPath - - $taskDefinitions = @($config.userTasks) - Set-ActivityWatchAcl -InstallRoot $NewInstallRoot -StateRoot $NewStateRoot -LogsRoot $newLogsRoot - Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $config.paths.launchScript -ConfigPath $newConfigPath - Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $config.paths.recoveryScript -ConfigPath $newConfigPath - Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName - Start-Sleep -Seconds 5 - - if (-not $SkipValidation) { - $validateScript = Join-Path $ToolkitRoot 'validate-deployment.ps1' - if (-not (Test-Path -LiteralPath $validateScript)) { - $validateScript = Join-Path $PSScriptRoot 'validate-deployment.ps1' - } - $report = & $validateScript -ConfigPath $newConfigPath - if (-not [bool]$report.overallOk) { - throw "Миграция выполнена, но validation завершился ошибкой. Backup: $backupRoot" - } - } - - [pscustomobject]@{ - migrated = $true - backupRoot = $backupRoot - configPath = $newConfigPath - installRoot = $NewInstallRoot - stateRoot = $NewStateRoot - } -} diff --git a/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 b/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 deleted file mode 100644 index bdfcb61..0000000 --- a/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 +++ /dev/null @@ -1,677 +0,0 @@ -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' -Import-Module $modulePath -Force - -$config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath -$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' } -$evtxExportScript = if ($config.paths.PSObject.Properties.Name -contains 'evtxExportScript') { [string]$config.paths.evtxExportScript } else { Join-Path $stateRoot 'export-evtx-for-hayabusa.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' } -$policyClientScript = if ($config.paths.PSObject.Properties.Name -contains 'policyClientScript') { [string]$config.paths.policyClientScript } else { Join-Path $stateRoot 'dlp-policy-client.ps1' } -$launchScript = [string]$config.paths.launchScript -$recoveryScript = [string]$config.paths.recoveryScript -$awHostname = if ($config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$config.awHostname)) { [string]$config.awHostname } else { [string]$env:COMPUTERNAME } -$serverUrl = '{0}://{1}:{2}' -f [string]$config.server.scheme, [string]$config.server.host, [int]$config.server.port -$apiBase = "$serverUrl/api/0" -$pollSeconds = if ($config.PSObject.Properties.Name -contains 'collector' -and $config.collector.PSObject.Properties.Name -contains 'pollSeconds') { [int]$config.collector.pollSeconds } else { 5 } -$pulseSeconds = if ($config.PSObject.Properties.Name -contains 'collector' -and $config.collector.PSObject.Properties.Name -contains 'pulseSeconds') { [int]$config.collector.pulseSeconds } else { [Math]::Max($pollSeconds * 3, 30) } -$freshnessSeconds = [Math]::Max($pollSeconds * 3, 30) -$sessionFreshnessSeconds = [Math]::Max($pollSeconds * 4, 45) -$transportStaleSeconds = [Math]::Max($pollSeconds * 12, 180) -$endpointFreshnessSeconds = [Math]::Max($transportStaleSeconds, 300) -$queueMaxDepth = 1000 - -$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 } -$sessionEventsConfig = if ($config.PSObject.Properties.Name -contains 'sessionEvents') { $config.sessionEvents } else { $null } -$sessionLogonEnabled = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$sessionEventsConfig.logonEnabled } else { $false } -$sessionProcessEventsEnabled = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'processEventsEnabled') { [bool]$sessionEventsConfig.processEventsEnabled } else { $false } -$sessionEventsBucketId = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]$sessionEventsConfig.bucketPrefix)) { - ('{0}_{1}' -f [string]$sessionEventsConfig.bucketPrefix, $awHostname) -} -else { - 'aw-session-events_' + $awHostname -} - -function Get-LoggedOnUsers { - param( - [bool]$IncludeDisconnected = $false - ) - - $users = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - $activeStates = @('Active', 'Активно') - $inactiveStates = @('Disc', 'Disconnected', 'Idle', 'Listen', 'Диск', 'Откл', 'Отключен') - try { - $lines = & quser.exe 2>$null - foreach ($line in @($lines)) { - $normalized = [string]$line - if ([string]::IsNullOrWhiteSpace($normalized)) { continue } - $normalized = $normalized.TrimStart(' ', '>') - if ([string]::IsNullOrWhiteSpace($normalized)) { continue } - if ($normalized -match '^(USERNAME|ПОЛЬЗОВАТЕЛЬ)\s+') { continue } - $parts = $normalized -split '\s+' - if ($parts.Count -lt 1) { continue } - $user = [string]$parts[0] - if ([string]::IsNullOrWhiteSpace($user)) { continue } - $state = $null - foreach ($part in @($parts | Select-Object -Skip 1)) { - $token = [string]$part - if ([string]::IsNullOrWhiteSpace($token)) { continue } - if ($activeStates -contains $token -or $inactiveStates -contains $token) { - $state = $token - break - } - } - if ($null -ne $state -and $activeStates -notcontains $state) { - if (-not $IncludeDisconnected -or $inactiveStates -notcontains $state) { - continue - } - } - [void]$users.Add($user) - [void]$users.Add(('{0}\{1}' -f $env:COMPUTERNAME, $user)) - if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) { - [void]$users.Add(('{0}\{1}' -f $env:USERDOMAIN, $user)) - } - } - } - catch { - } - return @($users) -} - -function Test-UserHasSession { - param( - [string]$UserId, - [string[]]$LoggedOnUsers - ) - - if ([string]::IsNullOrWhiteSpace($UserId)) { return $false } - $candidateIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) - [void]$candidateIds.Add($UserId) - $leafUser = $UserId - if ($leafUser -match '^[^\\]+\\(.+)$') { - $leafUser = $Matches[1] - [void]$candidateIds.Add($leafUser) - } - [void]$candidateIds.Add(('{0}\{1}' -f $env:COMPUTERNAME, $leafUser)) - if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) { - [void]$candidateIds.Add(('{0}\{1}' -f $env:USERDOMAIN, $leafUser)) - } - foreach ($candidate in @($candidateIds)) { - if ($LoggedOnUsers -contains $candidate) { return $true } - } - return $false -} - -function Get-CollectorProcesses { - param( - [Parameter(Mandatory = $true)] - [string]$ScriptPath - ) - - return @( - Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - ($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and - $_.CommandLine -and - $_.CommandLine -match [Regex]::Escape($ScriptPath) - } | - Select-Object @{ Name = 'Name'; Expression = { $_.Name } }, @{ Name = 'Id'; Expression = { [int]$_.ProcessId } }, @{ Name = 'SessionId'; Expression = { [int]$_.SessionId } }, @{ Name = 'CommandLine'; Expression = { [string]$_.CommandLine } } - ) -} - -function Get-DuplicateProcessGroups { - param( - [object[]]$Processes, - [bool]$PerSession = $true - ) - - if (-not $Processes -or @($Processes).Count -eq 0) { return @() } - $groups = if ($PerSession) { - $Processes | Group-Object -Property Name, SessionId - } - else { - $Processes | Group-Object -Property Name - } - - return @( - $groups | - Where-Object { $_.Count -gt 1 } | - ForEach-Object { - [pscustomobject]@{ - name = [string]$_.Name - count = [int]$_.Count - members = @($_.Group | Select-Object Name, Id, SessionId, CommandLine) - } - } - ) -} - -function Convert-ToUtcDate { - param($Value) - - if ($null -eq $Value) { return $null } - try { - return ([DateTimeOffset]::Parse([string]$Value)).UtcDateTime - } - catch { - return $null - } -} - -function Get-BucketHealth { - param( - [Parameter(Mandatory = $true)] - [string]$BucketId, - [Parameter(Mandatory = $true)] - [int]$MaxAgeSeconds, - [bool]$Required = $true, - [bool]$RequireFreshEvent = $true - ) - - $events = @() - $queryOk = $false - $errorMessage = $null - try { - $response = Invoke-RestMethod -Method Get -Uri "$apiBase/buckets/$BucketId/events?limit=25" -TimeoutSec 15 -DisableKeepAlive -ErrorAction Stop - $events = @($response) - $queryOk = $true - } - catch { - $errorMessage = $_.Exception.Message - } - - $latestTimestampUtc = $null - $ageSeconds = $null - if ($events.Count -gt 0) { - $latestTimestampUtc = @( - $events | - ForEach-Object { Convert-ToUtcDate $_.timestamp } | - Where-Object { $null -ne $_ } | - Sort-Object -Descending - ) | Select-Object -First 1 - if ($null -ne $latestTimestampUtc) { - $ageSeconds = [int][Math]::Floor(((Get-Date).ToUniversalTime() - $latestTimestampUtc).TotalSeconds) - } - } - - $hasFreshEvent = ($null -ne $ageSeconds -and $ageSeconds -le $MaxAgeSeconds) - $hasAnyEvent = ($events.Count -gt 0) - $ok = if (-not $Required) { $true } elseif ($RequireFreshEvent) { $queryOk -and $hasFreshEvent } else { $queryOk -and $hasAnyEvent } - - return [pscustomobject]@{ - bucketId = $BucketId - required = [bool]$Required - requireFreshEvent = [bool]$RequireFreshEvent - maxAgeSeconds = [int]$MaxAgeSeconds - queryOk = [bool]$queryOk - latestTimestampUtc = if ($null -ne $latestTimestampUtc) { $latestTimestampUtc.ToString('o') } else { $null } - ageSeconds = if ($null -ne $ageSeconds) { [int]$ageSeconds } else { $null } - count = [int]$events.Count - ok = [bool]$ok - error = $errorMessage - } -} - -function Get-TransportQueueHealth { - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(Mandatory = $true)] - [string]$QueuePath, - [Parameter(Mandatory = $true)] - [string]$LockPath, - [Parameter(Mandatory = $true)] - [int]$StaleAfterSeconds, - [Parameter(Mandatory = $true)] - [int]$MaxDepth, - [int]$ActiveProcessCount = 0, - [bool]$Required = $true - ) - - $queueExists = Test-Path -LiteralPath $QueuePath - $depth = 0 - $sizeBytes = 0 - $ageSeconds = $null - $lastWriteUtc = $null - if ($queueExists) { - $item = Get-Item -LiteralPath $QueuePath -ErrorAction SilentlyContinue - if ($item) { - $sizeBytes = [int64]$item.Length - $lastWriteUtc = $item.LastWriteTimeUtc - $ageSeconds = [int][Math]::Floor(((Get-Date).ToUniversalTime() - $lastWriteUtc).TotalSeconds) - } - try { - $depth = [int]((Get-Content -LiteralPath $QueuePath -ErrorAction SilentlyContinue | Measure-Object).Count) - } - catch { - $depth = 0 - } - } - - $lockExists = Test-Path -LiteralPath $LockPath - $lockHeld = $false - if ($lockExists) { - try { - $lockHandle = [System.IO.File]::Open($LockPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) - $lockHandle.Dispose() - } - catch { - $lockHeld = $true - } - } - - $staleQueue = ($depth -gt 0 -and $null -ne $ageSeconds -and $ageSeconds -gt $StaleAfterSeconds -and -not $lockHeld) - $orphanedQueue = ($depth -gt 0 -and $ActiveProcessCount -le 0 -and $null -ne $ageSeconds -and $ageSeconds -gt $StaleAfterSeconds) - $oversizedQueue = ($depth -gt $MaxDepth) - $ok = if (-not $Required) { $true } else { -not ($staleQueue -or $orphanedQueue -or $oversizedQueue) } - - return [pscustomobject]@{ - name = $Name - required = [bool]$Required - queuePath = $QueuePath - queueExists = [bool]$queueExists - depth = [int]$depth - sizeBytes = [int64]$sizeBytes - lastWriteUtc = if ($null -ne $lastWriteUtc) { $lastWriteUtc.ToString('o') } else { $null } - ageSeconds = if ($null -ne $ageSeconds) { [int]$ageSeconds } else { $null } - lockPath = $LockPath - lockExists = [bool]$lockExists - lockHeld = [bool]$lockHeld - activeProcessCount = [int]$ActiveProcessCount - staleAfterSeconds = [int]$StaleAfterSeconds - maxDepth = [int]$MaxDepth - staleQueue = [bool]$staleQueue - orphanedQueue = [bool]$orphanedQueue - oversizedQueue = [bool]$oversizedQueue - ok = [bool]$ok - } -} - -function Get-TransportQueueGroupHealth { - param( - [Parameter(Mandatory = $true)] - [string]$Name, - [Parameter(Mandatory = $true)] - [string]$StateRoot, - [Parameter(Mandatory = $true)] - [string]$QueuePattern, - [Parameter(Mandatory = $true)] - [int]$StaleAfterSeconds, - [Parameter(Mandatory = $true)] - [int]$MaxDepth, - [int]$ActiveProcessCount = 0, - [bool]$Required = $true - ) - - $queues = @(Get-ChildItem -LiteralPath $StateRoot -Filter $QueuePattern -ErrorAction SilentlyContinue | Sort-Object Name) - if ($queues.Count -eq 0) { - return [pscustomobject]@{ - name = $Name - required = [bool]$Required - queuePattern = $QueuePattern - queueCount = 0 - queues = @() - depth = 0 - sizeBytes = 0 - activeProcessCount = [int]$ActiveProcessCount - staleAfterSeconds = [int]$StaleAfterSeconds - maxDepth = [int]$MaxDepth - ok = [bool](-not $Required) - } - } - - $items = @() - foreach ($queue in $queues) { - $lockPath = [System.IO.Path]::ChangeExtension($queue.FullName, '.lock') - $items += Get-TransportQueueHealth -Name $queue.BaseName -QueuePath $queue.FullName -LockPath $lockPath -StaleAfterSeconds $StaleAfterSeconds -MaxDepth $MaxDepth -ActiveProcessCount $ActiveProcessCount -Required $Required - } - - return [pscustomobject]@{ - name = $Name - required = [bool]$Required - queuePattern = $QueuePattern - queueCount = [int]$items.Count - queues = @($items) - depth = [int](($items | Measure-Object -Property depth -Sum).Sum) - sizeBytes = [int64](($items | Measure-Object -Property sizeBytes -Sum).Sum) - activeProcessCount = [int]$ActiveProcessCount - staleAfterSeconds = [int]$StaleAfterSeconds - maxDepth = [int]$MaxDepth - ok = [bool](-not ($items | Where-Object { -not $_.ok })) - } -} - -function Resolve-ActivityWatchLaunchTaskName { - param( - [Parameter(Mandatory = $true)] - [string]$TaskName - ) - - if ($TaskName -notmatch '\[[^\]]+_Administrator\]') { - return $TaskName - } - - $localizedCandidate = 'ActivityWatch Launch [{0}_Администратор]' -f $awHostname - $localizedTask = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $localizedCandidate } | Select-Object -First 1 - if ($localizedTask) { - return $localizedCandidate - } - - try { - $builtinAdmin = Get-LocalUser -ErrorAction Stop | - Where-Object { [string]$_.SID -match '-500$' } | - Select-Object -First 1 - if ($builtinAdmin -and -not [string]::IsNullOrWhiteSpace([string]$builtinAdmin.Name)) { - $candidate = 'ActivityWatch Launch [{0}_{1}]' -f $awHostname, [string]$builtinAdmin.Name - $existing = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $candidate } | Select-Object -First 1 - if ($existing) { - return $candidate - } - } - } - catch { - } - - return $TaskName -} - -function Get-TaskSnapshot { - param( - [Parameter(Mandatory = $true)] - [string[]]$TaskNames - ) - - return @( - foreach ($taskName in @($TaskNames | Sort-Object -Unique)) { - $task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1 - if ($null -eq $task) { - [pscustomobject]@{ - taskName = $taskName - present = $false - enabled = $false - state = 'Отсутствует' - lastResult = $null - ok = $false - } - continue - } - - $taskInfo = $null - try { - $taskInfo = Get-ScheduledTaskInfo -TaskName $task.TaskName -TaskPath $task.TaskPath -ErrorAction Stop - } - catch { - } - - $enabled = $true - try { - if ($task.Settings.PSObject.Properties.Name -contains 'Enabled') { - $enabled = [bool]$task.Settings.Enabled - } - } - catch { - } - [pscustomobject]@{ - taskName = [string]$task.TaskName - present = $true - enabled = [bool]$enabled - state = [string]$task.State - lastResult = if ($taskInfo) { [int64]$taskInfo.LastTaskResult } else { $null } - ok = [bool]$enabled - } - } - ) -} - -$requiredFiles = @( - $collectorScript, - $endpointCollectorScript, - $sessionCollectorScript, - $evtxExportScript, - $rulesPath, - $policyPath, - $policyClientScript, - $launchScript, - $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 [string]::IsNullOrWhiteSpace([string]$_) } | - Where-Object { -not (Test-Path -LiteralPath $_) } -) - -$runningWatchers = @() -$expectedWatcherNames = @() -if ($afkExpected) { $expectedWatcherNames += 'aw-watcher-afk' } -if ($windowExpected) { $expectedWatcherNames += 'aw-watcher-window' } -if ($expectedWatcherNames.Count -gt 0) { - $runningWatchers = @(Get-Process -Name $expectedWatcherNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId) -} - -$sessionCollectorProcesses = @(Get-CollectorProcesses -ScriptPath $sessionCollectorScript) -$endpointCollectorProcesses = @(Get-CollectorProcesses -ScriptPath $endpointCollectorScript) -$fileCollectorProcesses = if ($fileOpsExpected) { @(Get-CollectorProcesses -ScriptPath $fileCollectorScript) } else { @() } -$browserCollectorProcesses = @(Get-CollectorProcesses -ScriptPath $collectorScript) - -$liveLoggedOnUsers = Get-LoggedOnUsers -$interactiveUsers = Get-LoggedOnUsers -IncludeDisconnected $true -$liveSessionBoundUsers = @( - @($config.userTasks) | - Where-Object { Test-UserHasSession -UserId ([string]$_.userId) -LoggedOnUsers $liveLoggedOnUsers } | - ForEach-Object { [string]$_.userId } -) -$interactiveSessionBoundUsers = @( - @($config.userTasks) | - Where-Object { Test-UserHasSession -UserId ([string]$_.userId) -LoggedOnUsers $interactiveUsers } | - ForEach-Object { [string]$_.userId } -) -$sessionScopedExpectedCount = if ($liveSessionBoundUsers.Count -gt 0) { - [int]$liveSessionBoundUsers.Count -} -elseif ($interactiveSessionBoundUsers.Count -gt 0) { - 1 -} -else { - 0 -} -$sessionScopedCollectorsRequired = ($sessionScopedExpectedCount -gt 0) -$liveSessionScopedCollectorsRequired = ($liveSessionBoundUsers.Count -gt 0) - -$collectorGuardService = Get-Service -Name 'AWatchRusCollectorGuard' -ErrorAction SilentlyContinue -$collectorGuardActive = [bool]($collectorGuardService -and $collectorGuardService.Status -eq 'Running') - -$taskNames = @() -if ($config.userTasks) { - $taskNames += @($config.userTasks | ForEach-Object { Resolve-ActivityWatchLaunchTaskName -TaskName ([string]$_.launchTaskName) }) -} -$taskNames += [string]$config.recovery.taskName -$tasks = @(Get-TaskSnapshot -TaskNames $taskNames) - -$watcherDuplicates = @(Get-DuplicateProcessGroups -Processes $runningWatchers -PerSession $true) -$sessionCollectorDuplicates = @(Get-DuplicateProcessGroups -Processes $sessionCollectorProcesses -PerSession $false) -$endpointCollectorDuplicates = @(Get-DuplicateProcessGroups -Processes $endpointCollectorProcesses -PerSession $true) -$fileCollectorDuplicates = @(Get-DuplicateProcessGroups -Processes $fileCollectorProcesses -PerSession $true) -$browserCollectorDuplicates = @(Get-DuplicateProcessGroups -Processes $browserCollectorProcesses -PerSession $true) - -$watcherByName = @{} -foreach ($watcher in $runningWatchers) { - if (-not $watcherByName.ContainsKey([string]$watcher.Name)) { - $watcherByName[[string]$watcher.Name] = 0 - } - $watcherByName[[string]$watcher.Name]++ -} - -$bucketChecks = @( - Get-BucketHealth -BucketId ('aw-worktime-sessions_' + $awHostname) -MaxAgeSeconds $sessionFreshnessSeconds -Required $true -RequireFreshEvent $true -) -if ($liveSessionScopedCollectorsRequired -and $afkExpected) { - $bucketChecks += Get-BucketHealth -BucketId ('aw-watcher-afk_' + $awHostname) -MaxAgeSeconds $freshnessSeconds -Required $true -RequireFreshEvent $false -} -if ($liveSessionScopedCollectorsRequired -and $windowExpected) { - $bucketChecks += Get-BucketHealth -BucketId ('aw-watcher-window_' + $awHostname) -MaxAgeSeconds $freshnessSeconds -Required $true -RequireFreshEvent $false -} -if ($liveSessionScopedCollectorsRequired) { - $bucketChecks += Get-BucketHealth -BucketId ('aw-dlp-endpoint-signals_' + $awHostname) -MaxAgeSeconds $endpointFreshnessSeconds -Required $true -RequireFreshEvent $true -} -if ($liveSessionScopedCollectorsRequired -and $fileOpsExpected) { - $bucketChecks += Get-BucketHealth -BucketId ('aw-file-operations_' + $awHostname) -MaxAgeSeconds $transportStaleSeconds -Required $false -RequireFreshEvent $true -} - -$queueChecks = @( - Get-TransportQueueGroupHealth -Name 'endpoint' -StateRoot $stateRoot -QueuePattern 'dlp-endpoint-signals-queue*.jsonl' -StaleAfterSeconds $transportStaleSeconds -MaxDepth $queueMaxDepth -ActiveProcessCount @($endpointCollectorProcesses).Count -Required $liveSessionScopedCollectorsRequired -) -if ($fileOpsExpected) { - $queueChecks += Get-TransportQueueGroupHealth -Name 'fileops' -StateRoot $stateRoot -QueuePattern 'file-operations-queue*.jsonl' -StaleAfterSeconds $transportStaleSeconds -MaxDepth $queueMaxDepth -ActiveProcessCount @($fileCollectorProcesses).Count -Required $liveSessionScopedCollectorsRequired -} - -$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 { -} - -$watcherCountsOk = $true -$endpointProcessOk = $true -$fileProcessOk = $true -$browserProcessOk = $true -$sessionCollectorOk = (@($sessionCollectorProcesses).Count -eq 1) - -$result = [ordered]@{ - generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - configPath = $ConfigPath - serverUrl = $serverUrl - apiBase = $apiBase - awHostname = $awHostname - installRoot = $installRoot - stateRoot = $stateRoot - timing = [ordered]@{ - pollSeconds = [int]$pollSeconds - pulseSeconds = [int]$pulseSeconds - freshnessSeconds = [int]$freshnessSeconds - sessionFreshnessSeconds = [int]$sessionFreshnessSeconds - transportStaleSeconds = [int]$transportStaleSeconds - } - files = [ordered]@{ - required = $requiredFiles - missing = $missingFiles - ok = ($missingFiles.Count -eq 0) - } - tasks = [ordered]@{ - list = $tasks - ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.ok })) - } - processes = [ordered]@{ - liveSessionBoundUsers = $liveSessionBoundUsers - sessionBoundUsers = $interactiveSessionBoundUsers - sessionScopedExpectedCount = [int]$sessionScopedExpectedCount - liveSessionScopedCollectorsRequired = [bool]$liveSessionScopedCollectorsRequired - collectorGuardServiceActive = [bool]$collectorGuardActive - watchers = @($runningWatchers) - watcherDuplicates = @($watcherDuplicates) - sessionCollectors = @($sessionCollectorProcesses) - sessionCollectorDuplicates = @($sessionCollectorDuplicates) - browserCollectors = @($browserCollectorProcesses) - browserCollectorDuplicates = @($browserCollectorDuplicates) - endpointCollectors = @($endpointCollectorProcesses) - endpointCollectorDuplicates = @($endpointCollectorDuplicates) - fileCollectors = @($fileCollectorProcesses) - fileCollectorDuplicates = @($fileCollectorDuplicates) - ok = [bool]( - $watcherCountsOk -and - $sessionCollectorOk -and - $browserProcessOk -and - $endpointProcessOk -and - $fileProcessOk -and - ($watcherDuplicates.Count -eq 0) -and - ($sessionCollectorDuplicates.Count -eq 0) -and - ($browserCollectorDuplicates.Count -eq 0) -and - ($endpointCollectorDuplicates.Count -eq 0) -and - ($fileCollectorDuplicates.Count -eq 0) - ) - } - buckets = [ordered]@{ - list = @($bucketChecks) - ok = [bool](-not ($bucketChecks | Where-Object { -not $_.ok })) - } - queues = [ordered]@{ - list = @($queueChecks) - ok = [bool](-not ($queueChecks | Where-Object { -not $_.ok })) - } - printTelemetry = [ordered]@{ - operationalLogEnabled = $printServiceOperationalEnabled - jobTitlePolicyEnabled = $printJobTitlePolicyEnabled - ok = [bool]($printServiceOperationalEnabled -and $printJobTitlePolicyEnabled) - } - sessionEvents = [ordered]@{ - bucketId = $sessionEventsBucketId - logonEnabled = [bool]$sessionLogonEnabled - processEventsEnabled = [bool]$sessionProcessEventsEnabled - ok = $true - } - forensics = [ordered]@{ - evtxExportRoot = if ($config.PSObject.Properties.Name -contains 'forensics' -and $config.forensics.PSObject.Properties.Name -contains 'evtxExportRoot') { [string]$config.forensics.evtxExportRoot } else { $null } - retentionDays = if ($config.PSObject.Properties.Name -contains 'forensics' -and $config.forensics.PSObject.Properties.Name -contains 'retentionDays') { [int]$config.forensics.retentionDays } else { $null } - evtxChannels = if ($config.PSObject.Properties.Name -contains 'forensics' -and $config.forensics.PSObject.Properties.Name -contains 'evtxChannels') { @($config.forensics.evtxChannels) } else { @() } - ok = [bool]( - ($config.PSObject.Properties.Name -contains 'forensics') -and - ($config.forensics.PSObject.Properties.Name -contains 'evtxExportRoot') -and - ($config.forensics.PSObject.Properties.Name -contains 'retentionDays') -and - ($config.forensics.PSObject.Properties.Name -contains 'evtxChannels') -and - (@($config.forensics.evtxChannels).Count -gt 0) - ) - } -} - -$result.summary = [ordered]@{ - failedSections = @( - 'files', 'tasks', 'processes', 'buckets', 'queues', 'printTelemetry', 'forensics' | - Where-Object { -not [bool]$result.$_.ok } - ) -} - -$result.overallOk = [bool]( - $result.files.ok -and - $result.tasks.ok -and - $result.processes.ok -and - $result.buckets.ok -and - $result.queues.ok -and - $result.printTelemetry.ok -and - $result.forensics.ok -) - -$result diff --git a/install-kit-awindows-20260427-211240/windows/web-category-rules.example.json b/install-kit-awindows-20260427-211240/windows/web-category-rules.example.json deleted file mode 100755 index ab51f5a..0000000 --- a/install-kit-awindows-20260427-211240/windows/web-category-rules.example.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "version": 1, - "description": "Override or extend built-in ActivityWatch web categorization rules.", - "rules": [ - { - "name": "work_crm", - "group": "work", - "domains": [ - "crm.example.com", - "portal.example.org" - ] - }, - { - "name": "work_erp", - "group": "work", - "domains": [ - "erp.example.com", - "bi.example.com" - ] - }, - { - "name": "neutral_training", - "group": "neutral", - "domains": [ - "wiki.example.net", - "kb.example.net" - ] - }, - { - "name": "personal_social", - "group": "personal", - "domains": [ - "social.example.net" - ] - } - ] -} diff --git a/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 b/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 deleted file mode 100644 index a551627..0000000 --- a/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 +++ /dev/null @@ -1,574 +0,0 @@ -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$Hostname, - [int]$PollSeconds = 0 -) - -# Force UTF-8 for console I/O -try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} -try { [Console]::InputEncoding = [System.Text.Encoding]::UTF8 } catch {} -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Continue' - -function Decode-Bytes-Auto { - param([byte[]]$Bytes) - if (-not $Bytes) { return '' } - - $candidates = @() - - # Try strict UTF8 first (detect invalid sequences) - try { - $utf8Strict = New-Object System.Text.UTF8Encoding($false,$true) - $txt = $utf8Strict.GetString($Bytes) - $candidates += @{enc='utf8'; text=$txt} - } - catch { - # invalid UTF8 sequences; ignore - } - - # Try CP866 and CP1251 - try { $cp866 = [System.Text.Encoding]::GetEncoding(866); $txt866 = $cp866.GetString($Bytes); $candidates += @{enc='cp866'; text=$txt866} } catch {} - try { $cp1251 = [System.Text.Encoding]::GetEncoding(1251); $txt1251 = $cp1251.GetString($Bytes); $candidates += @{enc='cp1251'; text=$txt1251} } catch {} - - # If nothing decoded yet, fallback to UTF8 permissive - if ($candidates.Count -eq 0) { - try { $txt = [System.Text.Encoding]::UTF8.GetString($Bytes); return $txt } catch { return '' } - } - - # Score decodings by count of Cyrillic letters; prefer highest - $best = $null; $bestScore = -1 - foreach ($c in $candidates) { - $t = $c.text - if (-not $t) { continue } - $score = 0 - try { $score = ([regex]::Matches($t,'\p{IsCyrillic}')).Count } catch { $score = 0 } - if ($score -gt $bestScore) { $best = $c; $bestScore = $score } - } - - if ($best -ne $null) { return $best.text } - - # Final fallback: first candidate text - return $candidates[0].text -} - -function Get-Config { - param([string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - throw "Config not found: $Path" - } - try { - $bytes = [System.IO.File]::ReadAllBytes($Path) - # Config is JSON. Prefer deterministic BOM-based decoding over heuristics. - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { - $text = [System.Text.Encoding]::UTF8.GetString($bytes) - } elseif ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { - $text = [System.Text.Encoding]::Unicode.GetString($bytes) - } else { - $text = [System.Text.Encoding]::UTF8.GetString($bytes) - } - $text = $text -replace '^\uFEFF', '' - return $text | ConvertFrom-Json -ErrorAction Stop - } - catch { - throw "Failed to read config: $Path - $($_.Exception.Message)" - } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - try { - $bytes = [System.Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -ErrorAction Stop | Out-Null - return $true - } - catch { - Write-Verbose "POST error: $($_.Exception.Message)" - return $false - } -} - -function Ensure-Bucket { - param( - [Parameter(Mandatory = $true)][string]$ApiBase, - [Parameter(Mandatory = $true)][string]$BucketId, - [Parameter(Mandatory = $true)][string]$HostnameValue, - [string]$ClientName = 'aw-worktime-session-collector', - [string]$BucketType = 'aw.worktime.session' - ) - try { Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" -ErrorAction Stop | Out-Null; return } catch { Write-Verbose "Bucket not found, creating: $BucketId" } - - $body = @{ client=$ClientName; type=$BucketType; hostname=$HostnameValue } | ConvertTo-Json -Compress - $attempts = 0 - while ($attempts -lt 3) { - $attempts++ - $ok = Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body - if ($ok) { return } - Start-Sleep -Seconds (2 * $attempts) - } - try { Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" -ErrorAction Stop | Out-Null } catch { Write-Verbose "Ensure-Bucket final check failed: $BucketId" } -} - -function Run-QueryUser { - $tries = @( - @{File='cmd.exe';Args='/c query user'}, - @{File='cmd.exe';Args='/c quser'}, - @{File='query.exe';Args='user'}, - @{File='quser.exe';Args=''} - ) - foreach ($t in $tries) { - try { - $psi = New-Object System.Diagnostics.ProcessStartInfo - $psi.FileName = $t.File - if ($t.Args) { $psi.Arguments = $t.Args } - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - $psi.UseShellExecute = $false - $psi.CreateNoWindow = $true - - $proc = [System.Diagnostics.Process]::Start($psi) - $stream = $proc.StandardOutput.BaseStream - $ms = New-Object System.IO.MemoryStream - $buffer = New-Object byte[] 4096 - while (($read = $stream.Read($buffer,0,$buffer.Length)) -gt 0) { $ms.Write($buffer,0,$read) } - if (-not $proc.WaitForExit(8000)) { - try { $proc.Kill() } catch {} - continue - } - $bytes = $ms.ToArray() - - $text = Decode-Bytes-Auto -Bytes $bytes - if ($text -and $text.Trim()) { return ($text -split "\r?\n") | Where-Object { $_ -ne '' } } - } - catch { - # try next - } - } - return @() -} - -function Parse-SessionLines { - param([string[]]$Lines) - $records = @() - if (-not $Lines) { return $records } - - $startIndex = 0 - # NOTE: Keep this script ASCII-only to stay compatible with Windows PowerShell 5 - # when the file is UTF-8 without BOM. Avoid Cyrillic literals in regex patterns. - if ($Lines.Count -gt 0 -and $Lines[0] -match '\b(USERNAME|UserName|USER)\b') { $startIndex = 1 } - - for ($i = $startIndex; $i -lt $Lines.Count; $i++) { - $line = ($Lines[$i] -replace '^\s*>', '').Trim() - if (-not $line) { continue } - - $parts = $line -split '\s+' - if ($parts.Count -lt 3) { continue } - $user = $parts[0] - $sess = '' - $id = -1 - $state = '' - - if ($parts.Count -ge 4 -and $parts[1] -match '^\d+$') { - $sess = '' - $id = [int]$parts[1] - $state = [string]$parts[2] - } - elseif ($parts.Count -ge 4 -and $parts[2] -match '^\d+$') { - $sess = [string]$parts[1] - $id = [int]$parts[2] - $state = [string]$parts[3] - } - else { - continue - } - - if ($id -lt 0) { continue } - - $records += [pscustomobject]@{ username=$user; sessionName=$sess; sessionId=$id; state=$state } - } - return $records -} - -function Test-SessionIsActive { - param([string]$State) - if (-not $State) { return $false } - $s = $State.Trim().ToLowerInvariant() - # Match English "active" and Russian "актив*" without embedding Cyrillic. - # "актив" = \u0430\u043A\u0442\u0438\u0432 - return ($s -match 'active') -or ($s -match '\u0430\u043a\u0442\u0438\u0432') -} - -function Get-CanonicalUserId { - param( - [pscustomobject]$Config, - [string]$HostnameValue, - [string]$Username - ) - - $normalizedUser = [string]$Username - if ([string]::IsNullOrWhiteSpace($normalizedUser)) { - return '' - } - - if ($Config -and $Config.PSObject.Properties.Name -contains 'userTasks' -and $Config.userTasks) { - foreach ($task in @($Config.userTasks)) { - try { - $taskUserId = [string]$task.userId - if ([string]::IsNullOrWhiteSpace($taskUserId)) { - continue - } - $parts = $taskUserId -split '\\', 2 - if ($parts.Count -eq 2 -and $parts[1].Equals($normalizedUser, [System.StringComparison]::OrdinalIgnoreCase)) { - return $taskUserId - } - } - catch { - } - } - } - - return "$HostnameValue\$normalizedUser" -} - -function Get-SessionEventsBucketId { - param( - [pscustomobject]$Config, - [string]$HostnameValue - ) - $prefix = 'aw-session-events' - if ( - $Config -and - $Config.PSObject.Properties.Name -contains 'sessionEvents' -and - $Config.sessionEvents -and - $Config.sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and - -not [string]::IsNullOrWhiteSpace([string]$Config.sessionEvents.bucketPrefix) - ) { - $prefix = [string]$Config.sessionEvents.bucketPrefix - } - return ('{0}_{1}' -f $prefix, $HostnameValue) -} - -function Test-SessionProcessEventsEnabled { - param([pscustomobject]$Config) - if ( - $Config -and - $Config.PSObject.Properties.Name -contains 'sessionEvents' -and - $Config.sessionEvents -and - $Config.sessionEvents.PSObject.Properties.Name -contains 'processEventsEnabled' - ) { - return [bool]$Config.sessionEvents.processEventsEnabled - } - return $true -} - -function Get-ProcessStatePath { - param([pscustomobject]$Config) - $stateRoot = '' - if ($Config -and $Config.PSObject.Properties.Name -contains 'paths' -and $Config.paths) { - if ($Config.paths.PSObject.Properties.Name -contains 'stateRoot') { - $stateRoot = [string]$Config.paths.stateRoot - } - } - if ([string]::IsNullOrWhiteSpace($stateRoot)) { - $stateRoot = 'C:\ProgramData\AWatch-rus' - } - return (Join-Path $stateRoot 'session-process-state.json') -} - -function Load-ProcessState { - param([string]$Path) - $map = @{} - try { - if (Test-Path -LiteralPath $Path) { - $raw = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop - if (-not [string]::IsNullOrWhiteSpace($raw)) { - $obj = $raw | ConvertFrom-Json -ErrorAction Stop - foreach ($item in @($obj.processes)) { - if (-not $item) { continue } - $key = [string]$item.key - if ([string]::IsNullOrWhiteSpace($key)) { continue } - $map[$key] = $item - } - } - } - } - catch { - Write-Verbose "Process state load error: $($_.Exception.Message)" - } - return $map -} - -function Save-ProcessState { - param( - [string]$Path, - [hashtable]$Map - ) - try { - $dir = Split-Path -Path $Path -Parent - if ($dir -and -not (Test-Path -LiteralPath $dir)) { - New-Item -Path $dir -ItemType Directory -Force | Out-Null - } - $items = @() - foreach ($entry in $Map.GetEnumerator()) { - $value = $entry.Value - if ($null -eq $value) { continue } - $items += [pscustomobject]@{ - key = [string]$entry.Key - processId = [int]$value.processId - sessionId = [int]$value.sessionId - username = [string]$value.username - userId = [string]$value.userId - state = [string]$value.state - processName = [string]$value.processName - commandLine = [string]$value.commandLine - createdAt = [string]$value.createdAt - hostname = [string]$value.hostname - } - } - $payload = [pscustomobject]@{ processes = $items } - $payload | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $Path -Encoding UTF8 - } - catch { - Write-Verbose "Process state save error: $($_.Exception.Message)" - } -} - -function Test-ExcludedSessionProcess { - param( - [string]$Name, - [string]$CommandLine - ) - $n = [string]$Name - if ([string]::IsNullOrWhiteSpace($n)) { return $true } - if ($n -match '^(Idle|System|Registry|svchost|services|lsass|winlogon|csrss|fontdrvhost|dwm|taskhostw|sihost|explorer)\.exe$') { return $true } - if ($n -match '^(aw-watcher-afk|aw-watcher-window|conhost)\.exe$') { return $true } - return $false -} - -function Get-SessionProcessSnapshot { - param( - [pscustomobject]$Config, - [string]$HostnameValue, - [object[]]$SessionRecords - ) - $bySession = @{} - foreach ($rec in @($SessionRecords)) { - if ($null -eq $rec) { continue } - $sid = [int]$rec.sessionId - $bySession[$sid] = [pscustomobject]@{ - username = [string]$rec.username - userId = Get-CanonicalUserId -Config $Config -HostnameValue $HostnameValue -Username ([string]$rec.username) - state = [string]$rec.state - } - } - - $snapshot = @{} - if ($bySession.Count -eq 0) { - return $snapshot - } - - try { - $procs = Get-Process -ErrorAction Stop | Where-Object { $bySession.ContainsKey([int]$_.SessionId) } - } - catch { - Write-Verbose "Process snapshot error: $($_.Exception.Message)" - return $snapshot - } - - foreach ($proc in @($procs)) { - try { - $sid = [int]$proc.SessionId - } - catch { - continue - } - if (-not $bySession.ContainsKey($sid)) { continue } - - $name = [string]$proc.ProcessName - if ($name -and $name -notmatch '\.exe$') { - $name = "$name.exe" - } - $commandLine = '' - if (Test-ExcludedSessionProcess -Name $name -CommandLine $commandLine) { continue } - - $createdAt = '' - try { - if ($proc.StartTime) { - $createdAt = $proc.StartTime.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - } - } - catch { - $createdAt = '' - } - if ([string]::IsNullOrWhiteSpace($createdAt)) { - $createdAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - } - - $key = ('{0}|{1}|{2}' -f $sid, [int]$proc.Id, $createdAt) - $sessionMeta = $bySession[$sid] - $snapshot[$key] = [pscustomobject]@{ - processId = [int]$proc.Id - sessionId = $sid - username = [string]$sessionMeta.username - userId = [string]$sessionMeta.userId - state = [string]$sessionMeta.state - processName = $name - commandLine = $commandLine - createdAt = $createdAt - hostname = $HostnameValue - } - } - return $snapshot -} - -function Publish-SessionProcessEvents { - param( - [string]$ApiBase, - [string]$BucketId, - [hashtable]$Previous, - [hashtable]$Current - ) - foreach ($entry in $Current.GetEnumerator()) { - if ($Previous.ContainsKey($entry.Key)) { continue } - $item = $entry.Value - $payload = [pscustomobject]@{ - timestamp = [string]$item.createdAt - duration = 0 - data = [pscustomobject]@{ - eventType = 'process_start' - username = [string]$item.username - userId = [string]$item.userId - sessionId = [int]$item.sessionId - state = [string]$item.state - processId = [int]$item.processId - processName = [string]$item.processName - commandLine = [string]$item.commandLine - createdAt = [string]$item.createdAt - hostname = [string]$item.hostname - source = 'worktime-session-collector' - } - } | ConvertTo-Json -Depth 6 -Compress - try { - [void](Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId/heartbeat?pulsetime=1" -Json $payload) - } - catch { - Write-Verbose "Process start publish error: $($_.Exception.Message)" - } - } - - $nowUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - foreach ($entry in $Previous.GetEnumerator()) { - if ($Current.ContainsKey($entry.Key)) { continue } - $item = $entry.Value - $payload = [pscustomobject]@{ - timestamp = $nowUtc - duration = 0 - data = [pscustomobject]@{ - eventType = 'process_stop' - username = [string]$item.username - userId = [string]$item.userId - sessionId = [int]$item.sessionId - state = [string]$item.state - processId = [int]$item.processId - processName = [string]$item.processName - commandLine = [string]$item.commandLine - createdAt = [string]$item.createdAt - hostname = [string]$item.hostname - source = 'worktime-session-collector' - } - } | ConvertTo-Json -Depth 6 -Compress - try { - [void](Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId/heartbeat?pulsetime=1" -Json $payload) - } - catch { - Write-Verbose "Process stop publish error: $($_.Exception.Message)" - } - } -} - -# Main -$cfg = Get-Config -Path $ConfigPath -$hostValue = if ($Hostname -and $Hostname.Trim()) { $Hostname.Trim() } elseif ($cfg -and $cfg.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$cfg.awHostname)) { [string]$cfg.awHostname } elseif ($cfg -and $cfg.awHostname) { [string]$cfg.awHostname } else { [string]$env:COMPUTERNAME } -try { $apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port } catch { throw 'Invalid server configuration in config file.' } - -$bucketId = 'aw-worktime-sessions_' + $hostValue -$sessionEventsBucketId = Get-SessionEventsBucketId -Config $cfg -HostnameValue $hostValue -$processEventsEnabled = Test-SessionProcessEventsEnabled -Config $cfg -$processStatePath = Get-ProcessStatePath -Config $cfg -$sleepSec = if ($PollSeconds -gt 0) { $PollSeconds } elseif ($cfg.collector -and $cfg.collector.pollSeconds) { [int]$cfg.collector.pollSeconds } else { 30 } -$pulse = [Math]::Max($sleepSec * 3, 30) - -Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue -if ($processEventsEnabled) { - Ensure-Bucket -ApiBase $apiBase -BucketId $sessionEventsBucketId -HostnameValue $hostValue -ClientName 'aw-session-events' -BucketType 'aw.session.event' - $previousProcessState = Load-ProcessState -Path $processStatePath -} -else { - $previousProcessState = @{} -} - -while ($true) { - $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - try { - $lines = Run-QueryUser - $records = Parse-SessionLines -Lines $lines - } - catch { - Write-Verbose "Session parse error: $($_.Exception.Message)" - $records = @() - } - - if (-not $records -or $records.Count -eq 0) { - # Fallback sample: keep bucket alive even when query user output is unavailable - # in non-interactive/session-0 contexts. - $records = @( - [pscustomobject]@{ - username = [string]$env:USERNAME - sessionName = '' - sessionId = [int](Get-Process -Id $PID).SessionId - state = 'Unknown' - } - ) - } - - if ($processEventsEnabled) { - $currentProcessState = Get-SessionProcessSnapshot -Config $cfg -HostnameValue $hostValue -SessionRecords $records - Publish-SessionProcessEvents -ApiBase $apiBase -BucketId $sessionEventsBucketId -Previous $previousProcessState -Current $currentProcessState - Save-ProcessState -Path $processStatePath -Map $currentProcessState - $previousProcessState = $currentProcessState - } - - foreach ($rec in $records) { - $canonicalUserId = Get-CanonicalUserId -Config $cfg -HostnameValue $hostValue -Username ([string]$rec.username) - $payloadObj = [PSCustomObject]@{ - timestamp = $now - duration = $sleepSec - data = [PSCustomObject]@{ - username = [string]$rec.username - userId = $canonicalUserId - sessionId = [int]$rec.sessionId - sessionName = [string]$rec.sessionName - state = [string]$rec.state - active = Test-SessionIsActive -State ([string]$rec.state) - sampleSeconds = $sleepSec - pollSeconds = $sleepSec - hostname = $hostValue - source = 'worktime-session-collector' - } - } - - $payload = $payloadObj | ConvertTo-Json -Depth 6 -Compress - - try { - $ok = Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload - if (-not $ok) { Write-Verbose "Heartbeat not confirmed for user $($rec.username)" } - } - catch { - Write-Verbose "Heartbeat error: $($_.Exception.Message)" - } - } - - Start-Sleep -Seconds $sleepSec -} diff --git a/ops/detmir-ai/detmir-auto-rust-shadow.service b/ops/detmir-ai/detmir-auto-rust-shadow.service index 8f41532..c63f480 100644 --- a/ops/detmir-ai/detmir-auto-rust-shadow.service +++ b/ops/detmir-ai/detmir-auto-rust-shadow.service @@ -10,8 +10,8 @@ Group=igor ExecCondition=/bin/sh -c '! systemctl -q is-active detmir-auto.service' Environment=DETMIR_AI_STATE_DIR=/var/lib/detmir-ai/shadow/detmir-auto-rust Environment=DETMIR_AI_RUN_DIR=/var/lib/detmir-ai/shadow/detmir-auto-rust/locks -Environment=no_proxy=localhost,127.0.0.1,192.168.100.18,10.10.10.13,10.10.10.2,10.10.10.0/24,192.168.100.0/24 -Environment=NO_PROXY=localhost,127.0.0.1,192.168.100.18,10.10.10.13,10.10.10.2,10.10.10.0/24,192.168.100.0/24 +Environment=no_proxy=localhost,127.0.0.1,198.51.100.18,192.0.2.13,192.0.2.2,192.0.2.0/24,198.51.100.0/24 +Environment=NO_PROXY=localhost,127.0.0.1,198.51.100.18,192.0.2.13,192.0.2.2,192.0.2.0/24,198.51.100.0/24 ExecStart=/usr/local/bin/detmir-auto-rust --no-heal --no-report --command-timeout-seconds 180 SuccessExitStatus=2 TimeoutStartSec=240 diff --git a/ops/detmir-ai/detmir-heal-safe b/ops/detmir-ai/detmir-heal-safe index a60eccd..cf1429f 100644 --- a/ops/detmir-ai/detmir-heal-safe +++ b/ops/detmir-ai/detmir-heal-safe @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -aw_host="${DETMIR_AW_SSH_HOST:-igor@10.10.10.13}" +aw_host="${DETMIR_AW_SSH_HOST:-igor@192.0.2.13}" ssh -o BatchMode=yes \ -o ConnectTimeout=10 \ diff --git a/pfsense/pfsense-aw-poller.example.json b/pfsense/pfsense-aw-poller.example.json index 113ac2a..78a86e1 100644 --- a/pfsense/pfsense-aw-poller.example.json +++ b/pfsense/pfsense-aw-poller.example.json @@ -1,7 +1,7 @@ { "poll_interval_seconds": 60, "aw": { - "server_host": "10.10.10.13", + "server_host": "192.0.2.13", "server_port": 5600, "hostname": "PFSENSE-EDGE01", "pulse_time_seconds": 120, @@ -9,7 +9,7 @@ }, "pfsense": { "name": "pfSense Edge 01", - "host": "10.10.10.1", + "host": "192.0.2.1", "scheme": "https", "verify_tls": false, "timeout_seconds": 15, diff --git a/proxmox/create-ct.sh b/proxmox/create-ct.sh index c92ef25..11ed720 100755 --- a/proxmox/create-ct.sh +++ b/proxmox/create-ct.sh @@ -60,7 +60,7 @@ pct exec "$CT_ID" -- bash -lc ' export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y curl ca-certificates bash unzip xz-utils jq - mkdir -p /root/bootstrap /etc/activitywatch + mkdir -p /opt/detmir/bootstrap /etc/activitywatch ' echo "CT $CT_ID created and bootstrapped" diff --git a/proxmox/pfsense_openvpn_client_export.php b/proxmox/pfsense_openvpn_client_export.php index 34b6bc9..538b61f 100644 --- a/proxmox/pfsense_openvpn_client_export.php +++ b/proxmox/pfsense_openvpn_client_export.php @@ -64,12 +64,12 @@ function build_csc_index_by_common_name(array $entries): array { function default_csc_template(): array { return [ 'custom_options' => '', - 'local_network' => '10.0.0.0/8, 172.16.0.0/12, 192.168.100.0/24', + 'local_network' => '10.0.0.0/8, 172.16.0.0/12, 198.51.100.0/24', 'local_networkv6' => '', 'remote_network' => '', 'remote_networkv6' => '', - 'dns_server1' => '192.168.100.1', - 'dns_server2' => '192.168.100.250', + 'dns_server1' => '198.51.100.1', + 'dns_server2' => '198.51.100.250', 'dns_server3' => '', 'dns_server4' => '', 'push_blockoutsidedns' => '', diff --git a/proxmox/push-aw-artifacts.sh b/proxmox/push-aw-artifacts.sh index d3b2d91..c8e437d 100755 --- a/proxmox/push-aw-artifacts.sh +++ b/proxmox/push-aw-artifacts.sh @@ -17,7 +17,7 @@ ENV_FILE="${1:-$DEFAULT_ENV_FILE}" : "${CT_ID:?missing CT_ID}" -pct exec "$CT_ID" -- mkdir -p /root/bootstrap +pct exec "$CT_ID" -- mkdir -p /opt/detmir/bootstrap for file_name in \ install_aw_server.sh \ @@ -28,12 +28,12 @@ for file_name in \ aw-sw-cleanup.js \ aw-host-groups.json do - pct push "$CT_ID" "$PROJECT_ROOT/aw-server/$file_name" "/root/bootstrap/$file_name" + pct push "$CT_ID" "$PROJECT_ROOT/aw-server/$file_name" "/opt/detmir/bootstrap/$file_name" done -pct exec "$CT_ID" -- mkdir -p /root/bootstrap/settings -pct push "$CT_ID" "$PROJECT_ROOT/aw-server/settings/classes-worktime.json" "/root/bootstrap/settings/classes-worktime.json" -pct push "$CT_ID" "$PROJECT_ROOT/aw-server/settings/views-default.json" "/root/bootstrap/settings/views-default.json" +pct exec "$CT_ID" -- mkdir -p /opt/detmir/bootstrap/settings +pct push "$CT_ID" "$PROJECT_ROOT/aw-server/settings/classes-worktime.json" "/opt/detmir/bootstrap/settings/classes-worktime.json" +pct push "$CT_ID" "$PROJECT_ROOT/aw-server/settings/views-default.json" "/opt/detmir/bootstrap/settings/views-default.json" if [ -n "${AW_SERVER_VERSION:-}" ] && [ -n "${AW_SERVER_DOWNLOAD_URL:-}" ] && @@ -64,4 +64,4 @@ else echo "WARN: AW_SERVER_* variables are incomplete in $ENV_FILE; /etc/activitywatch/aw-server.env was not updated" >&2 fi -echo "Bootstrap artifacts pushed to CT $CT_ID:/root/bootstrap" +echo "Bootstrap artifacts pushed to CT $CT_ID:/opt/detmir/bootstrap" diff --git a/proxmox/test_tsj_guardian_bot.py b/proxmox/test_tsj_guardian_bot.py index d80ebb4..8341a75 100644 --- a/proxmox/test_tsj_guardian_bot.py +++ b/proxmox/test_tsj_guardian_bot.py @@ -79,8 +79,8 @@ def make_slo_cycle_bot(summary): class TsjGuardianBotTests(unittest.TestCase): def test_new_incident_auto_resolved_before_notification_stays_silent(self): - check_out = "2026-05-24 10:01:18 [FAIL] node_13: curl failed: http://10.10.10.13:5600/\n" - heal_out = "2026-05-24 10:01:21 [OK] node_13: HTTP 200 OK: http://10.10.10.13:5600/api/0/info\n" + check_out = "2026-05-24 10:01:18 [FAIL] node_13: curl failed: http://192.0.2.13:5600/\n" + heal_out = "2026-05-24 10:01:21 [OK] node_13: HTTP 200 OK: http://192.0.2.13:5600/api/0/info\n" bot = make_bot(check_rc=1, check_out=check_out, heal_rc=0, heal_out=heal_out) bot._handle_check_cycle() @@ -90,8 +90,8 @@ class TsjGuardianBotTests(unittest.TestCase): self.assertTrue(any("auto-resolved before operator notification" in msg for _, msg in bot._logs)) def test_transient_failure_is_suppressed_until_quorum(self): - check_out = "2026-05-24 10:01:18 [FAIL] node_13: curl failed: http://10.10.10.13:5600/\n" - heal_out = "2026-05-24 10:01:40 [FAIL] node_13: curl failed: http://10.10.10.13:5600/\n" + check_out = "2026-05-24 10:01:18 [FAIL] node_13: curl failed: http://192.0.2.13:5600/\n" + heal_out = "2026-05-24 10:01:40 [FAIL] node_13: curl failed: http://192.0.2.13:5600/\n" bot = make_bot(check_rc=1, check_out=check_out, heal_rc=1, heal_out=heal_out) bot.incident_failure_quorum_checks = 2 @@ -121,8 +121,8 @@ class TsjGuardianBotTests(unittest.TestCase): self.assertTrue(bot._notifications) def test_new_incident_notifies_only_after_autoheal_failure(self): - check_out = "2026-05-24 10:01:18 [FAIL] node_13: curl failed: http://10.10.10.13:5600/\n" - heal_out = "2026-05-24 10:01:40 [FAIL] node_13: curl failed: http://10.10.10.13:5600/\n" + check_out = "2026-05-24 10:01:18 [FAIL] node_13: curl failed: http://192.0.2.13:5600/\n" + heal_out = "2026-05-24 10:01:40 [FAIL] node_13: curl failed: http://192.0.2.13:5600/\n" bot = make_bot(check_rc=1, check_out=check_out, heal_rc=1, heal_out=heal_out) bot._handle_check_cycle() @@ -388,7 +388,7 @@ class TelegramApiDocumentTests(unittest.TestCase): class OpenVpnHelperTests(unittest.TestCase): def test_load_pfsense_readonly_env_uses_instance_override_path(self): env_text = """ -PFSENSE_URL=https://10.10.10.1:8443 +PFSENSE_URL=https://192.0.2.1:8443 PFSENSE_API_KEY=test-key VERIFY_SSL=false """ @@ -397,7 +397,7 @@ VERIFY_SSL=false bot.pfsense_env_path = "/tmp/pfsense.env.readonly" base_url, api_key, verify_ssl = MODULE.TSJGuardianBot._load_pfsense_readonly_env(bot) - self.assertEqual((base_url, api_key, verify_ssl), ("https://10.10.10.1:8443", "test-key", False)) + self.assertEqual((base_url, api_key, verify_ssl), ("https://192.0.2.1:8443", "test-key", False)) read_mock.assert_called_once_with(encoding="utf-8") def test_parse_openvpn_helper_result_returns_config_bytes(self): @@ -410,7 +410,7 @@ VERIFY_SSL=false "cert_created": True, "csc_created": True, "config_b64": MODULE.base64.b64encode( - b"client\nremote 10.10.10.1 1194 udp\n\nX\n\n" + b"client\nremote 192.0.2.1 1194 udp\n\nX\n\n" ).decode("ascii"), } ) @@ -421,7 +421,7 @@ VERIFY_SSL=false self.assertIn("planshet", summary) self.assertIn("10.0.13.22/24", summary) self.assertIn("сертификат создан: да", summary) - self.assertEqual(config_bytes, b"client\nremote 10.10.10.1 1194 udp\n\nX\n\n") + self.assertEqual(config_bytes, b"client\nremote 192.0.2.1 1194 udp\n\nX\n\n") def test_parse_openvpn_helper_result_rejects_invalid_config(self): bot = object.__new__(MODULE.TSJGuardianBot) @@ -439,8 +439,8 @@ VERIFY_SSL=false inventory = """ ### 2. pfSense -- SSH LAN: `10.10.10.1:2022` -- Web UI: `10.10.10.1:8443` +- SSH LAN: `192.0.2.1:2022` +- Web UI: `192.0.2.1:8443` - Admin login: `admin` - Admin password: `admin-secret` - Root login: `root` @@ -453,13 +453,13 @@ VERIFY_SSL=false bot.pfsense_inventory_path = "/tmp/inventory.md" host, port, user, password = MODULE.TSJGuardianBot._load_pfsense_inventory_access(bot) - self.assertEqual((host, port, user, password), ("10.10.10.1", 2022, "root", "secret")) + self.assertEqual((host, port, user, password), ("192.0.2.1", 2022, "root", "secret")) def test_load_pfsense_web_access_parses_web_values(self): inventory = """ ### 2. pfSense -- Web UI: `10.10.10.1:8443` +- Web UI: `192.0.2.1:8443` - Admin login: `admin` - Admin password: `admin-secret` @@ -470,7 +470,7 @@ VERIFY_SSL=false bot.pfsense_inventory_path = "/tmp/inventory.md" base_url, user, password = MODULE.TSJGuardianBot._load_pfsense_web_access(bot) - self.assertEqual((base_url, user, password), ("https://10.10.10.1:8443", "admin", "admin-secret")) + self.assertEqual((base_url, user, password), ("https://192.0.2.1:8443", "admin", "admin-secret")) def test_extract_pfsense_csrf_token(self): token = MODULE.TSJGuardianBot._extract_pfsense_csrf_token('var csrfMagicToken = "abc123";') @@ -491,8 +491,8 @@ class WorktimeCsvParseTests(unittest.TestCase): def test_parse_worktime_today_csv_uses_named_active_seconds_column(self): csv_text = ( "user,user_id,active_seconds,active_hhmm\n" - "user1,SHARKON2025\\\\user1,155,00:02\n" - "администратор,SHARKON2025\\\\Администратор,9330,02:35\n" + "user1,HOST-EXAMPLE\\\\user1,155,00:02\n" + "администратор,HOST-EXAMPLE\\\\Администратор,9330,02:35\n" ) rows = MODULE.TSJGuardianBot._parse_worktime_today_csv(csv_text) @@ -515,11 +515,11 @@ class WorktimeCsvParseTests(unittest.TestCase): timeout_exc = MODULE.requests.exceptions.ReadTimeout("read timeout") response = mock.Mock() response.raise_for_status.return_value = None - response.text = "user,user_id,active_seconds\nuser1,SHARKON2025\\\\user1,155\n" + response.text = "user,user_id,active_seconds\nuser1,HOST-EXAMPLE\\\\user1,155\n" get_mock.side_effect = [timeout_exc, response] bot = object.__new__(MODULE.TSJGuardianBot) - bot.aw_rus_worktime_base = "http://10.10.10.13:5610" + bot.aw_rus_worktime_base = "http://192.0.2.13:5610" csv_text = MODULE.TSJGuardianBot._fetch_worktime_today_csv(bot, timeout_sec=3, attempts=2) @@ -546,15 +546,15 @@ class AwRusDlpProbeTests(unittest.TestCase): if url.endswith("/buckets"): return response( { - "aw-worktime-sessions_SHARKON2025": {"metadata": {"end": fresh_worktime}}, - "aw-watcher-window_SHARKON2025": {"metadata": {"end": fresh_watcher}}, - "aw-watcher-afk_SHARKON2025": {"metadata": {"end": fresh_watcher}}, - "aw-dlp-endpoint-signals_SHARKON2025": {"metadata": {"end": stale_endpoint}}, - "aw-file-operations_SHARKON2025": {"metadata": {"end": fresh_fileops}}, - "aw-file-operations_10.10.10.13": {"metadata": {"end": fresh_fileops}}, + "aw-worktime-sessions_HOST-EXAMPLE": {"metadata": {"end": fresh_worktime}}, + "aw-watcher-window_HOST-EXAMPLE": {"metadata": {"end": fresh_watcher}}, + "aw-watcher-afk_HOST-EXAMPLE": {"metadata": {"end": fresh_watcher}}, + "aw-dlp-endpoint-signals_HOST-EXAMPLE": {"metadata": {"end": stale_endpoint}}, + "aw-file-operations_HOST-EXAMPLE": {"metadata": {"end": fresh_fileops}}, + "aw-file-operations_192.0.2.13": {"metadata": {"end": fresh_fileops}}, } ) - if "aw-rus-collector-guard_SHARKON2025/events" in url: + if "aw-rus-collector-guard_HOST-EXAMPLE/events" in url: return response( [ { @@ -563,41 +563,41 @@ class AwRusDlpProbeTests(unittest.TestCase): } ] ) - if "aw-worktime-sessions_SHARKON2025/events" in url: + if "aw-worktime-sessions_HOST-EXAMPLE/events" in url: return response([{"timestamp": fresh_worktime, "data": {"active": True}}]) - if "aw-watcher-window_SHARKON2025/events" in url: + if "aw-watcher-window_HOST-EXAMPLE/events" in url: return response([{"timestamp": fresh_watcher, "data": {}}]) - if "aw-watcher-afk_SHARKON2025/events" in url: + if "aw-watcher-afk_HOST-EXAMPLE/events" in url: return response([{"timestamp": fresh_watcher, "data": {}}]) - if "aw-dlp-endpoint-signals_SHARKON2025/events" in url: + if "aw-dlp-endpoint-signals_HOST-EXAMPLE/events" in url: return response([{"timestamp": stale_endpoint, "data": {"signalType": "self_test"}}]) - if "aw-file-operations_SHARKON2025/events" in url: + if "aw-file-operations_HOST-EXAMPLE/events" in url: return response([{"timestamp": fresh_fileops, "data": {}}]) - if "aw-file-operations_10.10.10.13/events" in url: + if "aw-file-operations_192.0.2.13/events" in url: return response([{"timestamp": fresh_fileops, "data": {}}]) - if url.endswith("/buckets/aw-watcher-window_SHARKON2025"): + if url.endswith("/buckets/aw-watcher-window_HOST-EXAMPLE"): return response({"metadata": {"end": fresh_watcher}}) - if url.endswith("/buckets/aw-watcher-afk_SHARKON2025"): + if url.endswith("/buckets/aw-watcher-afk_HOST-EXAMPLE"): return response({"metadata": {"end": fresh_watcher}}) - if url.endswith("/buckets/aw-dlp-endpoint-signals_SHARKON2025"): + if url.endswith("/buckets/aw-dlp-endpoint-signals_HOST-EXAMPLE"): return response({"metadata": {"end": stale_endpoint}}) - if url.endswith("/buckets/aw-file-operations_SHARKON2025"): + if url.endswith("/buckets/aw-file-operations_HOST-EXAMPLE"): return response({"metadata": {"end": fresh_fileops}}) - if url.endswith("/buckets/aw-file-operations_10.10.10.13"): + if url.endswith("/buckets/aw-file-operations_192.0.2.13"): return response({"metadata": {"end": fresh_fileops}}) raise AssertionError(f"unexpected url {url}") get_mock.side_effect = fake_get bot = object.__new__(MODULE.TSJGuardianBot) - bot.aw_rus_api_base = "http://10.10.10.13:5600/api/0" - bot.aw_rus_worktime_base = "http://10.10.10.13:5610" - bot.aw_rus_host = "SHARKON2025" + bot.aw_rus_api_base = "http://192.0.2.13:5600/api/0" + bot.aw_rus_worktime_base = "http://192.0.2.13:5610" + bot.aw_rus_host = "HOST-EXAMPLE" bot.aw_rus_stale_sec = 900 bot.aw_rus_primary_user = "user1" bot._fetch_worktime_today_csv = lambda timeout_sec=20, attempts=2: ( "user,user_id,active_seconds\n" - "user1,SHARKON2025\\\\user1,120\n" + "user1,HOST-EXAMPLE\\\\user1,120\n" ) with mock.patch("proxmox.tsj_guardian_bot.datetime") as dt_mock: @@ -625,8 +625,8 @@ class AwRusDlpProbeTests(unittest.TestCase): def fake_get(url, timeout=20): if url.endswith("/buckets"): - return response({"aw-worktime-sessions_SHARKON2025": {"metadata": {"end": fresh_worktime}}}) - if "aw-rus-collector-guard_SHARKON2025/events" in url: + return response({"aw-worktime-sessions_HOST-EXAMPLE": {"metadata": {"end": fresh_worktime}}}) + if "aw-rus-collector-guard_HOST-EXAMPLE/events" in url: return response( [ { @@ -635,35 +635,35 @@ class AwRusDlpProbeTests(unittest.TestCase): } ] ) - if "aw-worktime-sessions_SHARKON2025/events" in url: + if "aw-worktime-sessions_HOST-EXAMPLE/events" in url: return response([{"timestamp": fresh_worktime, "data": {"active": False}}]) - if "aw-watcher-window_SHARKON2025/events" in url: + if "aw-watcher-window_HOST-EXAMPLE/events" in url: return response([{"timestamp": fresh_watcher, "data": {}}]) - if "aw-watcher-afk_SHARKON2025/events" in url: + if "aw-watcher-afk_HOST-EXAMPLE/events" in url: return response([{"timestamp": fresh_watcher, "data": {}}]) - if "aw-dlp-endpoint-signals_SHARKON2025/events" in url: + if "aw-dlp-endpoint-signals_HOST-EXAMPLE/events" in url: return response([{"timestamp": stale_endpoint, "data": {"signalType": "self_test"}}]) - if "aw-file-operations_SHARKON2025/events" in url: + if "aw-file-operations_HOST-EXAMPLE/events" in url: return response([]) - if "aw-file-operations_10.10.10.13/events" in url: + if "aw-file-operations_192.0.2.13/events" in url: return response([]) - if url.endswith("/buckets/aw-file-operations_SHARKON2025"): + if url.endswith("/buckets/aw-file-operations_HOST-EXAMPLE"): return response({"metadata": {"end": stale_endpoint}}) - if url.endswith("/buckets/aw-file-operations_10.10.10.13"): + if url.endswith("/buckets/aw-file-operations_192.0.2.13"): return response({"metadata": {"end": stale_endpoint}}) raise AssertionError(f"unexpected url {url}") get_mock.side_effect = fake_get bot = object.__new__(MODULE.TSJGuardianBot) - bot.aw_rus_api_base = "http://10.10.10.13:5600/api/0" - bot.aw_rus_worktime_base = "http://10.10.10.13:5610" - bot.aw_rus_host = "SHARKON2025" + bot.aw_rus_api_base = "http://192.0.2.13:5600/api/0" + bot.aw_rus_worktime_base = "http://192.0.2.13:5610" + bot.aw_rus_host = "HOST-EXAMPLE" bot.aw_rus_stale_sec = 900 bot.aw_rus_primary_user = "user1" bot._fetch_worktime_today_csv = lambda timeout_sec=20, attempts=2: ( "user,user_id,active_seconds\n" - "user1,SHARKON2025\\\\user1,0\n" + "user1,HOST-EXAMPLE\\\\user1,0\n" ) with mock.patch("proxmox.tsj_guardian_bot.datetime") as dt_mock: @@ -691,8 +691,8 @@ class AwRusDlpProbeTests(unittest.TestCase): def fake_get(url, timeout=20): if url.endswith("/buckets"): - return response({"aw-worktime-sessions_SHARKON2025": {"metadata": {"end": fresh_worktime}}}) - if "aw-rus-collector-guard_SHARKON2025/events" in url: + return response({"aw-worktime-sessions_HOST-EXAMPLE": {"metadata": {"end": fresh_worktime}}}) + if "aw-rus-collector-guard_HOST-EXAMPLE/events" in url: return response( [ { @@ -701,30 +701,30 @@ class AwRusDlpProbeTests(unittest.TestCase): } ] ) - if "aw-worktime-sessions_SHARKON2025/events" in url: + if "aw-worktime-sessions_HOST-EXAMPLE/events" in url: return response([{"timestamp": fresh_worktime, "data": {"active": False}}]) - if "aw-watcher-window_SHARKON2025/events" in url: + if "aw-watcher-window_HOST-EXAMPLE/events" in url: return response([{"timestamp": fresh_watcher, "data": {}}]) - if "aw-watcher-afk_SHARKON2025/events" in url: + if "aw-watcher-afk_HOST-EXAMPLE/events" in url: return response([{"timestamp": fresh_watcher, "data": {}}]) - if "aw-dlp-endpoint-signals_SHARKON2025/events" in url: + if "aw-dlp-endpoint-signals_HOST-EXAMPLE/events" in url: return response([{"timestamp": fresh_watcher, "data": {"signalType": "self_test"}}]) - if "aw-file-operations_SHARKON2025/events" in url: + if "aw-file-operations_HOST-EXAMPLE/events" in url: return response([]) - if "aw-file-operations_10.10.10.13/events" in url: + if "aw-file-operations_192.0.2.13/events" in url: return response([]) - if url.endswith("/buckets/aw-file-operations_SHARKON2025"): + if url.endswith("/buckets/aw-file-operations_HOST-EXAMPLE"): return response({"metadata": {"end": fresh_watcher}}) - if url.endswith("/buckets/aw-file-operations_10.10.10.13"): + if url.endswith("/buckets/aw-file-operations_192.0.2.13"): return response({"metadata": {"end": fresh_watcher}}) raise AssertionError(f"unexpected url {url}") get_mock.side_effect = fake_get bot = object.__new__(MODULE.TSJGuardianBot) - bot.aw_rus_api_base = "http://10.10.10.13:5600/api/0" - bot.aw_rus_worktime_base = "http://10.10.10.13:5610" - bot.aw_rus_host = "SHARKON2025" + bot.aw_rus_api_base = "http://192.0.2.13:5600/api/0" + bot.aw_rus_worktime_base = "http://192.0.2.13:5610" + bot.aw_rus_host = "HOST-EXAMPLE" bot.aw_rus_stale_sec = 900 bot.aw_rus_primary_user = "user1" bot._fetch_worktime_today_csv = mock.Mock(side_effect=MODULE.requests.exceptions.ReadTimeout("read timeout")) diff --git a/proxmox/tsj_guardian_bot.py b/proxmox/tsj_guardian_bot.py index 7fcf04c..bafea1f 100644 --- a/proxmox/tsj_guardian_bot.py +++ b/proxmox/tsj_guardian_bot.py @@ -372,9 +372,9 @@ class TSJGuardianBot: self.check_script = os.getenv( "CHECK_SCRIPT", f"{self.infra_admin_root}/scripts/system_self_support.sh --check" ) - self.aw_rus_api_base = os.getenv("AW_RUS_API_BASE", "http://10.10.10.13:5600/api/0").strip() - self.aw_rus_worktime_base = os.getenv("AW_RUS_WORKTIME_BASE", "http://10.10.10.13:5610").strip() - self.aw_dlp_policy_api_base = os.getenv("AW_DLP_POLICY_API_BASE", "http://10.10.10.13:5601/api/0").strip() + self.aw_rus_api_base = os.getenv("AW_RUS_API_BASE", "http://192.0.2.13:5600/api/0").strip() + self.aw_rus_worktime_base = os.getenv("AW_RUS_WORKTIME_BASE", "http://192.0.2.13:5610").strip() + self.aw_dlp_policy_api_base = os.getenv("AW_DLP_POLICY_API_BASE", "http://192.0.2.13:5601/api/0").strip() self.aw_dlp_policy_actor = os.getenv("AW_DLP_POLICY_ACTOR", "tsj-guardian-bot").strip() or "tsj-guardian-bot" self.aw_rus_worktime_heal_cmd = os.getenv( "AW_RUS_WORKTIME_HEAL_CMD", @@ -384,13 +384,13 @@ class TSJGuardianBot: "AW_RUS_DLP_HEAL_CMD", "", ).strip() - self.aw_rus_case_api_base = os.getenv("AW_RUS_CASE_API_BASE", "http://10.10.10.13:5602").strip() + self.aw_rus_case_api_base = os.getenv("AW_RUS_CASE_API_BASE", "http://192.0.2.13:5602").strip() self.aw_rus_hayabusa_enabled = env_bool("AW_RUS_HAYABUSA_ENABLED", True) self.aw_rus_hayabusa_ssh_cmd = os.getenv( "AW_RUS_HAYABUSA_SSH_CMD", "", ).strip() - self.aw_rus_host = os.getenv("AW_RUS_HOST", "SHARKON2025").strip() + self.aw_rus_host = os.getenv("AW_RUS_HOST", "HOST-EXAMPLE").strip() self.aw_rus_primary_user = os.getenv("AW_RUS_PRIMARY_USER", "USER1").strip() self.aw_rus_stale_sec = max(60, env_int("AW_RUS_STALE_SEC", 900)) self.aw_rus_slo_enabled = env_bool("AW_RUS_SLO_ENABLED", True) @@ -401,7 +401,7 @@ class TSJGuardianBot: "AW_RUS_SLO_SUMMARY_CMD", "", ).strip() - self.aw_rus_windows_host = os.getenv("AW_RUS_WINDOWS_HOST", "192.168.100.18").strip() or "192.168.100.18" + self.aw_rus_windows_host = os.getenv("AW_RUS_WINDOWS_HOST", "198.51.100.18").strip() or "198.51.100.18" self.aw_rus_windows_ssh_user = os.getenv("AW_RUS_WINDOWS_SSH_USER", "Администратор").strip() or "Администратор" self.aw_rus_windows_ssh_password = os.getenv("AW_RUS_WINDOWS_SSH_PASSWORD", "").strip() self.aw_rus_windows_config_path = os.getenv( @@ -418,7 +418,7 @@ class TSJGuardianBot: ).strip() or r"C:\ProgramData\AWatch-rus\worktime-session-collector.ps1" launch_tasks_raw = os.getenv( "AW_RUS_WINDOWS_LAUNCH_TASKS", - "ActivityWatch Launch [SHARKON2025_Администратор];ActivityWatch Launch [SHARKON2025_user5]", + "ActivityWatch Launch [HOST-EXAMPLE_Администратор];ActivityWatch Launch [HOST-EXAMPLE_user5]", ) self.aw_rus_windows_launch_tasks = [item.strip() for item in launch_tasks_raw.split(";") if item.strip()] self.aw_rus_windows_policy_path = os.getenv( @@ -2637,20 +2637,20 @@ class TSJGuardianBot: if "proxmox_api" in text: suggestions.append("Перезапустить pveproxy/pvedaemon/pve-cluster и проверить порт 8006.") if "pfsense_web" in text: - suggestions.append("Проверить доступность pfSense 10.10.10.1:8443, перезапустить WebGUI/nginx.") + suggestions.append("Проверить доступность pfSense 192.0.2.1:8443, перезапустить WebGUI/nginx.") if "pfsense_mcp" in text: suggestions.append("Проверить локальный pfsense-mcp-server.service, bearer token и endpoint 127.0.0.1:3010/mcp.") if "influxdb" in text: suggestions.append("Проверить контейнер InfluxDB и restart сервиса influxdb.") if "grafana" in text: - suggestions.append("Проверить grafana-server и NO_PROXY для 10.10.10.0/24.") + suggestions.append("Проверить grafana-server и NO_PROXY для 192.0.2.0/24.") if "loki" in text or "alloy" in text: suggestions.append("Проверить LXC логов и restart сервисов loki/alloy.") if "aw-rus:watcher-" in text or "aw-rus:worktime:" in text: - suggestions.append("Проверить Windows collector recovery: worktime-session-collector, ActivityWatch Recovery и Launch tasks на 192.168.100.18.") + suggestions.append("Проверить Windows collector recovery: worktime-session-collector, ActivityWatch Recovery и Launch tasks на 198.51.100.18.") suggestions.append("После Windows recovery проверить server-side aw-worktime-autoheal/ui-bridge для пересборки afk/window bucket'ов.") if "aw-rus:dlp-" in text: - suggestions.append("Проверить DLP endpoint/fileops collectors и server-side DLP transport на 10.10.10.13.") + suggestions.append("Проверить DLP endpoint/fileops collectors и server-side DLP transport на 192.0.2.13.") if "filesystem_usage" in text: suggestions.append("Проверить самые большие каталоги: du -x /var /srv /home, журналы в /var/log и apt cache.") suggestions.append("Проверить давление по снапшотам/хранилищу Proxmox и решить: очистка, ротация или расширение диска.") @@ -3490,7 +3490,7 @@ class TSJGuardianBot: fileops_checks = [ (f"aw-file-operations_{host}", "dlp-fileops-host", host), - ("aw-file-operations_10.10.10.13", "dlp-fileops-server", "10.10.10.13"), + ("aw-file-operations_192.0.2.13", "dlp-fileops-server", "192.0.2.13"), ] for bucket_id, label, bucket_host in fileops_checks: if worktime_activity is None: @@ -3604,12 +3604,12 @@ class TSJGuardianBot: bucket_defs = { f"aw-dlp-endpoint-signals_{host}": ("aw.dlp.endpoint.signal", "aw-dlp-endpoint-signals", host), f"aw-file-operations_{host}": ("aw.file.operation", "aw-file-operations", host), - "aw-file-operations_10.10.10.13": ("aw.file.operation", "aw-file-operations", "10.10.10.13"), + "aw-file-operations_192.0.2.13": ("aw.file.operation", "aw-file-operations", "192.0.2.13"), } map_fail_to_bucket = { "dlp-endpoint": f"aw-dlp-endpoint-signals_{host}", "dlp-fileops-host": f"aw-file-operations_{host}", - "dlp-fileops-server": "aw-file-operations_10.10.10.13", + "dlp-fileops-server": "aw-file-operations_192.0.2.13", } selected = [] diff --git a/scripts/aw-contour-smoke-10.10.10.2.sh b/scripts/aw-contour-smoke-gateway.sh similarity index 88% rename from scripts/aw-contour-smoke-10.10.10.2.sh rename to scripts/aw-contour-smoke-gateway.sh index 64cd38c..eddece2 100644 --- a/scripts/aw-contour-smoke-10.10.10.2.sh +++ b/scripts/aw-contour-smoke-gateway.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Smoke checks for the Proxmox/gateway/1C host 10.10.10.2. +# Smoke checks for the Proxmox/gateway/1C host 192.0.2.2. set -uo pipefail @@ -193,25 +193,25 @@ section "Ports" check_tcp "nginx http" 127.0.0.1 80 check_tcp "nginx https" 127.0.0.1 443 check_tcp "proxmox web" 127.0.0.1 8006 -check_tcp "1C company API" 10.10.10.2 8710 +check_tcp "1C company API" 192.0.2.2 8710 check_tcp "clickhouse native" 127.0.0.1 9000 check_tcp "clickhouse http" 127.0.0.1 8123 ss -tulpn | grep -E ':(80|443|8006|8710|8123|9000)\b' | sed 's/^/ /' || true section "Gateway HTTP" check_http_code "nginx healthz" "https://127.0.0.1/healthz" '^200$' -check_http_redirect "go proxmox gui" "https://127.0.0.1/go/proxmox-gui" '^30[1278]$' 'https://10.10.10.2:8006/' -check_http_redirect "go file1c brief" "https://127.0.0.1/go/file1c-brief" '^30[1278]$' 'http://10.10.10.2:8710/manager/brief' -check_http_redirect "go file1c actions" "https://127.0.0.1/go/file1c-actions" '^30[1278]$' 'http://10.10.10.2:8710/manager/actions' +check_http_redirect "go proxmox gui" "https://127.0.0.1/go/proxmox-gui" '^30[1278]$' 'https://192.0.2.2:8006/' +check_http_redirect "go file1c brief" "https://127.0.0.1/go/file1c-brief" '^30[1278]$' 'http://192.0.2.2:8710/manager/brief' +check_http_redirect "go file1c actions" "https://127.0.0.1/go/file1c-actions" '^30[1278]$' 'http://192.0.2.2:8710/manager/actions' section "1C Company API" -check_http_code "1C root redirect" "http://10.10.10.2:8710/" '^307$' -check_http_code "1C /health" "http://10.10.10.2:8710/health" '^200$' -check_http_code "1C /api/health" "http://10.10.10.2:8710/api/health" '^200$' -check_http_code "1C manager brief" "http://10.10.10.2:8710/manager/brief" '^200$' -check_http_code "1C manager actions" "http://10.10.10.2:8710/manager/actions" '^200$' -check_http_code "1C manager recovery" "http://10.10.10.2:8710/manager/recovery" '^200$' -check_http_code "1C weekly digest" "http://10.10.10.2:8710/manager/digest/weekly" '^200$' +check_http_code "1C root redirect" "http://192.0.2.2:8710/" '^307$' +check_http_code "1C /health" "http://192.0.2.2:8710/health" '^200$' +check_http_code "1C /api/health" "http://192.0.2.2:8710/api/health" '^200$' +check_http_code "1C manager brief" "http://192.0.2.2:8710/manager/brief" '^200$' +check_http_code "1C manager actions" "http://192.0.2.2:8710/manager/actions" '^200$' +check_http_code "1C manager recovery" "http://192.0.2.2:8710/manager/recovery" '^200$' +check_http_code "1C weekly digest" "http://192.0.2.2:8710/manager/digest/weekly" '^200$' section "ClickHouse" check_docker_container "aw-rus-1c-clickhouse" diff --git a/scripts/aw-contour-smoke-local.sh b/scripts/aw-contour-smoke-local.sh index 339438c..c2509d1 100644 --- a/scripts/aw-contour-smoke-local.sh +++ b/scripts/aw-contour-smoke-local.sh @@ -17,26 +17,26 @@ done ANSIBLE_DIR="$REPO_ROOT/ansible" INVENTORY="${AW_SMOKE_INVENTORY:-$ANSIBLE_DIR/inventory.ini}" -REMOTE_SCRIPT_SRC="$REPO_ROOT/scripts/aw-contour-smoke-10.10.10.2.sh" +REMOTE_SCRIPT_SRC="$REPO_ROOT/scripts/aw-contour-smoke-gateway.sh" REMOTE_SCRIPT_DST="${AW_SMOKE_REMOTE_SCRIPT:-/usr/local/sbin/aw-contour-smoke.sh}" REMOTE_RUST_SRC="${AW_SMOKE_REMOTE_RUST_SRC:-${CARGO_TARGET_DIR:-$REPO_ROOT/adk-rust/target}/release/aw-contour-smoke}" REMOTE_RUST_DST="${AW_SMOKE_REMOTE_RUST_BIN:-/usr/local/sbin/aw-contour-smoke}" -AW_SERVER="${AW_SMOKE_AW_SERVER:-http://10.10.10.13:5600}" -WORKTIME_API="${AW_SMOKE_WORKTIME_API:-http://10.10.10.13:5610}" -GRAFANA_URL="${AW_SMOKE_GRAFANA_URL:-http://10.10.10.11:3000}" +AW_SERVER="${AW_SMOKE_AW_SERVER:-http://192.0.2.13:5600}" +WORKTIME_API="${AW_SMOKE_WORKTIME_API:-http://192.0.2.13:5610}" +GRAFANA_URL="${AW_SMOKE_GRAFANA_URL:-http://192.0.2.11:3000}" GRAFANA_USER="${GRAFANA_USER:-igor}" GRAFANA_PASSWORD="${GRAFANA_PASSWORD:-}" -PROXMOX_HOST="${AW_SMOKE_PROXMOX_HOST:-10.10.10.2}" -AW_HOST="${AW_SMOKE_AW_HOST:-10.10.10.13}" -GRAFANA_HOST="${AW_SMOKE_GRAFANA_HOST:-10.10.10.11}" -WINDOWS_HOST="${AW_SMOKE_WINDOWS_HOST:-192.168.100.18}" -AW_SOURCE_HOSTNAME="${AW_SMOKE_SOURCE_HOSTNAME:-SHARKON2025}" +PROXMOX_HOST="${AW_SMOKE_PROXMOX_HOST:-192.0.2.2}" +AW_HOST="${AW_SMOKE_AW_HOST:-192.0.2.13}" +GRAFANA_HOST="${AW_SMOKE_GRAFANA_HOST:-192.0.2.11}" +WINDOWS_HOST="${AW_SMOKE_WINDOWS_HOST:-198.51.100.18}" +AW_SOURCE_HOSTNAME="${AW_SMOKE_SOURCE_HOSTNAME:-HOST-EXAMPLE}" LOG_DIR="${AW_SMOKE_LOG_DIR:-$REPO_ROOT/output/smoke}" RUN_REMOTE="${AW_SMOKE_RUN_REMOTE:-1}" RUN_WINRM="${AW_SMOKE_RUN_WINRM:-1}" RUN_SERVER_SYSTEMD="${AW_SMOKE_RUN_SERVER_SYSTEMD:-1}" -NO_PROXY_REQUIRED="localhost,127.0.0.1,$PROXMOX_HOST,$AW_HOST,$GRAFANA_HOST,$WINDOWS_HOST,10.10.10.0/24,192.168.100.0/24" +NO_PROXY_REQUIRED="localhost,127.0.0.1,$PROXMOX_HOST,$AW_HOST,$GRAFANA_HOST,$WINDOWS_HOST,192.0.2.0/24,198.51.100.0/24" if [ -n "${no_proxy:-}" ]; then export no_proxy="$no_proxy,$NO_PROXY_REQUIRED" else @@ -72,10 +72,10 @@ usage() { Usage: $(basename "$0") [--skip-remote] [--skip-winrm] [--skip-server-systemd] Environment overrides: - AW_SMOKE_AW_SERVER=http://10.10.10.13:5600 - AW_SMOKE_WORKTIME_API=http://10.10.10.13:5610 - AW_SMOKE_GRAFANA_URL=http://10.10.10.11:3000 - AW_SMOKE_SOURCE_HOSTNAME=SHARKON2025 + AW_SMOKE_AW_SERVER=http://192.0.2.13:5600 + AW_SMOKE_WORKTIME_API=http://192.0.2.13:5610 + AW_SMOKE_GRAFANA_URL=http://192.0.2.11:3000 + AW_SMOKE_SOURCE_HOSTNAME=HOST-EXAMPLE AW_SMOKE_ENV_FILE=$HOME/.config/aw-contour-smoke.env AW_SMOKE_LOG_DIR=$REPO_ROOT/output/smoke GRAFANA_USER/GRAFANA_PASSWORD via env or a local env file @@ -436,7 +436,7 @@ check_bucket_freshness() { run_remote_proxmox_script() { if [ "$RUN_REMOTE" != "1" ]; then - skip "remote 10.10.10.2 smoke skipped" + skip "remote 192.0.2.2 smoke skipped" return fi if ! have ansible; then @@ -448,7 +448,7 @@ run_remote_proxmox_script() { return fi - section "Deploy Remote Script To 10.10.10.2" + section "Deploy Remote Script To 192.0.2.2" if [ -x "$REMOTE_RUST_SRC" ]; then if ANSIBLE_NOCOLOR=1 ansible proxmox -i "$INVENTORY" -m copy -a "src=$REMOTE_RUST_SRC dest=$REMOTE_RUST_DST owner=root group=root mode=0755" >/tmp/aw-smoke-copy-rust.$$ 2>&1; then pass "remote Rust smoke deployed to $REMOTE_RUST_DST" @@ -471,14 +471,14 @@ run_remote_proxmox_script() { fi rm -f /tmp/aw-smoke-copy.$$ - section "Remote 10.10.10.2 Smoke" + section "Remote 192.0.2.2 Smoke" local tmp tmp="$(mktemp)" if ANSIBLE_NOCOLOR=1 ansible proxmox -i "$INVENTORY" -m shell -a "$REMOTE_SCRIPT_DST" >"$tmp" 2>&1; then - pass "remote 10.10.10.2 smoke completed" + pass "remote 192.0.2.2 smoke completed" sed 's/^/ /' "$tmp" else - fail "remote 10.10.10.2 smoke failed" + fail "remote 192.0.2.2 smoke failed" sed 's/^/ /' "$tmp" fi rm -f "$tmp" @@ -582,7 +582,7 @@ if [ "$RUN_WINRM" = "1" ] && have ansible; then check_ansible_module "Windows win_ping" aw_windows win_ping check_ansible_win_shell "Windows sessions" aw_windows '$psi = [System.Diagnostics.ProcessStartInfo]::new(); $psi.FileName = "$env:SystemRoot\System32\query.exe"; $psi.Arguments = "user"; $psi.UseShellExecute = $false; $psi.RedirectStandardOutput = $true; $psi.RedirectStandardError = $true; $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866); $psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866); $p = [System.Diagnostics.Process]::Start($psi); $out = $p.StandardOutput.ReadToEnd(); $err = $p.StandardError.ReadToEnd(); $p.WaitForExit(); [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); $out; if ($err) { $err }; if ($out -match "USERNAME|ПОЛЬЗОВАТЕЛЬ|администратор|Администратор") { exit 0 } else { exit $p.ExitCode }' check_ansible_win_shell_warn "Windows collector processes" aw_windows '$p = Get-Process aw-watcher-afk,aw-watcher-window -ErrorAction SilentlyContinue; if ($p) { $p | Select-Object Name,Id,SessionId,StartTime | Format-Table -AutoSize } else { "no aw-watcher-afk/window process visible to this WinRM session" }' - check_ansible_win_shell "Windows ActivityWatch tasks" aw_windows 'schtasks /Query /TN "ActivityWatch Recovery" /FO LIST /V; schtasks /Query /TN "ActivityWatch Launch [SHARKON2025_Администратор]" /FO LIST /V' + check_ansible_win_shell "Windows ActivityWatch tasks" aw_windows 'schtasks /Query /TN "ActivityWatch Recovery" /FO LIST /V; schtasks /Query /TN "ActivityWatch Launch [HOST-EXAMPLE_Администратор]" /FO LIST /V' else skip "Windows WinRM checks skipped" fi diff --git a/scripts/aw-webui-browser-smoke.mjs b/scripts/aw-webui-browser-smoke.mjs index 0f97304..da610ac 100644 --- a/scripts/aw-webui-browser-smoke.mjs +++ b/scripts/aw-webui-browser-smoke.mjs @@ -368,7 +368,7 @@ async function main() { const playwright = requestedEngine === "chromium-cli" ? null : loadPlaywright(); const awBase = normalizeBase(env("AW_BROWSER_SMOKE_AW_BASE", env("AW_SMOKE_AW_SERVER", "http://127.0.0.1:5600"))); const worktimeBase = normalizeBase(env("AW_BROWSER_SMOKE_WORKTIME_BASE", env("AW_SMOKE_WORKTIME_API", "http://127.0.0.1:5610"))); - const host = env("AW_BROWSER_SMOKE_HOST", env("AW_SMOKE_SOURCE_HOSTNAME", "SHARKON2025")); + const host = env("AW_BROWSER_SMOKE_HOST", env("AW_SMOKE_SOURCE_HOSTNAME", "HOST-EXAMPLE")); const timeoutMs = Number(env("AW_BROWSER_SMOKE_TIMEOUT_MS", "20000")); const settleMs = Number(env("AW_BROWSER_SMOKE_SETTLE_MS", "6000")); const renderTimeoutMs = Number(env("AW_BROWSER_SMOKE_RENDER_TIMEOUT_MS", "15000")); diff --git a/scripts/diag_and_manual_restart.sh b/scripts/diag_and_manual_restart.sh index ef33cba..3800adb 100644 --- a/scripts/diag_and_manual_restart.sh +++ b/scripts/diag_and_manual_restart.sh @@ -84,20 +84,20 @@ seed_server_dlp_events() { local ts ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" ansible -i "$INVENTORY" aw_server -b -m ansible.builtin.shell -a "cat >/tmp/aw-endpoint-seed.json <<'JSON' -{\"timestamp\":\"${ts}\",\"duration\":0.0,\"data\":{\"hostname\":\"SHARKON2025\",\"signalType\":\"self_test\",\"source\":\"diag_and_manual_restart\",\"username\":\"system\",\"queueDepth\":0,\"eventsEnqueued\":0,\"eventsFlushed\":0,\"sendFailures\":0}} +{\"timestamp\":\"${ts}\",\"duration\":0.0,\"data\":{\"hostname\":\"HOST-EXAMPLE\",\"signalType\":\"self_test\",\"source\":\"diag_and_manual_restart\",\"username\":\"system\",\"queueDepth\":0,\"eventsEnqueued\":0,\"eventsFlushed\":0,\"sendFailures\":0}} JSON cat >/tmp/aw-fileops-seed-host.json <<'JSON' -{\"timestamp\":\"${ts}\",\"duration\":0.0,\"data\":{\"hostname\":\"SHARKON2025\",\"operation\":\"self_test\",\"source\":\"diag_and_manual_restart\"}} +{\"timestamp\":\"${ts}\",\"duration\":0.0,\"data\":{\"hostname\":\"HOST-EXAMPLE\",\"operation\":\"self_test\",\"source\":\"diag_and_manual_restart\"}} JSON cat >/tmp/aw-fileops-seed-server.json <<'JSON' -{\"timestamp\":\"${ts}\",\"duration\":0.0,\"data\":{\"hostname\":\"10.10.10.13\",\"operation\":\"self_test\",\"source\":\"diag_and_manual_restart\"}} +{\"timestamp\":\"${ts}\",\"duration\":0.0,\"data\":{\"hostname\":\"192.0.2.13\",\"operation\":\"self_test\",\"source\":\"diag_and_manual_restart\"}} JSON -curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-dlp-endpoint-signals_SHARKON2025' -H 'Content-Type: application/json' -d '{\"client\":\"aw-dlp-endpoint-signals\",\"type\":\"aw.dlp.endpoint.signal\",\"hostname\":\"SHARKON2025\"}' >/dev/null 2>&1 || true -curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_SHARKON2025' -H 'Content-Type: application/json' -d '{\"client\":\"aw-file-operations\",\"type\":\"aw.file.operation\",\"hostname\":\"SHARKON2025\"}' >/dev/null 2>&1 || true -curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_10.10.10.13' -H 'Content-Type: application/json' -d '{\"client\":\"aw-file-operations\",\"type\":\"aw.file.operation\",\"hostname\":\"10.10.10.13\"}' >/dev/null 2>&1 || true -curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-dlp-endpoint-signals_SHARKON2025/heartbeat?pulsetime=30' -H 'Content-Type: application/json' --data-binary @/tmp/aw-endpoint-seed.json >/dev/null -curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_SHARKON2025/heartbeat?pulsetime=30' -H 'Content-Type: application/json' --data-binary @/tmp/aw-fileops-seed-host.json >/dev/null -curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_10.10.10.13/heartbeat?pulsetime=30' -H 'Content-Type: application/json' --data-binary @/tmp/aw-fileops-seed-server.json >/dev/null +curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-dlp-endpoint-signals_HOST-EXAMPLE' -H 'Content-Type: application/json' -d '{\"client\":\"aw-dlp-endpoint-signals\",\"type\":\"aw.dlp.endpoint.signal\",\"hostname\":\"HOST-EXAMPLE\"}' >/dev/null 2>&1 || true +curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_HOST-EXAMPLE' -H 'Content-Type: application/json' -d '{\"client\":\"aw-file-operations\",\"type\":\"aw.file.operation\",\"hostname\":\"HOST-EXAMPLE\"}' >/dev/null 2>&1 || true +curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_192.0.2.13' -H 'Content-Type: application/json' -d '{\"client\":\"aw-file-operations\",\"type\":\"aw.file.operation\",\"hostname\":\"192.0.2.13\"}' >/dev/null 2>&1 || true +curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-dlp-endpoint-signals_HOST-EXAMPLE/heartbeat?pulsetime=30' -H 'Content-Type: application/json' --data-binary @/tmp/aw-endpoint-seed.json >/dev/null +curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_HOST-EXAMPLE/heartbeat?pulsetime=30' -H 'Content-Type: application/json' --data-binary @/tmp/aw-fileops-seed-host.json >/dev/null +curl -sS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-file-operations_192.0.2.13/heartbeat?pulsetime=30' -H 'Content-Type: application/json' --data-binary @/tmp/aw-fileops-seed-server.json >/dev/null " >/dev/null } @@ -108,7 +108,7 @@ restart_windows_collectors() { seed_windows_dlp_events() { log "Seeding endpoint/file-ops events from aw_windows..." - ansible -i "$INVENTORY" aw_windows -m ansible.windows.win_shell -a "powershell -NoProfile -ExecutionPolicy Bypass -Command \"\$ErrorActionPreference = 'Stop'; \$ts = (Get-Date).ToUniversalTime().ToString('o'); \$api='http://10.10.10.13:5600/api/0'; \$endpoint=@{timestamp=\$ts;duration=0.0;data=@{hostname='SHARKON2025';signalType='self_test';source='diag_and_manual_restart';username=\$env:USERNAME;queueDepth=0;eventsEnqueued=0;eventsFlushed=0;sendFailures=0}} | ConvertTo-Json -Depth 8 -Compress; \$fileops=@{timestamp=\$ts;duration=0.0;data=@{hostname='SHARKON2025';operation='self_test';source='diag_and_manual_restart';username=\$env:USERNAME}} | ConvertTo-Json -Depth 8 -Compress; Invoke-RestMethod -Method Post -Uri \$api'/buckets/aw-dlp-endpoint-signals_SHARKON2025' -ContentType 'application/json' -Body '{\\\"client\\\":\\\"aw-dlp-endpoint-signals\\\",\\\"type\\\":\\\"aw.dlp.endpoint.signal\\\",\\\"hostname\\\":\\\"SHARKON2025\\\"}' -TimeoutSec 15 -DisableKeepAlive -ErrorAction SilentlyContinue | Out-Null; Invoke-RestMethod -Method Post -Uri \$api'/buckets/aw-file-operations_SHARKON2025' -ContentType 'application/json' -Body '{\\\"client\\\":\\\"aw-file-operations\\\",\\\"type\\\":\\\"aw.file.operation\\\",\\\"hostname\\\":\\\"SHARKON2025\\\"}' -TimeoutSec 15 -DisableKeepAlive -ErrorAction SilentlyContinue | Out-Null; Invoke-RestMethod -Method Post -Uri \$api'/buckets/aw-dlp-endpoint-signals_SHARKON2025/heartbeat?pulsetime=30' -ContentType 'application/json' -Body \$endpoint -TimeoutSec 15 -DisableKeepAlive | Out-Null; Invoke-RestMethod -Method Post -Uri \$api'/buckets/aw-file-operations_SHARKON2025/heartbeat?pulsetime=30' -ContentType 'application/json' -Body \$fileops -TimeoutSec 15 -DisableKeepAlive | Out-Null; Write-Output 'windows-dlp-seeded'\"" + ansible -i "$INVENTORY" aw_windows -m ansible.windows.win_shell -a "powershell -NoProfile -ExecutionPolicy Bypass -Command \"\$ErrorActionPreference = 'Stop'; \$ts = (Get-Date).ToUniversalTime().ToString('o'); \$api='http://192.0.2.13:5600/api/0'; \$endpoint=@{timestamp=\$ts;duration=0.0;data=@{hostname='HOST-EXAMPLE';signalType='self_test';source='diag_and_manual_restart';username=\$env:USERNAME;queueDepth=0;eventsEnqueued=0;eventsFlushed=0;sendFailures=0}} | ConvertTo-Json -Depth 8 -Compress; \$fileops=@{timestamp=\$ts;duration=0.0;data=@{hostname='HOST-EXAMPLE';operation='self_test';source='diag_and_manual_restart';username=\$env:USERNAME}} | ConvertTo-Json -Depth 8 -Compress; Invoke-RestMethod -Method Post -Uri \$api'/buckets/aw-dlp-endpoint-signals_HOST-EXAMPLE' -ContentType 'application/json' -Body '{\\\"client\\\":\\\"aw-dlp-endpoint-signals\\\",\\\"type\\\":\\\"aw.dlp.endpoint.signal\\\",\\\"hostname\\\":\\\"HOST-EXAMPLE\\\"}' -TimeoutSec 15 -DisableKeepAlive -ErrorAction SilentlyContinue | Out-Null; Invoke-RestMethod -Method Post -Uri \$api'/buckets/aw-file-operations_HOST-EXAMPLE' -ContentType 'application/json' -Body '{\\\"client\\\":\\\"aw-file-operations\\\",\\\"type\\\":\\\"aw.file.operation\\\",\\\"hostname\\\":\\\"HOST-EXAMPLE\\\"}' -TimeoutSec 15 -DisableKeepAlive -ErrorAction SilentlyContinue | Out-Null; Invoke-RestMethod -Method Post -Uri \$api'/buckets/aw-dlp-endpoint-signals_HOST-EXAMPLE/heartbeat?pulsetime=30' -ContentType 'application/json' -Body \$endpoint -TimeoutSec 15 -DisableKeepAlive | Out-Null; Invoke-RestMethod -Method Post -Uri \$api'/buckets/aw-file-operations_HOST-EXAMPLE/heartbeat?pulsetime=30' -ContentType 'application/json' -Body \$fileops -TimeoutSec 15 -DisableKeepAlive | Out-Null; Write-Output 'windows-dlp-seeded'\"" } confirm_restart() { diff --git a/scripts/install_aw_console_ssh_logger.sh b/scripts/install_aw_console_ssh_logger.sh index c12bafb..0a91f85 100755 --- a/scripts/install_aw_console_ssh_logger.sh +++ b/scripts/install_aw_console_ssh_logger.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh set -eu -SERVER_HOST="10.10.10.13" +SERVER_HOST="192.0.2.13" SERVER_PORT="5600" POLL_INTERVAL="5" INSTALL_ROOT="${HOME}/.local/opt/aw-console-ssh-logger" @@ -50,7 +50,7 @@ usage() { Usage: install_aw_console_ssh_logger.sh [options] Options: - --server-host HOST AW server host (default: 10.10.10.13) + --server-host HOST AW server host (default: 192.0.2.13) --server-port PORT AW server port (default: 5600) --poll-interval SEC Poll interval for history/session tracking (default: 5) -h, --help Show this help diff --git a/scripts/install_aw_linux_client.sh b/scripts/install_aw_linux_client.sh index 19223d9..a22175b 100755 --- a/scripts/install_aw_linux_client.sh +++ b/scripts/install_aw_linux_client.sh @@ -2,7 +2,7 @@ set -eu VERSION="0.13.2" -SERVER_HOST="10.10.10.13" +SERVER_HOST="192.0.2.13" SERVER_PORT="5600" INSTALL_BASE="${HOME}/.local/opt/activitywatch" BIN_DIR="${HOME}/.local/bin" @@ -48,7 +48,7 @@ usage() { Usage: install_aw_linux_client.sh [options] Options: - --server-host HOST Remote AW server host (default: 10.10.10.13) + --server-host HOST Remote AW server host (default: 192.0.2.13) --server-port PORT Remote AW server port (default: 5600) --version VERSION ActivityWatch version (default: 0.13.2) --install-base PATH Install root (default: ~/.local/opt/activitywatch) diff --git a/scripts/install_aw_linux_remote_worker.sh b/scripts/install_aw_linux_remote_worker.sh index d3865a5..e044629 100644 --- a/scripts/install_aw_linux_remote_worker.sh +++ b/scripts/install_aw_linux_remote_worker.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh set -eu -SERVER_HOST="10.10.10.13" +SERVER_HOST="192.0.2.13" SERVER_PORT="5600" POLL_INTERVAL="5" AW_VERSION="0.13.2" @@ -44,7 +44,7 @@ usage() { Usage: install_aw_linux_remote_worker.sh [options] Options: - --server-host HOST Remote AW server host (default: 10.10.10.13) + --server-host HOST Remote AW server host (default: 192.0.2.13) --server-port PORT Remote AW server port (default: 5600) --poll-interval SEC Poll interval for Linux loggers (default: 5) --version VERSION ActivityWatch version for GUI watcher bundle (default: 0.13.2) diff --git a/scripts/install_aw_linux_web_category_logger.sh b/scripts/install_aw_linux_web_category_logger.sh index b58d641..46dfff6 100644 --- a/scripts/install_aw_linux_web_category_logger.sh +++ b/scripts/install_aw_linux_web_category_logger.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh set -eu -SERVER_HOST="10.10.10.13" +SERVER_HOST="192.0.2.13" SERVER_PORT="5600" POLL_INTERVAL="5" INSTALL_ROOT="${HOME}/.local/opt/aw-linux-web-category" @@ -49,7 +49,7 @@ usage() { Usage: install_aw_linux_web_category_logger.sh [options] Options: - --server-host HOST AW server host (default: 10.10.10.13) + --server-host HOST AW server host (default: 192.0.2.13) --server-port PORT AW server port (default: 5600) --poll-interval SEC Poll interval in seconds (default: 5) -h, --help Show this help diff --git a/scripts/install_aw_pve_webadmin_logger.sh b/scripts/install_aw_pve_webadmin_logger.sh index 6fcc196..1ec896f 100755 --- a/scripts/install_aw_pve_webadmin_logger.sh +++ b/scripts/install_aw_pve_webadmin_logger.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh set -eu -SERVER_HOST="10.10.10.13" +SERVER_HOST="192.0.2.13" SERVER_PORT="5600" POLL_INTERVAL="5" INSTALL_ROOT="/opt/aw-pve-webadmin-logger" @@ -48,7 +48,7 @@ usage() { Usage: install_aw_pve_webadmin_logger.sh [options] Options: - --server-host HOST AW server host (default: 10.10.10.13) + --server-host HOST AW server host (default: 192.0.2.13) --server-port PORT AW server port (default: 5600) --poll-interval SEC Poll interval in seconds (default: 5) -h, --help Show this help diff --git a/scripts/install_detmir_powershell_mcp.sh b/scripts/install_detmir_powershell_mcp.sh index 288bb79..35d6de3 100644 --- a/scripts/install_detmir_powershell_mcp.sh +++ b/scripts/install_detmir_powershell_mcp.sh @@ -59,7 +59,7 @@ if [ ! -f "$LOCAL_CFG" ]; then if [ -n "$host" ] || [ -n "$user" ] || [ -n "$password" ]; then cat > "$LOCAL_CFG" <&1) +RESP=$(no_proxy=192.0.2.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/info" 2>&1) if [ $? -eq 0 ] && echo "$RESP" | jq -e '.version' > /dev/null 2>&1; then VERSION=$(echo "$RESP" | jq -r '.version') echo -e "${GREEN}OK${NC} (aw-server $VERSION)" @@ -95,7 +95,7 @@ fi echo "" # Context for inactive/event-driven classification. -WORKTIME_EVENT_DATA=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/aw-worktime-sessions_$HOSTNAME_FILTER/events?limit=1" 2>&1) +WORKTIME_EVENT_DATA=$(no_proxy=192.0.2.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/aw-worktime-sessions_$HOSTNAME_FILTER/events?limit=1" 2>&1) WORKTIME_TS=$(echo "$WORKTIME_EVENT_DATA" | jq -r '.[0].timestamp // ""' 2>/dev/null) WORKTIME_ACTIVE=$(echo "$WORKTIME_EVENT_DATA" | jq -r '.[0].data.active // false' 2>/dev/null) if [ -n "$WORKTIME_TS" ]; then @@ -108,7 +108,7 @@ if [ -n "$WORKTIME_TS" ]; then fi fi -GUARD_EVENT_DATA=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/aw-rus-collector-guard_$HOSTNAME_FILTER/events?limit=1" 2>&1) +GUARD_EVENT_DATA=$(no_proxy=192.0.2.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/aw-rus-collector-guard_$HOSTNAME_FILTER/events?limit=1" 2>&1) GUARD_TS=$(echo "$GUARD_EVENT_DATA" | jq -r '.[0].timestamp // ""' 2>/dev/null) GUARD_STATUS=$(echo "$GUARD_EVENT_DATA" | jq -r '.[0].data.status // ""' 2>/dev/null) GUARD_PROBLEMS=$(echo "$GUARD_EVENT_DATA" | jq -r '([.[0].data.problems[]?] | length) // 0' 2>/dev/null) @@ -142,7 +142,7 @@ for bucket in "${BUCKETS[@]}"; do bucket_full="${bucket}_${HOSTNAME_FILTER}" # Получаем последний event - EVENT_DATA=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/$bucket_full/events?limit=1" 2>&1) + EVENT_DATA=$(no_proxy=192.0.2.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/buckets/$bucket_full/events?limit=1" 2>&1) LAST_ID=$(echo "$EVENT_DATA" | jq '.[0].id // 0') LAST_TS=$(echo "$EVENT_DATA" | jq -r '.[0].timestamp // "no events"') @@ -176,7 +176,7 @@ echo "" # Проверка CORS echo "--- CORS Check ---" -CORS_RESP=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 -o /dev/null -w '%{http_code}' -H "Origin: http://10.10.10.13:5600" "$SERVER/api/0/settings/" 2>&1) +CORS_RESP=$(no_proxy=192.0.2.13 curl -s --connect-timeout 10 --max-time 15 -o /dev/null -w '%{http_code}' -H "Origin: http://192.0.2.13:5600" "$SERVER/api/0/settings/" 2>&1) if [ "$CORS_RESP" = "200" ]; then echo -e "${GREEN}CORS: OK${NC} (HTTP 200)" else diff --git a/scripts/metagpt-aw-scout.sh b/scripts/metagpt-aw-scout.sh index c6c5a85..50c2507 100644 --- a/scripts/metagpt-aw-scout.sh +++ b/scripts/metagpt-aw-scout.sh @@ -49,7 +49,7 @@ case "$1" in TASK="Prepare a rebuild and validation checklist for install-kit-awindows, including rebuild_install_kit.sh, check_install_kit_vs_repo.sh, validate_install_kit.sh, and optional InnoSetup exe rebuild." ;; windows-i18n) - TASK="Prepare a validation checklist for Windows localized account names and Cyrillic handling: CP866 query.exe decoding, SHARKON2025_Администратор scheduled task, WinRM output, SSH checks, and ActivityWatch Launch fallback rules." + TASK="Prepare a validation checklist for Windows localized account names and Cyrillic handling: CP866 query.exe decoding, HOST-EXAMPLE_Администратор scheduled task, WinRM output, SSH checks, and ActivityWatch Launch fallback rules." ;; *) TASK="$*" @@ -85,14 +85,14 @@ Hard constraints: - Assume Groq/free-tier constraints: keep the response compact. Known operational facts: -- AW server: http://10.10.10.13:5600 -- Worktime API: http://10.10.10.13:5610 -- Grafana: http://10.10.10.11:3000 -- RDP host: 192.168.100.18 / SHARKON2025 +- AW server: http://192.0.2.13:5600 +- Worktime API: http://192.0.2.13:5610 +- Grafana: http://192.0.2.11:3000 +- RDP host: 198.51.100.18 / HOST-EXAMPLE - Ansible inventory: ansible/inventory.ini - Windows deploy root: C:\\Program Files\\AWatch-rus - Windows state root: C:\\ProgramData\\AWatch-rus -- Localized built-in Administrator launch task: ActivityWatch Launch [SHARKON2025_Администратор] +- Localized built-in Administrator launch task: ActivityWatch Launch [HOST-EXAMPLE_Администратор] - For localized Windows console output, prefer query.exe user via CP866 decoding and emit UTF-8. Task: diff --git a/scripts/powershell/detmir-powershell-profile.ps1 b/scripts/powershell/detmir-powershell-profile.ps1 index 7ee45ff..bbb2e48 100644 --- a/scripts/powershell/detmir-powershell-profile.ps1 +++ b/scripts/powershell/detmir-powershell-profile.ps1 @@ -40,7 +40,7 @@ function Get-DetMirWindowsSetting { return $Default } -$Global:DetMirWindowsHost = Get-DetMirWindowsSetting -EnvName 'DETMIR_WINDOWS_SSH_HOST' -ConfigKey 'Host' -Default '192.168.100.18' +$Global:DetMirWindowsHost = Get-DetMirWindowsSetting -EnvName 'DETMIR_WINDOWS_SSH_HOST' -ConfigKey 'Host' -Default '198.51.100.18' $Global:DetMirWindowsUser = Get-DetMirWindowsSetting -EnvName 'DETMIR_WINDOWS_SSH_USER' -ConfigKey 'User' -Default 'Администратор' $Global:DetMirWindowsPassword = Get-DetMirWindowsSetting -EnvName 'DETMIR_WINDOWS_SSH_PASSWORD' -ConfigKey 'Password' $Global:DetMirWindowsPort = [int](Get-DetMirWindowsSetting -EnvName 'DETMIR_WINDOWS_SSH_PORT' -ConfigKey 'Port' -Default '22') diff --git a/scripts/powershell/detmir-windows.psd1.example b/scripts/powershell/detmir-windows.psd1.example index ebc3acd..c214e60 100644 --- a/scripts/powershell/detmir-windows.psd1.example +++ b/scripts/powershell/detmir-windows.psd1.example @@ -1,5 +1,5 @@ @{ - Host = '192.168.100.18' + Host = '198.51.100.18' User = 'Администратор' Password = 'CHANGE_ME' Port = 22 diff --git a/scripts/prod_rollout.sh b/scripts/prod_rollout.sh index 9271d66..c929179 100644 --- a/scripts/prod_rollout.sh +++ b/scripts/prod_rollout.sh @@ -88,8 +88,8 @@ if [[ ! -f ansible/inventory.ini ]]; then 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.18)" + prompt_secret AW_SSH_PASSWORD "Enter SSH password for aw_server (root@192.0.2.13)" + prompt_secret AW_WINRM_PASSWORD "Enter WinRM password for aw_windows (198.51.100.18)" fi if [[ -z "${AW_SSH_PASSWORD:-}" || -z "${AW_WINRM_PASSWORD:-}" ]]; then diff --git a/scripts/rdp-worktime-report.sh b/scripts/rdp-worktime-report.sh index 371ba0e..2f43889 100644 --- a/scripts/rdp-worktime-report.sh +++ b/scripts/rdp-worktime-report.sh @@ -4,8 +4,8 @@ set -euo pipefail DAY="" FROM="" TO="" -AW_BASE_URL="${AW_BASE_URL:-http://10.10.10.13:5600/api/0}" -AW_WORKTIME_HOST="${AW_WORKTIME_HOST:-SHARKON2025}" +AW_BASE_URL="${AW_BASE_URL:-http://192.0.2.13:5600/api/0}" +AW_WORKTIME_HOST="${AW_WORKTIME_HOST:-HOST-EXAMPLE}" AW_WORKTIME_DEFAULT_SAMPLE_SECONDS="${AW_WORKTIME_DEFAULT_SAMPLE_SECONDS:-30}" AW_WORKTIME_MAX_SAMPLE_SECONDS="${AW_WORKTIME_MAX_SAMPLE_SECONDS:-300}" OUT_DIR="${OUT_DIR:-reports}" @@ -80,7 +80,7 @@ import urllib.request from datetime import datetime, timedelta, timezone base, host, default_sample, max_sample, from_d, to_d, csv_out, json_out = sys.argv[1:9] -base = (base or "http://10.10.10.13:5600").rstrip("/") +base = (base or "http://192.0.2.13:5600").rstrip("/") if not base.endswith("/api/0"): base = base + "/api/0" default_sample = max(1.0, float(default_sample)) diff --git a/windows/ActivityWatch.Windows.Common.psm1 b/windows/ActivityWatch.Windows.Common.psm1 index 053a497..34225ed 100755 --- a/windows/ActivityWatch.Windows.Common.psm1 +++ b/windows/ActivityWatch.Windows.Common.psm1 @@ -312,7 +312,7 @@ function Get-ActivityWatchBuiltInAdministratorName { catch { } - if ([string]$env:COMPUTERNAME -ieq 'SHARKON2025') { + if ([string]$env:COMPUTERNAME -ieq 'HOST-EXAMPLE') { $script:ActivityWatchBuiltInAdministratorName = 'Администратор' return $script:ActivityWatchBuiltInAdministratorName } diff --git a/windows/aw-collector-guard.ps1 b/windows/aw-collector-guard.ps1 index 559995e..9d59022 100644 --- a/windows/aw-collector-guard.ps1 +++ b/windows/aw-collector-guard.ps1 @@ -322,18 +322,18 @@ function Invoke-GuardSelfTest { $oldComputerName = $env:COMPUTERNAME try { - $env:COMPUTERNAME = 'SHARKON2025' + $env:COMPUTERNAME = 'HOST-EXAMPLE' $sessionRecords = @( [pscustomobject]@{ SessionName = 'USER5'; UserName = 'USER5'; SessionId = 2; State = 'Disc'; IsLive = $false }, [pscustomobject]@{ SessionName = 'console'; UserName = ''; SessionId = 1; State = 'Conn'; IsLive = $true } ) $taskDefs = @( - [pscustomobject]@{ taskName = 'ActivityWatch Launch [SHARKON2025_user5]'; userId = 'SHARKON2025\user5' } + [pscustomobject]@{ taskName = 'ActivityWatch Launch [HOST-EXAMPLE_user5]'; userId = 'HOST-EXAMPLE\user5' } ) - if (-not (Test-ActivityWatchUserHasManagedSession -UserId 'SHARKON2025\user5' -SessionRecords $sessionRecords -IncludeDisconnected)) { + if (-not (Test-ActivityWatchUserHasManagedSession -UserId 'HOST-EXAMPLE\user5' -SessionRecords $sessionRecords -IncludeDisconnected)) { throw 'expected disconnected managed session to match task user' } - if (Test-ActivityWatchUserHasManagedSession -UserId 'SHARKON2025\user5' -SessionRecords $sessionRecords -IncludeLive) { + if (Test-ActivityWatchUserHasManagedSession -UserId 'HOST-EXAMPLE\user5' -SessionRecords $sessionRecords -IncludeLive) { throw 'disconnected managed session should not match live-only filter' } $managed = @(Get-ActivityWatchManagedInteractiveSessions -TaskDefinitions $taskDefs -SessionRecords $sessionRecords -IncludeDisconnected) diff --git a/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss b/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss index b510572..b2da005 100644 --- a/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss +++ b/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss @@ -5,7 +5,7 @@ #define AwDefaultServerHost "aw-server" #define AwDefaultServerPort "5600" #define AwDefaultWorktimeReportBase "http://aw-server:5610" -#define AwDefaultWorktimeHost "SHARKON2025" +#define AwDefaultWorktimeHost "HOST-EXAMPLE" #define AwDefaultUsers "user1,user2,user3,user4,user5" #define AwDefaultInstallRoot "C:\\Program Files\\AWatch-rus\\bin" #define AwDefaultStateRoot "C:\\ProgramData\\AWatch-rus" @@ -13,7 +13,7 @@ ; This installer wraps the standalone-service path. ; It is suitable for standalone/headless deployment and must not be treated -; as the canonical multi-user RDP deployment path used on SHARKON2025. +; as the canonical multi-user RDP deployment path used on HOST-EXAMPLE. [Setup] AppId={{6D6A1F74-0F4F-4A57-B5E3-1C2C2F56C0E9} diff --git a/windows/run-user1-probe.ps1 b/windows/run-user1-probe.ps1 index 641250a..3ef5fbb 100644 --- a/windows/run-user1-probe.ps1 +++ b/windows/run-user1-probe.ps1 @@ -1,6 +1,6 @@ [CmdletBinding()] param( - [string]$UserId = 'SHARKON2025\user1' + [string]$UserId = 'HOST-EXAMPLE\user1' ) Set-StrictMode -Version Latest @@ -13,7 +13,7 @@ Start-Sleep -Seconds 10 Get-Process notepad -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue '@ | Set-Content -LiteralPath $probeScriptPath -Encoding UTF8 -schtasks /Run /TN 'ActivityWatch Launch [SHARKON2025_user1]' | Out-Null +schtasks /Run /TN 'ActivityWatch Launch [HOST-EXAMPLE_user1]' | Out-Null Start-Sleep -Seconds 3 $taskName = 'AW User1 Notepad Probe'