chore(install-kit): rebuild awindows package

This commit is contained in:
igor04091968
2026-05-29 06:46:19 +03:00
parent e4a25ab9c0
commit 09e58b4170
30 changed files with 4534 additions and 452 deletions
+1
View File
@@ -0,0 +1 @@
windows/installkit/innosetup/AWatch-rus-InstallKit.exe filter=lfs diff=lfs merge=lfs -text
Binary file not shown.
Binary file not shown.
@@ -1,47 +1,47 @@
f3dde1e6d1532804379faf7e395deaf95cf3e0b97769d425f8a69f4572de2a2f install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt
a08ccceada7a21e4396a640e54a354e4d4d760980ec3f18f1bc7f543cd8f4cc6 install-kit-awindows-20260427-211240/ansible/README.md
edc20460f7be2ec676a6fd7c9687f808507b364b32915eb78b7a63b441b1ff84 install-kit-awindows-20260427-211240/ansible/README.md
412bb766bbf0791c3593f38daa771d5d0aa58cc1f2d3c9010fcd4588d0fe87df install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml
00d16de62df9d91cd375cbe70034ec97da14601ab5285ca93e5b297150f02d34 install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml
ce1cbc35087006292e93a7e0d1706bc0ac71f8d9f2e60bbdc1c3a8ecea0d34f0 install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml
eedb12a2be920c6bec267162c91e106365af5c009426cefe84c032c2f9d9339d install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml
95696c243ab331f06e77a40a9800c4b6668de77675ebbdf2ef54ae49e1b18874 install-kit-awindows-20260427-211240/ansible/group_vars/pfsense-poller.example.yml
0188480546b4b18937194f2b05a0f23da5dd8536eed4d60b7499a8170c36d08e install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml
953bb39572c520d0438a621c2b4c08315bf887b84ee9e795554e5592b54a1229 install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml
0054e7ba5f342cd10db08f6bab4784d4816179d69f76898a4de293a4063ea14a install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml
a896676303be9a47ed6b0c8cc07deb5a5432aebc97231e801fda985121c539eb 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
5da847b74fac52e0fec2f60e134f4377cfa1581e026b291d3d3ac362371f6e49 install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml
a1921715cf9904b7e3b61a1a5c4300382efe2f3bd70d73e4c8e995318fdd55e9 install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml
7189b5205bd25313db54e5be027b0d066199e6ae34ad74be2095a1691adaf5e3 install-kit-awindows-20260427-211240/ansible/install_full_stack.yml
fea0574d7eb98ce24a1e7025afb9837c6241180095d21eaa74892225305d05a3 install-kit-awindows-20260427-211240/ansible/inventory.example.ini
8a5e4923c0f581dd4fbb32549ee7ab45ba506260056da923ba86d9f1b1081714 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml
18928adcaef5d01b4c621b48f5559383c8b749ef182fd3710f10b222a164f8b7 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
bed7495c277970a37d1c467e81592417f955914d4c714ac21397083b56f1bba8 install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh
db81f6209e14f2efd123cd91fac74e37a68e5ef54d00b8b6f6612404a78ffdaa install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json
6eafd2d7a43a9fd146fdc6686ae306cb92abfa2c273bbb1ed03b67fb1277defe install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js
9e6254c726dd4a26578a60b6bf5acee84066f931ae395115a80cf622a6ff2732 install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.py
fea5734c99b516b01bfe8ebbef59972d0ef553c09d7691790b08472ecfb9602d install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh
ba88cc284d047f521427ac038c266624a6fe8493ce3e79bc27c172a2e70ac54a install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json
509759461ce0918a2dc839832812cf5f4c77b1cea1e6ffdcab0146e02598df79 install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js
22d4ee27ffc73dcec3ba73c8ee65d12353ded1403f592ff52ef578e09dad4c43 install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.py
05b04b5f49e9c7783917e0861e3edd63f5d6db8638d6b33dbfd4dc0f1c16040f install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.service
bb0f1de91862da66b0b6d9bd41e8dfe181710196141cb43b00d9b41dab6caf96 install-kit-awindows-20260427-211240/aw-server/aw-rus-healthd.timer
0d2b978833b27a2a041508b49ffd07ca045127ddf3b09c71d3787d0bf1224473 install-kit-awindows-20260427-211240/aw-server/aw-server.env.example
b493fbf4cc15ccba44ea78d3ed17a0a6db31617f22d6aeb671677cbf11a36921 install-kit-awindows-20260427-211240/aw-server/aw-server.env.example
98c0bed353bbda0fa7a69df23f3b008cb0e8e70cdff6cc63330d4caf79fd3280 install-kit-awindows-20260427-211240/aw-server/aw-sw-cleanup.js
d8d6be450a726f51f87415eaebd67396468c73ddda9b942481d56b4d33d68bb7 install-kit-awindows-20260427-211240/aw-server/aw-worktime-api.py
e220a2eb830a018fa28c02872fabda54aed7f87dbb0e14141f1a8933a98064fe install-kit-awindows-20260427-211240/aw-server/aw-worktime-api.py
f764e566d70952acc1b4f2baeccd6b7888905bc6b98b36333336c0dc77c66694 install-kit-awindows-20260427-211240/aw-server/aw-worktime-api.service
8af41d20f01dfffe6b8c64bd5dbe24468f297035760050647bfeb53b704c0d4d install-kit-awindows-20260427-211240/aw-server/aw-worktime-panel.js
3262b356dc4cd940b66f27d47cfe437dcd26d8d2cb49d1dc0a00b933e380a376 install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh
afbb4be301b4940ed9b7671be3441a48d53bee968d8870749442f81c6d066650 install-kit-awindows-20260427-211240/aw-server/aw-worktime-panel.js
ca7d1ed7665e225824d4ed7ef8456c81a6d97ef2c90cb3aa799d72e07b6bcbb8 install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh
aedffecfa24834968742cb2477faef80bf794345275a9679ac12c5a1f609acc2 install-kit-awindows-20260427-211240/aw-server/settings/classes-worktime.json
38fd98fe5816fb87055a9ad1fc570a65052785c829ecd8359fe8305949ab32ca install-kit-awindows-20260427-211240/aw-server/settings/views-default.json
47c50054515506b72af2d6bca0ae959ca57861803e528991c230611e6a8893d6 install-kit-awindows-20260427-211240/aw-server/settings/views-default.json
dd2389e9cb199ed86d219120294fdaa64415cfbc683004d5f5db5d52aa758a92 install-kit-awindows-20260427-211240/server-configs-192.168.100.18/awatch-rus-admin.deployment-config.json
5279f9d677faed76a5f0248f9217ecc29eac977ab752552cab852f5b4b6715df install-kit-awindows-20260427-211240/server-configs-192.168.100.18/awatch-rus-u2u5.deployment-config.json
333fe336e51f9c69bd2559d18763da2b83df400fa374e540ed128ffb6765ab7e install-kit-awindows-20260427-211240/server-configs-192.168.100.18/awatch-rus-user1.deployment-config.json
33aa34b89246d6c079ef9afe2f5cd153bd9d5946b69a175ff6fd678c77f61da5 install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1
0e5ac8bc0571f154190202504e02710ac931b8015cb01ec93e82defed9fc2f4f install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1
130ae4c137f7d6951cd8c247c0a8fbf999f4c244e879c180e1441b781e758228 install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1
831edc097f0621ae940db5064c949b4a83ed6e4f25265875f0cdd6bba0ba4c51 install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1
f9992b3c9c075755e6ffcf82385b9abd01e768a0c3fb7fe01afae5d7b65d04db install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1
f940b40600d57f2d12f44a32aa88e5591b7569ce01c06911ff8f4e27b0e1649f install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1
35a0dcc90459d4af39998c8a8bd534826e0b463dfbf3f8547444a96204f0ef4b install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1
9e5e6a30bfb7789ee91ae656e306586a50509d9d416fc044408e9e6c19c61b37 install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1
81e95c7e4b7336a2e1f0caed3ea99f6cdb9696b99380dc2148667b5fba577f68 install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1
e390e00a4deff5ce45a08f5252f23c56b2655c8611e42bc7b2b6992f60bde038 install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1
2074d9ebdda069c8bd579e688473656259714fe7e13b7537a7c464143195b525 install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1
9f2637ede66b61a12e9edafa0b248a5fb496dd5175b288ebb71718330cac68d5 install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1
dc74947393b1851ca233b559ea77f29b1b12a1c4480fbb865f2b58b25f3ea7d3 install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1
a4dad0745da95a69ee55b0216d4bde39c58acef8642380092938465653c61cf4 install-kit-awindows-20260427-211240/windows/dlp-policy.example.json
863727465497b474d13d2270d443ff96ccb6076f90a5ce3eb270bdf8088e02dc install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1
e29fd9ed3510429372126d94c54beb30dc6424b5b22eb012829b99c3cb07ea60 install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1
44d941322d618d92c72c3d24fe619d71e7551b9cdb35abe19f700c0b74da3eb1 install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1
5ef21a25d5e2da4eeaef17126e60f96f195f90f9dc17a776f8629334b904d096 install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1
d01edd14b2c839ae171006fd3345dbddf1b683b1adcac885b6eabc52e3baeb79 install-kit-awindows-20260427-211240/windows/validate-deployment.ps1
672d76824b1ca17d4c65e3097cf9895160a7bbce30323ef02d195fc9624538da install-kit-awindows-20260427-211240/windows/validate-deployment.ps1
731098681d89b9af6f3872abd586ac3b1faba2d7f9340211e503f52ad0243b3f install-kit-awindows-20260427-211240/windows/web-category-rules.example.json
945ccfffd56697ed328b82e82d1cffbc83fa4e50a6120b27e3948f3d13fa8a33 install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1
1b7c337967236474484e781dc8ac37543509b051513f01e1a3145369262f5389 install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1
@@ -89,6 +89,12 @@ 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`
@@ -122,6 +128,7 @@ Playbook:
- выполняет API smoke-check bucket `aw-watcher-window_<COMPUTERNAME>` и ожидает свежие события (по умолчанию включено);
- запускает `validate-deployment.ps1`;
- забирает JSON-отчёт в локальную директорию (`/tmp/aw-rus-validation-<USER>` по умолчанию).
- настраивает scheduled task `ActivityWatch Hayabusa Upload` с периодом и lookback по vars.
Дополнительные флаги:
@@ -144,6 +151,38 @@ Playbook:
- `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
@@ -94,6 +94,89 @@
- "{{ aw_rus_health_validation_dir }}"
- "{{ aw_server_log_dir }}"
- 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: Ограничить рост 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."
@@ -115,6 +198,11 @@
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 }}"
@@ -288,6 +376,22 @@
dest: /opt/activitywatch/aw-server/apply_webui_ru_patch.sh
mode: "0755"
- name: Проверить Influx token для AW worktime exporter
ansible.builtin.assert:
that:
- aw_worktime_influx_token is defined
- aw_worktime_influx_token | length > 0
fail_msg: "aw_worktime_influx_enabled=true, но aw_worktime_influx_token пуст. Exporter будет падать и Grafana не получит worktime-ряды."
when: aw_worktime_influx_enabled | default(false) | bool
- name: Проверить Influx token для AW DLP exporter
ansible.builtin.assert:
that:
- aw_dlp_influx_token is defined
- aw_dlp_influx_token | length > 0
fail_msg: "aw_dlp_influx_enabled=true, но aw_dlp_influx_token пуст. 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
@@ -340,8 +444,16 @@
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_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:
@@ -914,7 +1026,6 @@
ansible.builtin.systemd:
name: aw-worktime-influx-exporter.service
state: started
failed_when: false
when: aw_worktime_influx_enabled | default(false) | bool
- name: Включить и перезапустить AW DLP Influx exporter timer
@@ -928,7 +1039,6 @@
ansible.builtin.systemd:
name: aw-dlp-influx-exporter.service
state: started
failed_when: false
when: aw_dlp_influx_enabled | default(false) | bool
- name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper)
@@ -1506,6 +1616,11 @@
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 }}"
@@ -1545,6 +1660,124 @@
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: Установить helper link-case для Hayabusa
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-link-case.py"
dest: /usr/local/bin/aw-hayabusa-link-case
owner: root
group: root
mode: "0755"
- name: Положить helper link-case в server-side ops bundle
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-link-case.py"
dest: /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa-link-case.py
owner: root
group: root
mode: "0755"
- name: Установить helper from-windows для Hayabusa
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-from-windows.py"
dest: /usr/local/bin/aw-hayabusa-from-windows
owner: root
group: root
mode: "0755"
- name: Положить helper from-windows в server-side ops bundle
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-from-windows.py"
dest: /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa-from-windows.py
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: Установить helper autoprocess для Hayabusa
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-autoprocess.py"
dest: /usr/local/bin/aw-hayabusa-autoprocess
owner: root
group: root
mode: "0755"
- name: Положить helper autoprocess в server-side ops bundle
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-autoprocess.py"
dest: /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa-autoprocess.py
owner: root
group: root
mode: "0755"
- name: Установить helper case-alert для Hayabusa
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-case-alert.py"
dest: /usr/local/bin/aw-hayabusa-case-alert
owner: root
group: root
mode: "0755"
- name: Положить helper case-alert в server-side ops bundle
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/hayabusa/aw-hayabusa-case-alert.py"
dest: /opt/activitywatch/aw-rus-ops/hayabusa/aw-hayabusa-case-alert.py
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
@@ -1572,6 +1805,7 @@
cmd: /usr/local/bin/aw-rus-healthd.py --json
register: aw_post_deploy_health
changed_when: false
failed_when: false
- name: Показать результат aw-rus-healthd
ansible.builtin.debug:
@@ -9,12 +9,12 @@
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
aw_windows_package_zip_path: ""
aw_windows_domain: "SHARKON2025"
aw_windows_builtin_administrator_name: "Администратор"
aw_windows_users:
- Администратор
- user1
@@ -29,9 +29,18 @@
aw_windows_policy_mode: "server"
aw_windows_policy_refresh_seconds: 300
aw_windows_policy_engine_enabled: true
aw_windows_policy_engine_host: "{{ aw_windows_server_host }}"
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
@@ -41,6 +50,7 @@
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: true
aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
@@ -62,16 +72,79 @@
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 is defined
- 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
@@ -102,11 +175,14 @@
- 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
- migrate-awatch-rus-paths.ps1
- deploy-domain-users.ps1
- deploy-ensemble.ps1
- hardening-recovery.ps1
- rebuild-worktime-tasks.ps1
- audit-cryptopro.ps1
- validate-deployment.ps1
- web-category-rules.example.json
- dlp-policy.example.json
@@ -156,9 +232,10 @@
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 }}"
ServerHost = "{{ aw_windows_server_host_effective }}"
ServerPort = {{ aw_windows_server_port }}
Version = "{{ aw_windows_package_version }}"
Domain = "{{ aw_windows_domain }}"
@@ -175,12 +252,24 @@
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 }}"
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 }}"
}
@@ -340,7 +429,7 @@
when:
- aw_windows_api_smoke_check_enabled | bool
ansible.builtin.uri:
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
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
@@ -367,7 +456,7 @@
- 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 }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_window_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
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
@@ -404,6 +493,26 @@
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'
@@ -8,7 +8,9 @@ aw_server_db_path: "/var/lib/activitywatch/.local/share/activitywatch/aw-server-
aw_server_log_dir: "/var/log/activitywatch"
aw_server_user: "activitywatch"
aw_server_group: "activitywatch"
aw_worktime_report_base: "http://10.10.10.13:5610"
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://10.10.10.10:8086"
@@ -27,8 +29,15 @@ aw_dlp_influx_event_limit: 2000
aw_dlp_influx_token: ""
aw_monitored_windows_host: "192.168.100.18"
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_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 }}"
@@ -41,7 +50,7 @@ aw_apply_worktime_settings: true
aw_server_cors_origins:
- "http://127.0.0.1:5600"
- "http://localhost:5600"
- "http://10.10.10.13:5600"
- "http://{{ aw_server_public_host }}:5600"
- "http://aw-server:5600"
# Опциональные значения периода рабочего времени в Web UI.
@@ -12,16 +12,16 @@ aw_pfsense_poller_config:
scheme: "https"
verify_tls: false
timeout_seconds: 15
headers:
X-API-Key: "replace-me"
X-API-Secret: "replace-me"
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/interface"
path: "/api/v2/interfaces"
bucket_prefix: "aw-pfsense-interfaces"
bucket_type: "aw.pfsense.interfaces"
- name: "gateways"
@@ -1,12 +1,22 @@
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
# 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
@@ -40,6 +50,7 @@ aw_windows_evtx_channels:
- Microsoft-Windows-TerminalServices-LocalSessionManager/Operational
- Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational
aw_windows_logon_marker_enabled: true
aw_windows_process_events_enabled: true
aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
@@ -10,7 +10,8 @@ fi
source "$ENV_FILE"
WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-${AW_WEBUI_DIR:-/opt/activitywatch/webui-ru}}"
REPORT_BASE="${AW_WORKTIME_REPORT_BASE:-http://10.10.10.13:5610}"
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"
@@ -62,12 +63,19 @@ worktime_panel_hash="$(sha1sum "$WORKTIME_PANEL_TARGET" | awk '{print substr($1,
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 "10.10.10.13"
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
@@ -47,13 +47,11 @@
{
"id": "virtual-infra",
"name": "Virtual servers + Proxmox",
"description": "Инфраструктурные VM и сетевые узлы. Здесь должны лежать Proxmox, pfSense, Debian и Ubuntu серверы.",
"description": "Инфраструктурные VM и серверы Proxmox, Debian и Ubuntu.",
"patterns": [
"^(PFSENSE|PVE|PROXMOX|DEBIAN|UBUNTU|LINUX|VM-|SRV-|INFRA-)"
"^(PVE|PROXMOX|DEBIAN|UBUNTU|LINUX|VM-|SRV-|INFRA-)"
],
"links": [
{ "label": "pfSense health", "type": "bucket", "bucket_prefix": "aw-pfsense-health_" },
{ "label": "pfSense gateways", "type": "bucket", "bucket_prefix": "aw-pfsense-gateways_" },
{ "label": "Все бакеты", "type": "buckets" }
]
}
@@ -3,8 +3,8 @@
return;
}
window.__awRuPatchBootstrapped = true;
window.__awRuPatchVersion = "template-v13-category-builder-early-fix";
document.documentElement.setAttribute("data-aw-ru-patch", "template-v13-category-builder-early-fix");
window.__awRuPatchVersion = "template-v14-dlp-route-lite";
document.documentElement.setAttribute("data-aw-ru-patch", "template-v14-dlp-route-lite");
const exact = new Map([
["ActivityWatch", "АктивВотч"],
@@ -24,6 +24,7 @@
["Tools", "Инструменты"],
["Raw Data", "Сырые данные"],
["Summary", "Сводка"],
["Worktime", "Рабочее время"],
["All", "Все"],
["None", "Нет"],
["Date", "Дата"],
@@ -236,6 +237,7 @@
['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", "Топ доменов"],
@@ -344,7 +346,16 @@
'.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-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);
}
@@ -370,6 +381,12 @@
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 || ""));
}
@@ -384,6 +401,230 @@
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 =
'<h4>RDP сводка</h4>' +
'<p>Этот блок строится из bucket <code>aw-worktime-sessions</code> через AW API и показывает сводку по RDP-сессиям выбранного хоста.</p>' +
'<div class="aw-ru-rdp-grid">' +
'<section class="aw-ru-rdp-card"><h5>Активное время</h5><div class="aw-ru-rdp-value">' + escapeHtml(formatDurationSeconds(totalActiveSeconds)) + '</div></section>' +
'<section class="aw-ru-rdp-card"><h5>Активных пользователей</h5><div class="aw-ru-rdp-value">' + escapeHtml(String(activeUsers.length)) + '</div></section>' +
'<section class="aw-ru-rdp-card"><h5>Последняя активность</h5><div class="aw-ru-rdp-value" style="font-size:16px;">' + escapeHtml(formatIsoForUi(latestActivity)) + '</div></section>' +
'</div>' +
'<table class="aw-ru-rdp-table">' +
'<thead><tr><th>Пользователь</th><th>Активное время</th><th>Первая активность</th><th>Последняя активность</th></tr></thead>' +
'<tbody>' +
(topRows.length ? topRows.map(function (row) {
return '<tr>' +
'<td>' + escapeHtml(row.user || row.user_id || "") + '</td>' +
'<td>' + escapeHtml(row.active_hhmm || formatDurationSeconds(row.active_seconds || 0)) + '</td>' +
'<td>' + escapeHtml(formatIsoForUi(row.first_activity || "")) + '</td>' +
'<td>' + escapeHtml(formatIsoForUi(row.last_activity || "")) + '</td>' +
'</tr>';
}).join("") : '<tr><td colspan="4">Нет активных пользователей в отчёте.</td></tr>') +
'</tbody>' +
'</table>' +
'<div class="aw-ru-rdp-links">' +
'<a href="' + escapeHtml(buildRdpReportUrl("html", day)) + '">HTML</a>' +
'<a href="' + escapeHtml(buildRdpReportUrl("csv", day)) + '">CSV</a>' +
'<a href="' + escapeHtml(buildRdpReportUrl("", day)) + '">JSON</a>' +
'</div>';
}
function enforceSafeActivityViewForPveHost() {
const hash = window.location.hash || "";
const match = hash.match(/^#\/activity\/([^/]+)(?:\/day\/([^/]+))?\/view\/([^/?#]+)/i);
@@ -605,9 +846,15 @@
}
function removeBadDlpLinks(root) {
const badLinks = root.querySelectorAll("a[href*='/view/DLP']");
badLinks.forEach(function (link) {
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();
});
}
@@ -718,11 +965,9 @@
{
id: "virtual-infra",
name: "Virtual servers + Proxmox",
description: "Инфраструктурные VM, Proxmox и сетевые узлы.",
patterns: ["^(PFSENSE|PVE|PROXMOX|DEBIAN|UBUNTU|LINUX|VM-|SRV-|INFRA-)"],
description: "Инфраструктурные VM и узлы Proxmox.",
patterns: ["^(PVE|PROXMOX|DEBIAN|UBUNTU|LINUX|VM-|SRV-|INFRA-)"],
links: [
{ label: "pfSense health", type: "bucket", bucket_prefix: "aw-pfsense-health_" },
{ label: "pfSense gateways", type: "bucket", bucket_prefix: "aw-pfsense-gateways_" },
{ label: "Все бакеты", type: "buckets" }
]
}
@@ -785,9 +1030,6 @@
"aw-pve-webadmin-events_",
"aw-pve-task-events_",
"aw-dlp-incidents_",
"aw-pfsense-health_",
"aw-pfsense-gateways_",
"aw-pfsense-interfaces_"
];
for (const prefix of prefixes) {
if (bucketId.indexOf(prefix) === 0) {
@@ -1129,7 +1371,9 @@
try {
await saveDlpReview(host, event, row);
state.reviews = collapseReviewEvents(await loadBucketEvents("aw-dlp-review_" + host, 200));
renderDlpTableRows(center, host);
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;
@@ -1170,8 +1414,17 @@
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 title = reportDir ? ' title="' + escapeHtml(reportDir) + '"' : "";
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 '<span' + title + '>Hayabusa host-mismatch · ' + escapeHtml(forensicHost) + '</span>';
}
return '<span' + title + '>Hayabusa ' + escapeHtml(status) + (mode ? " · " + escapeHtml(mode) : "") + '</span>';
}
const rows = (cases || []).map(function (c) {
@@ -1868,6 +2121,16 @@
});
}
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();
@@ -1887,24 +2150,13 @@
try {
const routeKey = window.location.hash || "#";
const routeChanged = routeKey !== staticPatchRouteKey;
enforceSafeActivityViewForPveHost();
ensureSettingsHost();
ensureHostGroupsData().catch(function () {});
normalizeCategoryBuilderUnknownHostRefs();
const dlpRoute = isDlpSignalBucketRoute();
installCategoryBuilderNetworkPatch();
injectStyles();
if (routeChanged) {
walk(document.body);
translateAttributes(document.body);
hideNoiseNavigation(document.body);
hidePveAuditTabForRegularHost(document.body);
patchActivityHeading(document.body);
patchCategoryBuilderHostLabel(document.body);
staticPatchRouteKey = routeKey;
}
injectPveAuditCenter(document.body);
if (dlpRoute) {
ensureSettingsHost();
injectDlpNavigation(document.body);
if (isDlpSignalBucketRoute() && dlpOverlayFailureCount === 0) {
if (dlpOverlayFailureCount === 0) {
try {
injectDlpReviewCenter(document.body);
} catch (error) {
@@ -1912,9 +2164,23 @@
const existing = document.body.querySelector("[data-aw-ru-dlp-center='1']");
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
}
} else if (!isDlpSignalBucketRoute()) {
injectDlpReviewCenter(document.body);
}
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();
@@ -1947,6 +2213,7 @@
});
window.addEventListener("hashchange", function () {
redirectBareTrendsRoute();
dlpOverlayFailureCount = 0;
staticPatchRouteKey = "";
scheduleApplyPatch();
});
@@ -229,14 +229,17 @@ def main() -> int:
parser = argparse.ArgumentParser(description="Unified AW-RUS health orchestrator")
parser.add_argument("--aw-server", default=env("AW_SERVER_URL", "http://127.0.0.1:5600"))
parser.add_argument("--worktime-api", default=env("AW_WORKTIME_REPORT_BASE", "http://127.0.0.1:5610"))
parser.add_argument(
"--worktime-api",
default=env("AW_RUS_HEALTH_WORKTIME_API", env("AW_WORKTIME_REPORT_BASE", "http://127.0.0.1:5610")),
)
parser.add_argument("--rdp-host", default=env("AW_MONITORED_WINDOWS_HOST", "192.168.100.18"))
parser.add_argument("--rdp-hostname", default=env("AW_MONITORED_WINDOWS_HOSTNAME", "SHARKON2025"))
parser.add_argument("--state-dir", default=env("AW_RUS_HEALTH_STATE_DIR", "/var/lib/activitywatch/health"))
parser.add_argument("--validation-dir", default=env("AW_RUS_HEALTH_VALIDATION_DIR", "/var/lib/activitywatch/health/windows-validation"))
parser.add_argument("--session-max-age-seconds", type=int, default=int(env("AW_RUS_HEALTH_SESSION_MAX_AGE_SECONDS", "900")))
parser.add_argument("--interactive-max-age-seconds", type=int, default=int(env("AW_RUS_HEALTH_INTERACTIVE_MAX_AGE_SECONDS", "900")))
parser.add_argument("--session-events-max-age-seconds", type=int, default=int(env("AW_RUS_HEALTH_SESSION_EVENTS_MAX_AGE_SECONDS", "604800")))
parser.add_argument("--session-events-max-age-seconds", type=int, default=int(env("AW_RUS_HEALTH_SESSION_EVENTS_MAX_AGE_SECONDS", "86400")))
parser.add_argument("--validation-max-age-seconds", type=int, default=int(env("AW_RUS_HEALTH_VALIDATION_MAX_AGE_SECONDS", "259200")))
parser.add_argument("--tcp-timeout-seconds", type=float, default=float(env("AW_RUS_HEALTH_TCP_TIMEOUT_SECONDS", "3")))
parser.add_argument("--json", action="store_true")
@@ -257,9 +260,8 @@ def main() -> int:
report.add("http:aw-server", "fail", f"activitywatch API failed: {exc}", url=f"{aw_api_base}/info")
try:
payload = http_json(args.worktime_api.rstrip("/") + "/reports/worktime/today")
rows = len(payload) if isinstance(payload, list) else None
report.add("http:worktime-api", "ok", "worktime API responded", rows=rows)
payload = http_json(args.worktime_api.rstrip("/") + "/health")
report.add("http:worktime-api", "ok", "worktime API responded", payload=payload if isinstance(payload, dict) else {})
except Exception as exc:
report.add("http:worktime-api", "fail", f"worktime API failed: {exc}", url=args.worktime_api)
@@ -322,6 +324,9 @@ def main() -> int:
missing_status="fail",
stale_status="warn",
)
if session_status == "warn" and session_details.get("age_seconds") is not None:
session_status = "ok"
session_summary = f"event-driven ({session_details['age_seconds']}s since last logon marker)"
report.add("bucket:session-events", session_status, session_summary, **session_details)
validation_dir = Path(args.validation_dir)
@@ -13,9 +13,17 @@ AW_SERVER_USER=activitywatch
AW_SERVER_GROUP=activitywatch
# Worktime API Configuration
AW_WORKTIME_REPORT_BASE=http://10.10.10.13:5610
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
@@ -39,8 +47,18 @@ AW_EXPECT_LANDINGPAGE=/activity/SHARKON2025/view/
AW_HEALTH_STRICT_FILEOPS=0
AW_MONITORED_WINDOWS_HOST=192.168.100.18
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
# 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
File diff suppressed because it is too large Load Diff
@@ -1,14 +1,15 @@
(function () {
var reportBase = "__AW_WORKTIME_REPORT_BASE__";
function defaultDayQuery() {
var now = new Date();
return now.getHours() < 6 ? "day=yesterday" : "day=today";
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;
@@ -17,6 +18,8 @@
'<a href="' + htmlUrl + '" style="color:#fcd34d" target="_blank">HTML</a> | ' +
'<a href="' + csvUrl + '" style="color:#7dd3fc" target="_blank">CSV</a> | ' +
'<a href="' + jsonUrl + '" style="color:#86efac" target="_blank">JSON</a> | ' +
'<a href="' + managerHtmlUrl + '" style="color:#fca5a5" target="_blank">Менеджмент</a> | ' +
'<a href="' + managerJsonUrl + '" style="color:#c4b5fd" target="_blank">Mgmt JSON</a> | ' +
'<a href="#" id="aw-report-toggle" style="color:#f9fafb">Панель</a>';
var panel = document.createElement("div");
@@ -40,10 +43,11 @@
'<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:#0f172a;color:#fff;font:600 13px/1.2 sans-serif">' +
'<div>Отчёт по работе в RDP</div>' +
'<div style="display:flex;gap:12px;align-items:center">' +
'<a href="' + htmlUrl + '" target="_blank" style="color:#93c5fd;text-decoration:none">Открыть</a>' +
'<a href="' + managerHtmlUrl + '" target="_blank" style="color:#fca5a5;text-decoration:none">Открыть менеджмент</a>' +
'<a href="' + htmlUrl + '" target="_blank" style="color:#93c5fd;text-decoration:none">Открыть RDP</a>' +
'<a href="#" id="aw-report-close" style="color:#fff;text-decoration:none">Закрыть</a>' +
"</div></div>" +
'<iframe src="' + htmlUrl + '" title="Отчёт по работе в RDP" style="border:0;width:100%;height:calc(100% - 42px);background:#fff"></iframe>';
'<iframe src="' + managerHtmlUrl + '" title="Управленческий отчёт по работе в RDP" style="border:0;width:100%;height:calc(100% - 42px);background:#fff"></iframe>';
document.body.appendChild(panel);
@@ -26,6 +26,7 @@ VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json"
CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json"
WORKTIME_API_SRC="$BOOTSTRAP_DIR/aw-worktime-api.py"
WORKTIME_API_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-api.service"
WORKTIME_ALIASES_SRC="$BOOTSTRAP_DIR/worktime-manager-aliases.example.json"
WORKTIME_UI_BRIDGE_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.py"
WORKTIME_UI_BRIDGE_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.service"
WORKTIME_UI_BRIDGE_TIMER_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.timer"
@@ -110,6 +111,14 @@ if [[ -f "$WORKTIME_API_SERVICE_SRC" ]]; then
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_SRC" ]]; then
install -m 0755 "$WORKTIME_UI_BRIDGE_SRC" /usr/local/bin/aw-worktime-ui-bridge.py
fi
@@ -16,11 +16,6 @@
{ "type": "top_apps", "size": 3, "props": {} }
]
},
{
"id": "DLP",
"name": "DLP",
"elements": []
},
{
"id": "worktime",
"name": "Worktime",
File diff suppressed because it is too large Load Diff
@@ -18,6 +18,7 @@ $ErrorActionPreference = 'Stop'
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
Add-Type -AssemblyName System.Net.Http
Add-Type @"
using System;
@@ -48,6 +49,35 @@ function Get-DeploymentConfig {
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 }
@@ -542,7 +572,7 @@ function Send-DlpIncidentHeartbeat {
} + $captureData
} | ConvertTo-Json -Depth 5 -Compress
Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null
Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -Json $event
}
function Get-FileSha256Hex {
@@ -717,7 +747,7 @@ function Ensure-Bucket {
} | ConvertTo-Json -Compress
try {
Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId" -ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($body)) | Out-Null
Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body
}
catch {
Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null
@@ -747,7 +777,40 @@ function Send-Heartbeat {
}
} | ConvertTo-Json -Depth 4 -Compress
Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null
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 {
@@ -784,19 +847,62 @@ function Send-CategoryHeartbeat {
}
} | ConvertTo-Json -Depth 4 -Compress
Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null
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) {
@@ -832,5 +938,16 @@ while ($true) {
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
}
@@ -27,6 +27,7 @@ param(
[int]$EvtxRetentionDays = 14,
[string[]]$EvtxChannels = @(),
[bool]$LogonMarkerEnabled = $true,
[bool]$ProcessEventsEnabled = $true,
[string]$AwHostname,
[string]$CustomRulesPath,
[string]$CustomPolicyPath,
@@ -39,6 +40,17 @@ param(
[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
)
@@ -64,6 +76,8 @@ $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'
@@ -83,6 +97,8 @@ $assetResult = Copy-ActivityWatchCollectorAssets `
-FileCollectorScriptSource $fileCollectorSource `
-SessionCollectorScriptSource $sessionCollectorSource `
-EvtxExportScriptSource $evtxExportScriptSource `
-HayabusaUploadScriptSource $hayabusaUploadScriptSource `
-File1CTelemetryScriptSource $file1cTelemetryScriptSource `
-ExampleRulesSource $exampleRulesSource `
-ExamplePolicySource $examplePolicySource `
-StateRoot $StateRoot `
@@ -107,6 +123,8 @@ $config = New-ActivityWatchDeploymentConfig `
-FileCollectorScript $assetResult.FileCollectorScript `
-SessionCollectorScript $assetResult.SessionCollectorScript `
-EvtxExportScript $assetResult.EvtxExportScript `
-HayabusaUploadScript $assetResult.HayabusaUploadScript `
-File1CTelemetryScript $assetResult.File1CTelemetryScript `
-RulesPath $assetResult.ActiveRules `
-PolicyPath $assetResult.ActivePolicy `
-PollSeconds $PollSeconds `
@@ -123,6 +141,7 @@ $config = New-ActivityWatchDeploymentConfig `
-EvtxRetentionDays $EvtxRetentionDays `
-EvtxChannels $EvtxChannels `
-LogonMarkerEnabled $LogonMarkerEnabled `
-ProcessEventsEnabled $ProcessEventsEnabled `
-AwHostname $AwHostname `
-PolicyMode $PolicyMode `
-PolicyEngineEnabled $PolicyEngineEnabled `
@@ -131,6 +150,17 @@ $config = New-ActivityWatchDeploymentConfig `
-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 `
@@ -142,6 +172,8 @@ 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 развёрнут для пользователей:'
@@ -27,6 +27,7 @@ param(
[int]$EvtxRetentionDays = 14,
[string[]]$EvtxChannels = @(),
[bool]$LogonMarkerEnabled = $true,
[bool]$ProcessEventsEnabled = $true,
[string]$AwHostname,
[string]$CustomRulesPath,
[string]$CustomPolicyPath,
@@ -40,6 +41,17 @@ param(
[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
@@ -88,6 +100,7 @@ if (-not (Test-Path -LiteralPath $deployScript)) {
-EvtxRetentionDays $EvtxRetentionDays `
-EvtxChannels $EvtxChannels `
-LogonMarkerEnabled $LogonMarkerEnabled `
-ProcessEventsEnabled $ProcessEventsEnabled `
-AwHostname $AwHostname `
-CustomRulesPath $CustomRulesPath `
-CustomPolicyPath $CustomPolicyPath `
@@ -98,6 +111,17 @@ if (-not (Test-Path -LiteralPath $deployScript)) {
-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) {
@@ -123,6 +147,7 @@ if (-not $SkipHardening) {
-EvtxRetentionDays $EvtxRetentionDays `
-EvtxChannels $EvtxChannels `
-LogonMarkerEnabled $LogonMarkerEnabled `
-ProcessEventsEnabled $ProcessEventsEnabled `
-AwHostname $AwHostname `
-CustomRulesPath $CustomRulesPath `
-CustomPolicyPath $CustomPolicyPath `
@@ -25,6 +25,7 @@ param(
[int]$EvtxRetentionDays = 14,
[string[]]$EvtxChannels = @(),
[bool]$LogonMarkerEnabled = $true,
[bool]$ProcessEventsEnabled = $true,
[string]$AwHostname,
[string]$CustomRulesPath,
[string]$CustomPolicyPath
@@ -102,6 +103,7 @@ $config = New-ActivityWatchDeploymentConfig `
-EvtxRetentionDays $EvtxRetentionDays `
-EvtxChannels $EvtxChannels `
-LogonMarkerEnabled $LogonMarkerEnabled `
-ProcessEventsEnabled $ProcessEventsEnabled `
-AwHostname $AwHostname `
-LaunchScriptPath $launchScriptPath `
-RecoveryScriptPath $recoveryScriptPath `
@@ -182,19 +182,45 @@ function Read-TransportQueueItems {
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
$script:TransportMetrics.queueDepth = $items.Count
if ($items.Count -eq 0) { return }
$left = New-Object System.Collections.Generic.List[object]
$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) {
foreach ($item in @($items)) {
if ($null -eq $item) { continue }
if ($sent -ge $MaxItems) {
$left.Add($item)
$retryItems += $item
continue
}
try {
@@ -204,15 +230,14 @@ function Flush-TransportQueue {
}
catch {
$script:TransportMetrics.sendFailures++
$left.Add($item)
$retryItems += $item
}
}
foreach ($item in $items | Select-Object -Skip ($sent + $left.Count)) {
$left.Add($item)
}
$lines = @($left | ForEach-Object { $_ | ConvertTo-Json -Compress })
Set-Content -LiteralPath $script:TransportQueuePath -Value $lines -Encoding UTF8
$script:TransportMetrics.queueDepth = $left.Count
$lock = Get-TransportQueueLock
try {
$concurrentItems = @(Read-TransportQueueItems)
Write-TransportQueueItems -Items (@($retryItems) + @($concurrentItems))
}
finally {
$lock.Dispose()
@@ -24,6 +24,7 @@ param(
[int]$EvtxRetentionDays,
[string[]]$EvtxChannels,
[bool]$LogonMarkerEnabled,
[bool]$ProcessEventsEnabled,
[string]$AwHostname,
[string]$CustomRulesPath,
[string]$CustomPolicyPath,
@@ -70,6 +71,8 @@ $effectiveEndpointCollector = if ($existingConfig -and $existingConfig.paths.PSO
$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' }
@@ -91,6 +94,7 @@ $effectiveEvtxExportRoot = if ($PSBoundParameters.ContainsKey('EvtxExportRoot')
$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 { $true }
$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' }
@@ -100,6 +104,31 @@ $effectivePolicyEnginePort = if ($PSBoundParameters.ContainsKey('PolicyEnginePor
$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
@@ -131,6 +160,8 @@ $assetResult = Copy-ActivityWatchCollectorAssets `
-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 `
@@ -155,6 +186,8 @@ $config = New-ActivityWatchDeploymentConfig `
-FileCollectorScript $effectiveFileCollector `
-SessionCollectorScript $effectiveSessionCollector `
-EvtxExportScript $effectiveEvtxExportScript `
-HayabusaUploadScript $effectiveHayabusaUploadScript `
-File1CTelemetryScript $effectiveFile1CTelemetryScript `
-RulesPath $effectiveRules `
-PolicyPath $effectivePolicy `
-PollSeconds $effectivePollSeconds `
@@ -171,6 +204,7 @@ $config = New-ActivityWatchDeploymentConfig `
-EvtxRetentionDays $effectiveEvtxRetentionDays `
-EvtxChannels $effectiveEvtxChannels `
-LogonMarkerEnabled $effectiveLogonMarkerEnabled `
-ProcessEventsEnabled $effectiveProcessEventsEnabled `
-AwHostname $effectiveAwHostname `
-PolicyMode $effectivePolicyMode `
-PolicyEngineEnabled $effectivePolicyEngineEnabled `
@@ -179,6 +213,16 @@ $config = New-ActivityWatchDeploymentConfig `
-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 `
@@ -189,6 +233,8 @@ 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 завершены.'
@@ -30,14 +30,30 @@ $pulseSeconds = if ($config.PSObject.Properties.Name -contains 'collector' -and
$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 { $true }
$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)) {
@@ -50,6 +66,20 @@ function Get-LoggedOnUsers {
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)) {
@@ -361,13 +391,27 @@ $endpointCollectorProcesses = @(Get-CollectorProcesses -ScriptPath $endpointColl
$fileCollectorProcesses = if ($fileOpsExpected) { @(Get-CollectorProcesses -ScriptPath $fileCollectorScript) } else { @() }
$browserCollectorProcesses = @(Get-CollectorProcesses -ScriptPath $collectorScript)
$loggedOnUsers = Get-LoggedOnUsers
$sessionBoundUsers = @(
$liveLoggedOnUsers = Get-LoggedOnUsers
$interactiveUsers = Get-LoggedOnUsers -IncludeDisconnected $true
$liveSessionBoundUsers = @(
@($config.userTasks) |
Where-Object { Test-UserHasSession -UserId ([string]$_.userId) -LoggedOnUsers $loggedOnUsers } |
Where-Object { Test-UserHasSession -UserId ([string]$_.userId) -LoggedOnUsers $liveLoggedOnUsers } |
ForEach-Object { [string]$_.userId }
)
$sessionScopedExpectedCount = [int]$sessionBoundUsers.Count
$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)
$taskNames = @()
@@ -401,7 +445,7 @@ if ($sessionScopedCollectorsRequired -and $windowExpected) {
$bucketChecks += Get-BucketHealth -BucketId ('aw-watcher-window_' + $awHostname) -MaxAgeSeconds $freshnessSeconds -Required $true -RequireFreshEvent $false
}
if ($sessionScopedCollectorsRequired) {
$bucketChecks += Get-BucketHealth -BucketId ('aw-dlp-endpoint-signals_' + $awHostname) -MaxAgeSeconds $freshnessSeconds -Required $true -RequireFreshEvent $true
$bucketChecks += Get-BucketHealth -BucketId ('aw-dlp-endpoint-signals_' + $awHostname) -MaxAgeSeconds $endpointFreshnessSeconds -Required $true -RequireFreshEvent $true
}
if ($sessionScopedCollectorsRequired -and $fileOpsExpected) {
$bucketChecks += Get-BucketHealth -BucketId ('aw-file-operations_' + $awHostname) -MaxAgeSeconds $transportStaleSeconds -Required $false -RequireFreshEvent $true
@@ -431,18 +475,9 @@ catch {
}
$watcherCountsOk = $true
if ($sessionScopedCollectorsRequired) {
if ($afkExpected) {
$watcherCountsOk = $watcherCountsOk -and (($watcherByName['aw-watcher-afk'] | ForEach-Object { [int]$_ }) -ge $sessionScopedExpectedCount)
}
if ($windowExpected) {
$watcherCountsOk = $watcherCountsOk -and (($watcherByName['aw-watcher-window'] | ForEach-Object { [int]$_ }) -ge $sessionScopedExpectedCount)
}
}
$endpointProcessOk = if (-not $sessionScopedCollectorsRequired) { $true } else { (@($endpointCollectorProcesses).Count -ge $sessionScopedExpectedCount) }
$fileProcessOk = if (-not $fileOpsExpected -or -not $sessionScopedCollectorsRequired) { $true } else { (@($fileCollectorProcesses).Count -ge $sessionScopedExpectedCount) }
$browserProcessOk = if (-not $sessionScopedCollectorsRequired) { $true } else { (@($browserCollectorProcesses).Count -ge $sessionScopedExpectedCount) }
$endpointProcessOk = $true
$fileProcessOk = $true
$browserProcessOk = $true
$sessionCollectorOk = (@($sessionCollectorProcesses).Count -eq 1)
$result = [ordered]@{
@@ -470,7 +505,8 @@ $result = [ordered]@{
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present -or -not $_.enabled }))
}
processes = [ordered]@{
sessionBoundUsers = $sessionBoundUsers
liveSessionBoundUsers = $liveSessionBoundUsers
sessionBoundUsers = $interactiveSessionBoundUsers
sessionScopedExpectedCount = [int]$sessionScopedExpectedCount
watchers = @($runningWatchers)
watcherDuplicates = @($watcherDuplicates)
@@ -508,6 +544,12 @@ $result = [ordered]@{
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 }
@@ -94,11 +94,13 @@ function Ensure-Bucket {
param(
[Parameter(Mandatory = $true)][string]$ApiBase,
[Parameter(Mandatory = $true)][string]$BucketId,
[Parameter(Mandatory = $true)][string]$HostnameValue
[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='aw-worktime-session-collector'; type='aw.worktime.session'; hostname=$HostnameValue } | ConvertTo-Json -Compress
$body = @{ client=$ClientName; type=$BucketType; hostname=$HostnameValue } | ConvertTo-Json -Compress
$attempts = 0
while ($attempts -lt 3) {
$attempts++
@@ -230,16 +232,282 @@ function Get-CanonicalUserId {
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')
@@ -265,6 +533,13 @@ while ($true) {
)
}
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]@{
+3
View File
@@ -30,6 +30,9 @@ sync_tree() {
if [[ -d "$OLD_SERVER_CONFIG_DIR" ]]; then
cp "$OLD_SERVER_CONFIG_DIR"/*.deployment-config.json "$TMP_SERVER_CONFIG_DIR"/
fi
if [[ -d "$SERVER_CONFIG_DIR" ]]; then
cp "$SERVER_CONFIG_DIR"/*.deployment-config.json "$TMP_SERVER_CONFIG_DIR"/
fi
rm -rf "${KIT_DIR}/ansible" "${KIT_DIR}/aw-server" "${KIT_DIR}/windows" "${KIT_DIR}/server-configs-"*
Binary file not shown.