From 5a41c4066b11b2dcc65d6088858ed4da7a6a0247 Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Mon, 27 Apr 2026 21:22:40 +0300 Subject: [PATCH] feat(windows): add install kit and stabilize dlp print document matching --- install-kit-awindows-20260427-211240.tar.gz | Bin 0 -> 28882 bytes install-kit-awindows-20260427-211240.zip | Bin 0 -> 41040 bytes .../MANIFEST.txt | 27 + .../README-INSTALL-KIT.txt | 10 + .../ansible/README.md | 119 +++ .../ansible/deploy_aw_windows_phase2.yml | 133 +++ .../ansible/windows.example.yml | 33 + .../phase2-admin.deployment-config.json | 57 + .../phase2-u2u5.deployment-config.json | 69 ++ .../phase2-user1.deployment-config.json | 57 + .../windows/ActivityWatch.Windows.Common.psd1 | 25 + .../windows/ActivityWatch.Windows.Common.psm1 | 982 ++++++++++++++++++ .../browser-domains-native-collector.ps1 | 822 +++++++++++++++ .../windows/deploy-domain-users.ps1 | 107 ++ .../windows/deploy-ensemble.ps1 | 137 +++ .../windows/deploy-single-user.ps1 | 106 ++ .../dlp-endpoint-signals-collector.ps1 | 796 ++++++++++++++ .../windows/dlp-policy.example.json | 99 ++ .../windows/hardening-recovery.ps1 | 144 +++ .../windows/validate-deployment.ps1 | 90 ++ .../windows/web-category-rules.example.json | 37 + windows/dlp-endpoint-signals-collector.ps1 | 114 +- 22 files changed, 3940 insertions(+), 24 deletions(-) create mode 100644 install-kit-awindows-20260427-211240.tar.gz create mode 100644 install-kit-awindows-20260427-211240.zip create mode 100644 install-kit-awindows-20260427-211240/MANIFEST.txt create mode 100644 install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt create mode 100644 install-kit-awindows-20260427-211240/ansible/README.md create mode 100644 install-kit-awindows-20260427-211240/ansible/deploy_aw_windows_phase2.yml create mode 100644 install-kit-awindows-20260427-211240/ansible/windows.example.yml create mode 100644 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-admin.deployment-config.json create mode 100644 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-u2u5.deployment-config.json create mode 100644 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-user1.deployment-config.json create mode 100644 install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1 create mode 100755 install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 create mode 100755 install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 create mode 100755 install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 create mode 100644 install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 create mode 100755 install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 create mode 100644 install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 create mode 100644 install-kit-awindows-20260427-211240/windows/dlp-policy.example.json create mode 100755 install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 create mode 100644 install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 create mode 100755 install-kit-awindows-20260427-211240/windows/web-category-rules.example.json diff --git a/install-kit-awindows-20260427-211240.tar.gz b/install-kit-awindows-20260427-211240.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..203655f85c7bf8e4cf485df076519e4be93f71c0 GIT binary patch literal 28882 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 literal 0 HcmV?d00001 diff --git a/install-kit-awindows-20260427-211240.zip b/install-kit-awindows-20260427-211240.zip new file mode 100644 index 0000000000000000000000000000000000000000..ac0b5bebce21263a4dbf86d813e76aee1a626a22 GIT binary patch literal 41040 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 literal 0 HcmV?d00001 diff --git a/install-kit-awindows-20260427-211240/MANIFEST.txt b/install-kit-awindows-20260427-211240/MANIFEST.txt new file mode 100644 index 0000000..0e6157c --- /dev/null +++ b/install-kit-awindows-20260427-211240/MANIFEST.txt @@ -0,0 +1,27 @@ +KIT_DIR=install-kit-awindows-20260427-211240 +CREATED_AT=2026-04-27T21:14:11+03:00 + +FILES: +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/MANIFEST.txt +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/ansible/README.md +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows_phase2.yml +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/ansible/windows.example.yml +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-admin.deployment-config.json +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-u2u5.deployment-config.json +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-user1.deployment-config.json +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1 +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/dlp-policy.example.json +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 +/home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240/windows/web-category-rules.example.json + +SHA256: +f48965a47781c329c08e261461ac4808031e35c5fe24fb84a2f1eaa798cd042d /home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240.tar.gz +e1ef90f566d821b196f16dfa0810f86530cc0b92d13dc83c9ae8ab0427c37c0b /home/igor/tmp/AWatch-rus/install-kit-awindows-20260427-211240.zip diff --git a/install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt b/install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt new file mode 100644 index 0000000..d09d6bb --- /dev/null +++ b/install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt @@ -0,0 +1,10 @@ +ActivityWatch DetMir Windows Install Kit + +Includes: +- windows/* (deploy scripts, collectors, common module, configs/examples) +- ansible/deploy_aw_windows_phase2.yml +- ansible/group_vars/windows.example.yml +- ansible/README.md + +Source: +- Local project snapshot at build time. diff --git a/install-kit-awindows-20260427-211240/ansible/README.md b/install-kit-awindows-20260427-211240/ansible/README.md new file mode 100644 index 0000000..b8a82d7 --- /dev/null +++ b/install-kit-awindows-20260427-211240/ansible/README.md @@ -0,0 +1,119 @@ +# Ansible ensemble for AWatch-rus + +Эта директория содержит Ansible-ensemble для двух сценариев: + +- деплой на уже существующий Debian host/CT; +- полный цикл с нуля в Proxmox: создание CT + bootstrap + установка ActivityWatch + RU patch. +- централизованный деплой Windows phase-2 collectors по WinRM. +- deployment внешнего pfSense poller'а на Debian/Ubuntu utility VM. + +## Файлы + +- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_server.yml` — основной playbook. +- `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_and_deploy_aw.yml` — full-stack playbook для Proxmox. +- `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml` — массовый full-stack playbook (несколько CT). +- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_windows_phase2.yml` — WinRM playbook для развёртывания phase-2 Windows collector'ов. +- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_pfsense_poller.yml` — deployment pfSense poller'а. +- `/home/igor/tmp/AWatch-rus/ansible/inventory.example.ini` — шаблон inventory. +- `/home/igor/tmp/AWatch-rus/ansible/group_vars/all.example.yml` — шаблон переменных. +- `/home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox.example.yml` — шаблон переменных CT в Proxmox. +- `/home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox-matrix.example.yml` — шаблон матрицы CT. +- `/home/igor/tmp/AWatch-rus/ansible/group_vars/windows.example.yml` — шаблон переменных Windows phase-2. +- `/home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.example.yml` — шаблон переменных pfSense poller'а. + +## Быстрый запуск + +1. Скопируйте шаблоны: + - `cp /home/igor/tmp/AWatch-rus/ansible/inventory.example.ini /home/igor/tmp/AWatch-rus/ansible/inventory.ini` + - `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/all.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/all.yml` +2. Заполните значения в `inventory.ini` и `group_vars/all.yml`. +3. Запустите: + +```bash +cd /home/igor/tmp/AWatch-rus/ansible +ansible-playbook -i inventory.ini deploy_aw_server.yml +``` + +## Полный запуск с нуля в Proxmox + +1. Подготовьте inventory и vars: + - `cp /home/igor/tmp/AWatch-rus/ansible/inventory.example.ini /home/igor/tmp/AWatch-rus/ansible/inventory.ini` + - `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/all.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/all.yml` + - `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox.yml` +2. Заполните `group_vars/proxmox.yml` и `group_vars/all.yml`. +3. Запустите playbook: + +```bash +cd /home/igor/tmp/AWatch-rus/ansible +ansible-playbook -i inventory.ini provision_proxmox_ct_and_deploy_aw.yml +``` + +## Массовый запуск (матрица CT) + +1. Подготовьте матрицу: + - `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox-matrix.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox-matrix.yml` +2. Заполните `proxmox-matrix.yml`. +3. Запустите: + +```bash +cd /home/igor/tmp/AWatch-rus/ansible +ansible-playbook -i inventory.ini provision_proxmox_ct_matrix_and_deploy_aw.yml +``` + +## Windows phase-2 rollout (WinRM) + +1. Подготовьте inventory и vars: + - `cp /home/igor/tmp/AWatch-rus/ansible/inventory.example.ini /home/igor/tmp/AWatch-rus/ansible/inventory.ini` + - `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/windows.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/windows.yml` +2. Заполните `inventory.ini` (секция `[aw_windows]`) и `group_vars/windows.yml`. +3. Запустите: + +```bash +cd /home/igor/tmp/AWatch-rus/ansible +ansible-playbook -i inventory.ini deploy_aw_windows_phase2.yml +``` + +Playbook: + +- выгружает `windows/*` toolkit на целевой хост в `C:\Deploy\AWatch-rus\windows`; +- выполняет `deploy-ensemble.ps1` (deploy + hardening/recovery) с phase-2 policy/rules; +- запускает `validate-deployment.ps1`; +- забирает JSON-отчёт в локальную директорию (`/tmp/aw-rus-validation` по умолчанию). + +Дополнительные флаги: + +- `aw_windows_afk_enabled: false` — не запускать `aw-watcher-afk`; +- `aw_windows_window_enabled: false` — не запускать `aw-watcher-window`; +- `aw_windows_incident_capture_enabled: false` — отключить блок incidentCapture; +- `aw_windows_incident_screenshot_enabled: false` — не делать скриншот при DLP-инциденте; +- `aw_windows_incident_artifacts_root: 'C:\...\incident-artifacts'` — переопределить путь артефактов; +- `aw_windows_skip_hardening: true` — пропустить `hardening-recovery.ps1` внутри ensemble-скрипта. + +## pfSense poller rollout + +1. Подготовьте vars: + - `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.yml` +2. Добавьте inventory group `[aw_pfsense_pollers]`. +3. Запустите: + +```bash +cd /home/igor/tmp/AWatch-rus/ansible +ansible-playbook -i inventory.ini deploy_aw_pfsense_poller.yml +``` + +Playbook: + +- ставит `python3`; +- копирует `pfsense-aw-poller.py`; +- пишет `/etc/aw-pfsense/poller.json`; +- поднимает `aw-pfsense-poller.service`. + +## Результат + +- Установлен ActivityWatch Server. +- Создан systemd-unit `activitywatch-server.service`. +- Установлен RU Web UI patch. +- Для Web UI используется checksum-based cache-bust для `ru-patch-v5.js` и `sw-cleanup.js`, чтобы браузер не держал старую DLP/русскую статику после деплоя. +- На `#/home` Web UI делит хосты на `Windows RDP` и `Virtual servers + Proxmox`. +- Выполнена валидация API `http://127.0.0.1:5600/api/0/info`. +- Для full-stack сценария CT создаётся автоматически через `pct create`. diff --git a/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows_phase2.yml b/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows_phase2.yml new file mode 100644 index 0000000..8cf0e9f --- /dev/null +++ b/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows_phase2.yml @@ -0,0 +1,133 @@ +--- +- name: Deploy AWatch-rus Windows phase2 collectors + hosts: aw_windows + gather_facts: false + + vars: + aw_windows_repo_root: "/home/igor/tmp/AWatch-rus" + aw_windows_deploy_root: "C:\\Deploy\\AWatch-rus" + aw_windows_server_host: "10.10.10.13" + aw_windows_server_port: 5600 + aw_windows_domain: "SHARKON2025" + aw_windows_users: + - user1 + - user2 + - user3 + - user4 + - user5 + aw_windows_extra_users: [] + aw_windows_users_effective: "{{ (aw_windows_users + aw_windows_extra_users) | unique }}" + aw_windows_install_root: "C:\\Program Files\\ActivityWatch-Phase2" + aw_windows_state_root: "C:\\ProgramData\\ActivityWatch-Phase2" + aw_windows_afk_enabled: true + aw_windows_window_enabled: true + aw_windows_local_agent_logs_enabled: false + aw_windows_incident_capture_enabled: true + aw_windows_incident_screenshot_enabled: true + aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts" + aw_windows_logon_marker_enabled: true + aw_windows_skip_hardening: false + aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json" + aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json" + aw_windows_validation_remote_path: "C:\\Windows\\Temp\\aw_validate_phase2_ansible.json" + aw_windows_validation_local_dir: "/tmp/aw-rus-validation" + + tasks: + - name: Validate required variables + ansible.builtin.assert: + that: + - aw_windows_server_host is defined + - aw_windows_server_port is defined + - aw_windows_domain is defined + - aw_windows_users_effective | length > 0 + - aw_windows_install_root is defined + - aw_windows_state_root is defined + fail_msg: "Missing required Windows deployment variables." + + - name: Ensure deploy directories exist + ansible.windows.win_file: + path: "{{ item }}" + state: directory + loop: + - "{{ aw_windows_deploy_root }}" + - "{{ aw_windows_deploy_root }}\\windows" + + - name: Upload Windows deployment toolkit + ansible.windows.win_copy: + src: "{{ aw_windows_repo_root }}/windows/{{ item }}" + dest: "{{ aw_windows_deploy_root }}\\windows\\{{ item }}" + loop: + - ActivityWatch.Windows.Common.psd1 + - ActivityWatch.Windows.Common.psm1 + - browser-domains-native-collector.ps1 + - dlp-endpoint-signals-collector.ps1 + - deploy-domain-users.ps1 + - deploy-ensemble.ps1 + - hardening-recovery.ps1 + - validate-deployment.ps1 + - web-category-rules.example.json + - dlp-policy.example.json + + - name: Upload user list for domain deploy + ansible.windows.win_copy: + dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt" + content: | + {% for user in aw_windows_users -%} + {{ user }} + {% endfor -%} + {% for user in aw_windows_extra_users -%} + {{ user }} + {% endfor -%} + + - name: Run phase2 ensemble deployment + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $params = @{ + ServerHost = "{{ aw_windows_server_host }}" + ServerPort = {{ aw_windows_server_port }} + Domain = "{{ aw_windows_domain }}" + UserListPath = "{{ aw_windows_deploy_root }}\windows\users.txt" + InstallRoot = "{{ aw_windows_install_root }}" + StateRoot = "{{ aw_windows_state_root }}" + AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }} + WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }} + LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }} + IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }} + IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }} + IncidentArtifactsRoot = "{{ aw_windows_incident_artifacts_root }}" + LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }} + CustomRulesPath = "{{ aw_windows_rules_path }}" + CustomPolicyPath = "{{ aw_windows_policy_path }}" + } + {% if aw_windows_skip_hardening | bool %} + $params.SkipHardening = $true + {% endif %} + & "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params + + - name: Run validation and store report on target + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" ` + -ConfigPath "{{ aw_windows_state_root }}\deployment-config.json" + $report | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8 + + - name: Ensure local validation directory exists + ansible.builtin.file: + path: "{{ aw_windows_validation_local_dir }}" + state: directory + mode: "0755" + delegate_to: localhost + + - name: Fetch validation report + ansible.builtin.fetch: + src: "{{ aw_windows_validation_remote_path }}" + dest: "{{ aw_windows_validation_local_dir }}/" + flat: false + + - name: Show report location + ansible.builtin.debug: + msg: + - "Windows phase2 deploy completed on {{ inventory_hostname }}." + - "Validation report: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}/C$/Windows/Temp/aw_validate_phase2_ansible.json" diff --git a/install-kit-awindows-20260427-211240/ansible/windows.example.yml b/install-kit-awindows-20260427-211240/ansible/windows.example.yml new file mode 100644 index 0000000..09e91d3 --- /dev/null +++ b/install-kit-awindows-20260427-211240/ansible/windows.example.yml @@ -0,0 +1,33 @@ +aw_windows_repo_root: "/home/igor/tmp/AWatch-rus" +aw_windows_deploy_root: "C:\\Deploy\\AWatch-rus" +aw_windows_server_host: "10.10.10.13" +aw_windows_server_port: 5600 +aw_windows_domain: "SHARKON2025" +aw_windows_users: + - user1 + - user2 + - user3 + - user4 + - user5 +aw_windows_extra_users: [] +# Например: +# aw_windows_extra_users: +# - Администратор + +# Рекомендуемый изолированный профиль для фазового раската. +aw_windows_install_root: "C:\\Program Files\\ActivityWatch-Phase2" +aw_windows_state_root: "C:\\ProgramData\\ActivityWatch-Phase2" +aw_windows_afk_enabled: true +aw_windows_window_enabled: true +aw_windows_local_agent_logs_enabled: false +aw_windows_incident_capture_enabled: true +aw_windows_incident_screenshot_enabled: true +aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts" +aw_windows_logon_marker_enabled: true +aw_windows_skip_hardening: false + +aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json" +aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json" + +aw_windows_validation_remote_path: "C:\\Windows\\Temp\\aw_validate_phase2_ansible.json" +aw_windows_validation_local_dir: "/tmp/aw-rus-validation" diff --git a/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-admin.deployment-config.json b/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-admin.deployment-config.json new file mode 100644 index 0000000..2b6d20c --- /dev/null +++ b/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-admin.deployment-config.json @@ -0,0 +1,57 @@ +{ + "version": 1, + "generatedAtUtc": "2026-04-27T01:14:21.9184268Z", + "server": { + "host": "10.10.10.13", + "port": 5600, + "scheme": "http" + }, + "paths": { + "installRoot": "C:\\Program Files\\ActivityWatch-Phase2-admin", + "stateRoot": "C:\\ProgramData\\ActivityWatch\\phase2-admin", + "logsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\logs", + "collectorScript": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\browser-domains-native-collector.ps1", + "endpointCollectorScript": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\dlp-endpoint-signals-collector.ps1", + "rulesPath": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\web-category-rules.json", + "policyPath": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\dlp-policy.json", + "launchScript": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\launch-watchers.ps1", + "recoveryScript": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\recovery-loop.ps1" + }, + "collector": { + "pollSeconds": 5, + "pulseSeconds": 30 + }, + "collectors": { + "afkEnabled": true, + "windowEnabled": true + }, + "logging": { + "localAgentLogsEnabled": false + }, + "incidentCapture": { + "enabled": true, + "screenshotEnabled": true, + "artifactsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-admin\\incident-artifacts" + }, + "sessionEvents": { + "logonEnabled": true, + "bucketPrefix": "aw-session-events" + }, + "recovery": { + "intervalSeconds": 180, + "taskName": "ActivityWatch Recovery" + }, + "dlp": { + "incidentBucketPrefix": "aw-dlp-incidents", + "enabled": true + }, + "package": { + "version": "v0.13.2" + }, + "userTasks": [ + { + "UserId": "SHARKON2025\\Администратор", + "LaunchTaskName": "ActivityWatch Launch [SHARKON2025_РђРґРјРёРЅРёСЃС_СЂР_С_РѕСЂ]" + } + ] +} diff --git a/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-u2u5.deployment-config.json b/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-u2u5.deployment-config.json new file mode 100644 index 0000000..a0f344f --- /dev/null +++ b/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-u2u5.deployment-config.json @@ -0,0 +1,69 @@ +{ + "version": 1, + "generatedAtUtc": "2026-04-27T01:09:42.4193209Z", + "server": { + "host": "10.10.10.13", + "port": 5600, + "scheme": "http" + }, + "paths": { + "installRoot": "C:\\Program Files\\ActivityWatch-Phase2-u2u5", + "stateRoot": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5", + "logsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\logs", + "collectorScript": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\browser-domains-native-collector.ps1", + "endpointCollectorScript": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\dlp-endpoint-signals-collector.ps1", + "rulesPath": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\web-category-rules.json", + "policyPath": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\dlp-policy.json", + "launchScript": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\launch-watchers.ps1", + "recoveryScript": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\recovery-loop.ps1" + }, + "collector": { + "pollSeconds": 5, + "pulseSeconds": 30 + }, + "collectors": { + "afkEnabled": true, + "windowEnabled": true + }, + "logging": { + "localAgentLogsEnabled": false + }, + "incidentCapture": { + "enabled": true, + "screenshotEnabled": true, + "artifactsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-u2u5\\incident-artifacts" + }, + "sessionEvents": { + "logonEnabled": true, + "bucketPrefix": "aw-session-events" + }, + "recovery": { + "intervalSeconds": 180, + "taskName": "ActivityWatch Recovery" + }, + "dlp": { + "incidentBucketPrefix": "aw-dlp-incidents", + "enabled": true + }, + "package": { + "version": "v0.13.2" + }, + "userTasks": [ + { + "UserId": "SHARKON2025\\user2", + "LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user2]" + }, + { + "UserId": "SHARKON2025\\user3", + "LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user3]" + }, + { + "UserId": "SHARKON2025\\user4", + "LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user4]" + }, + { + "UserId": "SHARKON2025\\user5", + "LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user5]" + } + ] +} diff --git a/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-user1.deployment-config.json b/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-user1.deployment-config.json new file mode 100644 index 0000000..0eecca9 --- /dev/null +++ b/install-kit-awindows-20260427-211240/server-configs-192.168.100.21/phase2-user1.deployment-config.json @@ -0,0 +1,57 @@ +{ + "version": 1, + "generatedAtUtc": "2026-04-27T01:09:38.9519788Z", + "server": { + "host": "10.10.10.13", + "port": 5600, + "scheme": "http" + }, + "paths": { + "installRoot": "C:\\Program Files\\ActivityWatch-Phase2", + "stateRoot": "C:\\ProgramData\\ActivityWatch\\phase2-user1", + "logsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\logs", + "collectorScript": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\browser-domains-native-collector.ps1", + "endpointCollectorScript": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\dlp-endpoint-signals-collector.ps1", + "rulesPath": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\web-category-rules.json", + "policyPath": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\dlp-policy.json", + "launchScript": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\launch-watchers.ps1", + "recoveryScript": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\recovery-loop.ps1" + }, + "collector": { + "pollSeconds": 5, + "pulseSeconds": 30 + }, + "collectors": { + "afkEnabled": true, + "windowEnabled": true + }, + "logging": { + "localAgentLogsEnabled": false + }, + "incidentCapture": { + "enabled": true, + "screenshotEnabled": true, + "artifactsRoot": "C:\\ProgramData\\ActivityWatch\\phase2-user1\\incident-artifacts" + }, + "sessionEvents": { + "logonEnabled": true, + "bucketPrefix": "aw-session-events" + }, + "recovery": { + "intervalSeconds": 180, + "taskName": "ActivityWatch Recovery" + }, + "dlp": { + "incidentBucketPrefix": "aw-dlp-incidents", + "enabled": true + }, + "package": { + "version": "v0.13.2" + }, + "userTasks": [ + { + "UserId": "SHARKON2025\\user1", + "LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user1]" + } + ] +} diff --git a/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1 b/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1 new file mode 100644 index 0000000..9bc5fa8 --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1 @@ -0,0 +1,25 @@ +@{ + RootModule = 'ActivityWatch.Windows.Common.psm1' + ModuleVersion = '1.0.0' + GUID = '90b3fcf6-df9f-4f9b-9ee0-8a7de4dc0ee2' + Author = 'igor04091968' + CompanyName = 'Private' + Description = 'Common PowerShell functions for ActivityWatch Windows deployment, hardening and recovery.' + PowerShellVersion = '5.1' + FunctionsToExport = @( + '*-ActivityWatch*', + 'Assert-Administrator', + 'Normalize-ActivityWatchUsers', + 'Get-ActivityWatchPackageUrl', + 'Remove-LegacyActivityWatchEntries' + ) + CmdletsToExport = @() + VariablesToExport = '*' + AliasesToExport = @() + PrivateData = @{ + PSData = @{ + Tags = @('ActivityWatch', 'Windows', 'Deployment', 'Recovery') + ProjectUri = 'https://github.com/igor04091968/AWatch-rus' + } + } +} diff --git a/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 b/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 new file mode 100755 index 0000000..4bca474 --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 @@ -0,0 +1,982 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Assert-Administrator { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($identity) + if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + throw 'Run this script from an elevated PowerShell session.' + } +} + +function New-ActivityWatchDirectory { + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + if (-not (Test-Path -LiteralPath $Path)) { + New-Item -Path $Path -ItemType Directory -Force | Out-Null + } +} + +function Get-ActivityWatchPackageUrl { + param( + [string]$Version = 'v0.13.2' + ) + + return "https://github.com/ActivityWatch/activitywatch/releases/download/$Version/activitywatch-$Version-windows-x86_64.zip" +} + +function Get-ActivityWatchArchive { + param( + [string]$PackageZipPath, + [string]$PackageUrl, + [string]$Version = 'v0.13.2', + [Parameter(Mandatory = $true)] + [string]$WorkingRoot + ) + + New-ActivityWatchDirectory -Path $WorkingRoot + + if ($PackageZipPath) { + $resolved = Resolve-Path -LiteralPath $PackageZipPath -ErrorAction Stop + return $resolved.Path + } + + if (-not $PackageUrl) { + $PackageUrl = Get-ActivityWatchPackageUrl -Version $Version + } + + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}.zip" -f $Version.TrimStart('v')) + Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath + return $archivePath +} + +function Get-ActivityWatchPackageRoot { + param( + [Parameter(Mandatory = $true)] + [string]$ExpandedRoot + ) + + $afkBinary = Get-ChildItem -Path $ExpandedRoot -Filter 'aw-watcher-afk.exe' -File -Recurse | + Select-Object -First 1 + + if (-not $afkBinary) { + throw "Cannot find aw-watcher-afk.exe under $ExpandedRoot." + } + + return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent) +} + +function Install-ActivityWatchPackage { + param( + [Parameter(Mandatory = $true)] + [string]$ArchivePath, + [Parameter(Mandatory = $true)] + [string]$InstallRoot, + [Parameter(Mandatory = $true)] + [string]$WorkingRoot, + [Parameter(Mandatory = $true)] + [string]$BackupRoot + ) + + New-ActivityWatchDirectory -Path $WorkingRoot + New-ActivityWatchDirectory -Path $BackupRoot + + $extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid) + if (Test-Path -LiteralPath $extractRoot) { + Remove-Item -LiteralPath $extractRoot -Recurse -Force + } + New-ActivityWatchDirectory -Path $extractRoot + + Expand-Archive -Path $ArchivePath -DestinationPath $extractRoot -Force + $packageRoot = Get-ActivityWatchPackageRoot -ExpandedRoot $extractRoot + + if (Test-Path -LiteralPath $InstallRoot) { + $existingItems = Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue + if ($existingItems) { + $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' + $backupPath = Join-Path $BackupRoot ("install-$stamp") + New-ActivityWatchDirectory -Path $backupPath + Copy-Item -Path (Join-Path $InstallRoot '*') -Destination $backupPath -Recurse -Force + Get-ChildItem -LiteralPath $InstallRoot -Force | Remove-Item -Recurse -Force + } + } + else { + New-ActivityWatchDirectory -Path $InstallRoot + } + + Copy-Item -Path (Join-Path $packageRoot '*') -Destination $InstallRoot -Recurse -Force + + return [pscustomobject]@{ + PackageRoot = $packageRoot + ExtractRoot = $extractRoot + BackupRoot = $BackupRoot + } +} + +function Get-ActivityWatchExecutableMap { + param( + [Parameter(Mandatory = $true)] + [string]$InstallRoot + ) + + $map = [ordered]@{ + Afk = Join-Path $InstallRoot 'aw-watcher-afk\aw-watcher-afk.exe' + Window = Join-Path $InstallRoot 'aw-watcher-window\aw-watcher-window.exe' + } + + foreach ($entry in $map.GetEnumerator()) { + if (-not (Test-Path -LiteralPath $entry.Value)) { + throw "Missing required ActivityWatch binary: $($entry.Value)" + } + } + + return [pscustomobject]$map +} + +function Normalize-ActivityWatchUsers { + param( + [string[]]$Users, + [string]$UserListPath, + [string]$Domain + ) + + $collected = New-Object System.Collections.Generic.List[string] + + if ($Users) { + foreach ($user in $Users) { + if (-not [string]::IsNullOrWhiteSpace($user)) { + $collected.Add($user.Trim()) + } + } + } + + if ($UserListPath) { + $resolved = Resolve-Path -LiteralPath $UserListPath -ErrorAction Stop + $extension = [IO.Path]::GetExtension($resolved.Path) + if ($extension -ieq '.csv') { + $rows = Import-Csv -LiteralPath $resolved.Path + foreach ($row in $rows) { + foreach ($column in 'User', 'Username', 'SamAccountName', 'Login') { + if ($row.PSObject.Properties.Name -contains $column) { + $value = [string]$row.$column + if (-not [string]::IsNullOrWhiteSpace($value)) { + $collected.Add($value.Trim()) + break + } + } + } + } + } + else { + Get-Content -LiteralPath $resolved.Path | ForEach-Object { + $line = $_.Trim() + if ($line -and -not $line.StartsWith('#')) { + $collected.Add($line) + } + } + } + } + + $normalized = $collected | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + ForEach-Object { + if ($Domain -and ($_ -notmatch '[\\@]')) { + '{0}\{1}' -f $Domain, $_ + } + else { + $_ + } + } | + Sort-Object -Unique + + if (-not $normalized -or $normalized.Count -eq 0) { + throw 'No target users resolved. Provide -Users or -UserListPath.' + } + + return @($normalized) +} + +function Get-ActivityWatchTaskNameToken { + param( + [Parameter(Mandatory = $true)] + [string]$UserId + ) + + $buffer = [Text.StringBuilder]::new() + foreach ($character in $UserId.ToCharArray()) { + if ([char]::IsLetterOrDigit($character)) { + [void]$buffer.Append($character) + } + else { + [void]$buffer.Append('_') + } + } + + return $buffer.ToString().Trim('_') +} + +function New-ActivityWatchUserTaskDefinitions { + param( + [Parameter(Mandatory = $true)] + [string[]]$Users + ) + + $result = foreach ($user in $Users) { + $token = Get-ActivityWatchTaskNameToken -UserId $user + [pscustomobject]@{ + UserId = $user + LaunchTaskName = "ActivityWatch Launch [$token]" + } + } + + return @($result) +} + +function Copy-ActivityWatchCollectorAssets { + param( + [Parameter(Mandatory = $true)] + [string]$CollectorScriptSource, + [Parameter(Mandatory = $true)] + [string]$EndpointCollectorScriptSource, + [Parameter(Mandatory = $true)] + [string]$ExampleRulesSource, + [Parameter(Mandatory = $true)] + [string]$ExamplePolicySource, + [Parameter(Mandatory = $true)] + [string]$StateRoot, + [string]$CustomRulesSource, + [string]$CustomPolicySource + ) + + New-ActivityWatchDirectory -Path $StateRoot + + $collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1' + $endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' + $exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json' + $rulesTarget = Join-Path $StateRoot 'web-category-rules.json' + $examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json' + $policyTarget = Join-Path $StateRoot 'dlp-policy.json' + + Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force + Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force + Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force + Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force + + if ($CustomRulesSource) { + $resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop + Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force + } + + if ($CustomPolicySource) { + $resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop + Copy-Item -LiteralPath $resolvedPolicy.Path -Destination $policyTarget -Force + } + else { + Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force + } + + return [pscustomobject]@{ + CollectorScript = $collectorTarget + EndpointCollectorScript = $endpointCollectorTarget + ExampleRules = $exampleRulesTarget + ActiveRules = $rulesTarget + ExamplePolicy = $examplePolicyTarget + ActivePolicy = $policyTarget + } +} + +function New-ActivityWatchDeploymentConfig { + param( + [Parameter(Mandatory = $true)] + [string]$ServerHost, + [Parameter(Mandatory = $true)] + [int]$ServerPort, + [Parameter(Mandatory = $true)] + [string]$ServerScheme, + [Parameter(Mandatory = $true)] + [string]$InstallRoot, + [Parameter(Mandatory = $true)] + [string]$StateRoot, + [Parameter(Mandatory = $true)] + [string]$LogsRoot, + [Parameter(Mandatory = $true)] + [string]$CollectorScript, + [Parameter(Mandatory = $true)] + [string]$EndpointCollectorScript, + [Parameter(Mandatory = $true)] + [string]$RulesPath, + [Parameter(Mandatory = $true)] + [string]$PolicyPath, + [Parameter(Mandatory = $true)] + [int]$PollSeconds, + [Parameter(Mandatory = $true)] + [int]$PulseSeconds, + [Parameter(Mandatory = $true)] + [int]$RecoveryIntervalSeconds, + [bool]$AfkEnabled = $true, + [bool]$WindowEnabled = $true, + [bool]$LocalAgentLogsEnabled = $true, + [bool]$IncidentCaptureEnabled = $true, + [bool]$IncidentScreenshotEnabled = $true, + [string]$IncidentArtifactsRoot, + [bool]$LogonMarkerEnabled = $true, + [Parameter(Mandatory = $true)] + [string]$LaunchScriptPath, + [Parameter(Mandatory = $true)] + [string]$RecoveryScriptPath, + [Parameter(Mandatory = $true)] + [pscustomobject[]]$UserTasks, + [string]$PackageVersion = 'v0.13.2' + ) + + $effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' } + + return [pscustomobject]@{ + version = 1 + generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') + server = [pscustomobject]@{ + host = $ServerHost + port = $ServerPort + scheme = $ServerScheme + } + paths = [pscustomobject]@{ + installRoot = $InstallRoot + stateRoot = $StateRoot + logsRoot = $LogsRoot + collectorScript = $CollectorScript + endpointCollectorScript = $EndpointCollectorScript + rulesPath = $RulesPath + policyPath = $PolicyPath + launchScript = $LaunchScriptPath + recoveryScript = $RecoveryScriptPath + } + collector = [pscustomobject]@{ + pollSeconds = $PollSeconds + pulseSeconds = $PulseSeconds + } + collectors = [pscustomobject]@{ + afkEnabled = $AfkEnabled + windowEnabled = $WindowEnabled + } + logging = [pscustomobject]@{ + localAgentLogsEnabled = $LocalAgentLogsEnabled + } + incidentCapture = [pscustomobject]@{ + enabled = $IncidentCaptureEnabled + screenshotEnabled = $IncidentScreenshotEnabled + artifactsRoot = $effectiveIncidentArtifactsRoot + } + sessionEvents = [pscustomobject]@{ + logonEnabled = $LogonMarkerEnabled + bucketPrefix = 'aw-session-events' + } + recovery = [pscustomobject]@{ + intervalSeconds = $RecoveryIntervalSeconds + taskName = 'ActivityWatch Recovery' + } + dlp = [pscustomobject]@{ + incidentBucketPrefix = 'aw-dlp-incidents' + enabled = $true + } + package = [pscustomobject]@{ + version = $PackageVersion + } + userTasks = @($UserTasks) + } +} + +function Write-ActivityWatchDeploymentConfig { + param( + [Parameter(Mandatory = $true)] + [pscustomobject]$Config, + [Parameter(Mandatory = $true)] + [string]$Path + ) + + $directory = Split-Path -Path $Path -Parent + if ($directory) { + New-ActivityWatchDirectory -Path $directory + } + + $json = $Config | ConvertTo-Json -Depth 8 + Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 +} + +function Read-ActivityWatchDeploymentConfig { + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + if (-not (Test-Path -LiteralPath $Path)) { + throw "Deployment config not found: $Path" + } + + return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json +} + +function Write-ActivityWatchLaunchScript { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + [Parameter(Mandatory = $true)] + [string]$ConfigPath + ) + + $content = @" +param( + [string]`$ConfigPath = '$ConfigPath' +) + +Set-StrictMode -Version Latest +`$ErrorActionPreference = 'Stop' + +function Get-DeploymentConfig { + param([string]`$Path) + return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json +} + +function Test-ProcessInSession { + param( + [string]`$Name, + [int]`$SessionId + ) + + return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1) +} + +function Test-CollectorRunning { + param( + [string]`$ScriptPath, + [int]`$SessionId + ) + + `$escapedCollector = [Regex]::Escape(`$ScriptPath) + `$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | + Where-Object { + (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and + `$_.SessionId -eq `$SessionId -and + `$_.CommandLine -match `$escapedCollector + } + + return [bool](`$processes | Select-Object -First 1) +} + +function Invoke-AwJsonPost { + param( + [Parameter(Mandatory = `$true)][string]`$Uri, + [Parameter(Mandatory = `$true)][string]`$Json + ) + + `$bytes = [Text.Encoding]::UTF8.GetBytes(`$Json) + Invoke-RestMethod -Method Post -Uri `$Uri -ContentType 'application/json; charset=utf-8' -Body `$bytes | Out-Null +} + +function Ensure-Bucket { + param( + [string]`$BucketId, + [string]`$ClientName, + [string]`$BucketType + ) + + if (`$script:KnownBuckets.ContainsKey(`$BucketId)) { + return + } + + try { + Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null + `$script:KnownBuckets[`$BucketId] = `$true + return + } + catch { + } + + `$body = @{ + client = `$ClientName + type = `$BucketType + hostname = `$script:Hostname + } | ConvertTo-Json -Compress + + try { + Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body + } + catch { + try { + Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null + } + catch { + return + } + } + + `$script:KnownBuckets[`$BucketId] = `$true +} + +function Send-LogonMarkerIfNeeded { + param( + [pscustomobject]`$Config, + [int]`$SessionId + ) + + `$sessionEvents = if (`$Config.PSObject.Properties.Name -contains 'sessionEvents') { `$Config.sessionEvents } else { `$null } + `$logging = if (`$Config.PSObject.Properties.Name -contains 'logging') { `$Config.logging } else { `$null } + `$logonEnabled = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]`$sessionEvents.logonEnabled } else { `$false } + if (-not `$logonEnabled) { + return + } + + `$bucketPrefix = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]`$sessionEvents.bucketPrefix)) { + [string]`$sessionEvents.bucketPrefix + } + else { + 'aw-session-events' + } + + `$stateRoot = [string]`$Config.paths.stateRoot + `$markerRoots = New-Object System.Collections.Generic.List[string] + if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) { + `$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'ActivityWatch-Phase2\markers')) + } + if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) { + `$markerRoots.Add((Join-Path `$stateRoot 'markers')) + } + + `$markerDir = `$null + foreach (`$candidate in `$markerRoots) { + try { + if (-not (Test-Path -LiteralPath `$candidate)) { + New-Item -Path `$candidate -ItemType Directory -Force | Out-Null + } + + `$probePath = Join-Path `$candidate 'write-test.tmp' + Set-Content -LiteralPath `$probePath -Value 'ok' -Encoding ASCII + Remove-Item -LiteralPath `$probePath -Force -ErrorAction SilentlyContinue + `$markerDir = `$candidate + break + } + catch { + } + } + + if (-not `$markerDir) { + return + } + + `$markerFile = Join-Path `$markerDir ("logon-{0}-{1}.marker" -f `$env:USERNAME, `$SessionId) + if (Test-Path -LiteralPath `$markerFile) { + return + } + + Set-Content -LiteralPath `$markerFile -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8 + + `$bucketId = ('{0}_{1}' -f `$bucketPrefix, `$script:Hostname) + Ensure-Bucket -BucketId `$bucketId -ClientName 'aw-session-events' -BucketType 'aw.session.event' + + `$payload = @{ + timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + duration = 0 + data = @{ + eventType = 'logon' + username = `$env:USERNAME + userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)" + sessionId = `$SessionId + hostname = `$script:Hostname + source = 'launch-watchers-phase2' + } + } | ConvertTo-Json -Depth 5 -Compress + + try { + Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$bucketId/heartbeat?pulsetime=1" -Json `$payload + } + catch { + Remove-Item -LiteralPath `$markerFile -Force -ErrorAction SilentlyContinue + throw + } +} + +function Start-CollectorScriptIfNeeded { + param( + [string]`$ScriptPath, + [string]`$ConfigPath, + [string]`$PowerShellExe, + [int]`$SessionId + ) + + if (-not (Test-Path -LiteralPath `$ScriptPath)) { + return + } + + if (Test-CollectorRunning -ScriptPath `$ScriptPath -SessionId `$SessionId) { + return + } + + Start-Process -FilePath `$PowerShellExe -ArgumentList @( + '-NoProfile', + '-WindowStyle', 'Hidden', + '-ExecutionPolicy', 'Bypass', + '-File', `$ScriptPath, + '-ConfigPath', `$ConfigPath + ) -WindowStyle Hidden +} + +`$config = Get-DeploymentConfig -Path `$ConfigPath +`$sessionId = (Get-Process -Id `$PID).SessionId +`$installRoot = [string]`$config.paths.installRoot +`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port +`$script:Hostname = `$env:COMPUTERNAME +`$script:KnownBuckets = @{} +`$collectorScript = [string]`$config.paths.collectorScript +`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { '' } +`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe' +`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe' +`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port) +`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' +`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true } +`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } + +if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) { + throw "Missing aw-watcher-afk.exe: `$afkExe" +} + +if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) { + throw "Missing aw-watcher-window.exe: `$windowExe" +} + +if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) { + Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden +} + +if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) { + Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden +} + +try { + Send-LogonMarkerIfNeeded -Config `$config -SessionId `$sessionId +} +catch { +} +Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId +Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId +"@ + + Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 +} + +function Write-ActivityWatchRecoveryScript { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + [Parameter(Mandatory = $true)] + [string]$ConfigPath + ) + + $content = @" +param( + [string]`$ConfigPath = '$ConfigPath' +) + +Set-StrictMode -Version Latest +`$ErrorActionPreference = 'Continue' + +function Get-DeploymentConfig { + param([string]`$Path) + return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json +} + +function Get-RecoveryConfigPaths { + param([string]`$PrimaryConfigPath) + + `$paths = New-Object System.Collections.Generic.List[string] + if (`$PrimaryConfigPath -and (Test-Path -LiteralPath `$PrimaryConfigPath)) { + `$paths.Add((Resolve-Path -LiteralPath `$PrimaryConfigPath).Path) + } + + `$searchRoot = `$env:ProgramData + if (`$PrimaryConfigPath) { + `$stateRoot = Split-Path -Path `$PrimaryConfigPath -Parent + `$candidateRoot = Split-Path -Path `$stateRoot -Parent + if (`$candidateRoot -and (Test-Path -LiteralPath `$candidateRoot)) { + `$searchRoot = `$candidateRoot + } + } + + if (Test-Path -LiteralPath `$searchRoot) { + Get-ChildItem -LiteralPath `$searchRoot -Directory -ErrorAction SilentlyContinue | + Where-Object { `$_.Name -like 'ActivityWatch*' } | + ForEach-Object { + `$candidate = Join-Path `$_.FullName 'deployment-config.json' + if (Test-Path -LiteralPath `$candidate) { + `$paths.Add(`$candidate) + } + } + } + + return @(`$paths | Sort-Object -Unique) +} + +function Get-RecoveryTaskNames { + param([string[]]`$ConfigPaths) + + `$taskNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) + foreach (`$candidatePath in @(`$ConfigPaths)) { + try { + `$config = Get-DeploymentConfig -Path `$candidatePath + foreach (`$task in @(`$config.userTasks)) { + `$taskName = [string]`$task.launchTaskName + if (-not [string]::IsNullOrWhiteSpace(`$taskName)) { + [void]`$taskNames.Add(`$taskName) + } + } + } + catch { + } + } + + return @(`$taskNames) +} + +while (`$true) { + `$sleepSeconds = 180 + try { + `$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath + foreach (`$taskName in Get-RecoveryTaskNames -ConfigPaths `$configPaths) { + Start-ScheduledTask -TaskName `$taskName -ErrorAction SilentlyContinue + } + + `$config = Get-DeploymentConfig -Path `$ConfigPath + if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) { + `$sleepSeconds = [Math]::Max([int]`$config.recovery.intervalSeconds, 30) + } + } + catch { + } + + Start-Sleep -Seconds `$sleepSeconds +} +"@ + + Set-Content -LiteralPath $Path -Value $content -Encoding UTF8 +} + +function Get-ActivityWatchHiddenLauncherPath { + param( + [Parameter(Mandatory = $true)] + [string]$ScriptPath + ) + + $directory = Split-Path -Path $ScriptPath -Parent + $baseName = [IO.Path]::GetFileNameWithoutExtension($ScriptPath) + return Join-Path $directory ("{0}-hidden.vbs" -f $baseName) +} + +function Write-ActivityWatchHiddenPowerShellWrapper { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + [Parameter(Mandatory = $true)] + [string]$ScriptPath, + [Parameter(Mandatory = $true)] + [string]$ConfigPath + ) + + $directory = Split-Path -Path $Path -Parent + if ($directory) { + New-ActivityWatchDirectory -Path $directory + } + + $powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' + $escapedPowerShellExe = $powershellExe.Replace('"', '""') + $escapedScriptPath = $ScriptPath.Replace('"', '""') + $escapedConfigPath = $ConfigPath.Replace('"', '""') + + $content = @" +Set shell = CreateObject("WScript.Shell") +shell.Run """$escapedPowerShellExe"" -NoProfile -ExecutionPolicy Bypass -File ""$escapedScriptPath"" -ConfigPath ""$escapedConfigPath""", 0, False +"@ + + Set-Content -LiteralPath $Path -Value $content -Encoding ASCII +} + +function Remove-LegacyActivityWatchEntries { + $legacyTaskNames = @( + 'ActivityWatch Watchers', + 'ActivityWatch Guard', + 'ActivityWatch Heal' + ) + + foreach ($taskName in $legacyTaskNames) { + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue + } + + $runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' + foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') { + Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue + } +} + +function Remove-ActivityWatchScheduledTask { + param( + [Parameter(Mandatory = $true)] + [string]$TaskName + ) + + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue + & cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null + + for ($attempt = 0; $attempt -lt 10; $attempt++) { + $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + if (-not $task) { + return + } + + Start-Sleep -Milliseconds 300 + } +} + +function Set-ActivityWatchScheduledTaskAction { + param( + [Parameter(Mandatory = $true)] + [string]$TaskName, + [Parameter(Mandatory = $true)] + [string]$Execute, + [Parameter(Mandatory = $true)] + [string]$Arguments + ) + + $taskCommand = ('"{0}" {1}' -f $Execute, $Arguments) + & schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "schtasks.exe /Change failed for $TaskName" + } +} + +function Get-ActivityWatchScheduledTaskByCommand { + param( + [Parameter(Mandatory = $true)] + [string]$TaskName, + [string]$CommandMatch + ) + + $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + if ($task) { + return $task + } + + if ([string]::IsNullOrWhiteSpace($CommandMatch)) { + return $null + } + + foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) { + foreach ($action in @($candidate.Actions)) { + if ([string]$action.Arguments -like "*$CommandMatch*") { + return $candidate + } + } + } + + return $null +} + +function Register-ActivityWatchUserTasks { + param( + [Parameter(Mandatory = $true)] + [pscustomobject[]]$TaskDefinitions, + [Parameter(Mandatory = $true)] + [string]$LaunchScriptPath, + [Parameter(Mandatory = $true)] + [string]$ConfigPath + ) + + $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' + $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath + Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath + + foreach ($definition in $TaskDefinitions) { + $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" + $trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId + $principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType Interactive -RunLevel Highest + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) + $existingTask = Get-ActivityWatchScheduledTaskByCommand -TaskName $definition.LaunchTaskName -CommandMatch $ConfigPath + + if ($existingTask) { + Set-ActivityWatchScheduledTaskAction -TaskName $existingTask.TaskName -Execute $wscriptExe -Arguments $action.Arguments + continue + } + + Remove-ActivityWatchScheduledTask -TaskName $definition.LaunchTaskName + Register-ScheduledTask -TaskName $definition.LaunchTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null + } +} + +function Register-ActivityWatchRecoveryTask { + param( + [Parameter(Mandatory = $true)] + [string]$TaskName, + [Parameter(Mandatory = $true)] + [string]$RecoveryScriptPath, + [Parameter(Mandatory = $true)] + [string]$ConfigPath + ) + + Remove-ActivityWatchScheduledTask -TaskName $TaskName + + $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe' + $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath + Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath + $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`"" + $trigger = New-ScheduledTaskTrigger -AtStartup + $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0) + + Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null +} + +function Set-ActivityWatchAcl { + param( + [Parameter(Mandatory = $true)] + [string]$InstallRoot, + [Parameter(Mandatory = $true)] + [string]$StateRoot, + [Parameter(Mandatory = $true)] + [string]$LogsRoot + ) + + foreach ($path in $InstallRoot, $StateRoot, $LogsRoot) { + New-ActivityWatchDirectory -Path $path + } + + & icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "icacls failed for $InstallRoot" + } + + & icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "icacls failed for $StateRoot" + } + + & icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "icacls failed for $LogsRoot" + } +} + +function Start-ActivityWatchTasks { + param( + [Parameter(Mandatory = $true)] + [pscustomobject[]]$TaskDefinitions, + [string]$RecoveryTaskName = 'ActivityWatch Recovery' + ) + + foreach ($definition in $TaskDefinitions) { + Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue + } + + Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue +} + +Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries diff --git a/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 b/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 new file mode 100755 index 0000000..220961e --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 @@ -0,0 +1,822 @@ +[CmdletBinding()] +param( + [string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json', + [string]$ServerHost, + [int]$ServerPort, + [ValidateSet('http', 'https')] + [string]$ServerScheme, + [string]$RulesPath, + [string]$PolicyPath, + [string]$LogPath, + [string]$IncidentLogPath, + [int]$PollSeconds, + [int]$PulseSeconds +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +Add-Type -AssemblyName UIAutomationClient +Add-Type -AssemblyName UIAutomationTypes + +Add-Type @" +using System; +using System.Runtime.InteropServices; +using System.Text; + +public static class NativeAwMethods { + [DllImport("user32.dll")] + public static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll")] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + public static extern int GetWindowTextLength(IntPtr hWnd); +} +"@ + +function Get-DeploymentConfig { + param([string]$Path) + if ($Path -and (Test-Path -LiteralPath $Path)) { + return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json + } + + return $null +} + +$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath +$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } +$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } +$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } +$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\ActivityWatch\web-category-rules.json' } +$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\ActivityWatch\dlp-policy.json' } +$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } +$resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pulseSeconds } else { 30 } +$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\ActivityWatch\logs' } +$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) } +$resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) } +$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } +$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'ActivityWatch-Phase2\\incident-artifacts' } +$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } + +if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { + New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null +} + +$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort +$script:Hostname = $env:COMPUTERNAME +$script:SessionId = (Get-Process -Id $PID).SessionId +$script:KnownBuckets = @{} +$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled +$script:LogPath = $resolvedLogPath +$script:IncidentLogPath = $resolvedIncidentLogPath +$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot +$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled +$script:ScreenshotTypesLoaded = $false +$script:IncidentState = @{} +$script:DlpRules = @() +$script:DlpDefaults = [ordered]@{ + enabled = $false + cooldownSeconds = 300 + action = 'log' + severity = 'low' +} +$script:BrowserMap = @{ + msedge = 'edge' + chrome = 'chrome' + brave = 'brave' + vivaldi = 'vivaldi' + opera = 'opera' + firefox = 'firefox' +} +$script:CategoryRules = @( + @{ Name = 'work_business_systems'; Group = 'work'; Domains = @('bitrix24.ru', '1c.ru', 'sbis.ru', 'kontur.ru', 'diadoc.ru', 'nalog.gov.ru', 'gosuslugi.ru') } + @{ Name = 'work_docs_collab'; Group = 'work'; Domains = @('office.com', 'sharepoint.com', 'docs.google.com', 'drive.google.com', 'notion.so', 'miro.com') } + @{ Name = 'work_dev'; Group = 'work'; Domains = @('github.com', 'gitlab.com', 'bitbucket.org', 'youtrack.cloud', 'atlassian.net') } + @{ Name = 'work_communication'; Group = 'work'; Domains = @('teams.microsoft.com', 'outlook.office.com', 'web.telegram.org', 'slack.com', 'zoom.us') } + @{ Name = 'neutral_search_reference'; Group = 'neutral'; Domains = @('google.com', 'google.ru', 'yandex.ru', 'bing.com', 'duckduckgo.com', 'wikipedia.org') } + @{ Name = 'neutral_news'; Group = 'neutral'; Domains = @('rbc.ru', 'tass.ru', 'ria.ru', 'kommersant.ru', 'vedomosti.ru') } + @{ Name = 'personal_social'; Group = 'personal'; Domains = @('vk.com', 'ok.ru', 'facebook.com', 'instagram.com', 'tiktok.com', 'x.com', 'twitter.com') } + @{ Name = 'personal_video'; Group = 'personal'; Domains = @('youtube.com', 'youtu.be', 'rutube.ru', 'twitch.tv', 'kinopoisk.ru') } + @{ Name = 'personal_marketplace'; Group = 'personal'; Domains = @('ozon.ru', 'wildberries.ru', 'avito.ru', 'aliexpress.com', 'market.yandex.ru') } + @{ Name = 'personal_entertainment'; Group = 'personal'; Domains = @('dzen.ru', 'pikabu.ru', 'dtf.ru', 'playground.ru') } +) + +function Write-CollectorLog { + param([string]$Message) + + if (-not $script:LocalAgentLogsEnabled) { + return + } + + try { + Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) + } + catch { + } +} + +function Write-DlpIncidentLog { + param([string]$Message) + + if (-not $script:LocalAgentLogsEnabled) { + return + } + + try { + Add-Content -LiteralPath $script:IncidentLogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) + } + catch { + } +} + +function Test-DomainMatch { + param( + [string]$DomainHost, + [string]$RuleDomain + ) + + if ([string]::IsNullOrWhiteSpace($DomainHost) -or [string]::IsNullOrWhiteSpace($RuleDomain)) { + return $false + } + + $left = $DomainHost.ToLowerInvariant() + $right = $RuleDomain.ToLowerInvariant() + return $left -eq $right -or $left.EndsWith('.' + $right) +} + +function Get-HostFromUrl { + param([string]$Url) + + if ([string]::IsNullOrWhiteSpace($Url)) { + return $null + } + + try { + $uri = [Uri]$Url + $host = $uri.Host.ToLowerInvariant() + if ($host.StartsWith('www.')) { + return $host.Substring(4) + } + + return $host + } + catch { + return $null + } +} + +function Get-RootDomain { + param([string]$DomainHost) + + if ([string]::IsNullOrWhiteSpace($DomainHost)) { + return $null + } + + $parts = $DomainHost.Split('.') + if ($parts.Count -le 2) { + return $DomainHost + } + + $suffix = ('{0}.{1}' -f $parts[$parts.Count - 2], $parts[$parts.Count - 1]).ToLowerInvariant() + $compoundTlds = @('co.uk', 'com.au', 'co.jp', 'com.br', 'co.in', 'com.tr', 'com.cn') + if (($compoundTlds -contains $suffix) -and $parts.Count -ge 3) { + return ('{0}.{1}' -f $parts[$parts.Count - 3], $suffix).ToLowerInvariant() + } + + return $suffix +} + +function ConvertTo-NormalizedUrl { + param([AllowNull()][string]$Value) + + if ([string]::IsNullOrWhiteSpace($Value)) { + return $null + } + + $candidate = $Value.Trim() + if ($candidate.Length -lt 4) { + return $null + } + + if ($candidate -match '^(?i)(search|find|address and search|search with|новая вкладка|new tab)') { + return $null + } + + if ($candidate -match '^(?i)(https?|file|ftp|chrome|edge|about|view-source)://') { + return $candidate + } + + if ($candidate -match '^(?i)localhost([/:]|$)') { + return "http://$candidate" + } + + if ($candidate -match '^[a-z0-9.-]+\.[a-z]{2,}([/:?#].*)?$') { + return "https://$candidate" + } + + return $null +} + +function Load-CustomCategoryRules { + param([string]$Path) + + if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { + return + } + + try { + $parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json + $rules = @() + + if ($parsed.rules) { + $sourceRules = @($parsed.rules) + } + elseif ($parsed -is [System.Collections.IEnumerable]) { + $sourceRules = @($parsed) + } + else { + $sourceRules = @() + } + + foreach ($rule in $sourceRules) { + if (-not $rule) { + continue + } + + $name = [string]$rule.name + $group = [string]$rule.group + $domains = @($rule.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) + + if ($name -and $group -and $domains.Count -gt 0) { + $rules += @{ + Name = $name + Group = $group + Domains = $domains + } + } + } + + if ($rules.Count -gt 0) { + $script:CategoryRules = @($rules) + @($script:CategoryRules) + Write-CollectorLog ("custom rules loaded: {0}" -f $rules.Count) + } + } + catch { + Write-CollectorLog ("custom rules load failed: {0}" -f $_.Exception.Message) + } +} + +function Get-WebCategory { + param([string]$DomainHost) + + foreach ($rule in $script:CategoryRules) { + foreach ($domain in $rule.Domains) { + if (Test-DomainMatch -DomainHost $DomainHost -RuleDomain $domain) { + return [pscustomobject]@{ + Name = [string]$rule.Name + Group = [string]$rule.Group + Rule = [string]$domain + } + } + } + } + + return [pscustomobject]@{ + Name = 'uncategorized' + Group = 'neutral' + Rule = 'none' + } +} + +function Test-DomainListMatch { + param( + [string]$DomainHost, + [string[]]$Domains + ) + + if (-not $Domains -or $Domains.Count -eq 0) { + return $false + } + + foreach ($domain in $Domains) { + if (Test-DomainMatch -DomainHost $DomainHost -RuleDomain $domain) { + return $true + } + } + + return $false +} + +function Test-DlpRuleTimeWindow { + param( + [int]$CurrentHour, + [AllowNull()][int]$HourFrom, + [AllowNull()][int]$HourTo + ) + + if ($null -eq $HourFrom -or $null -eq $HourTo) { + return $true + } + + if ($HourFrom -eq $HourTo) { + return $true + } + + if ($HourFrom -lt $HourTo) { + return ($CurrentHour -ge $HourFrom -and $CurrentHour -lt $HourTo) + } + + return ($CurrentHour -ge $HourFrom -or $CurrentHour -lt $HourTo) +} + +function Load-DlpPolicy { + param([string]$Path) + + if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { + Write-CollectorLog ("dlp policy not found, disabled: {0}" -f $Path) + return + } + + try { + $parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json + $defaults = $parsed.defaults + if ($defaults) { + if ($defaults.PSObject.Properties.Name -contains 'enabled') { + $script:DlpDefaults.enabled = [bool]$defaults.enabled + } + if ($defaults.cooldownSeconds) { + $script:DlpDefaults.cooldownSeconds = [int]$defaults.cooldownSeconds + } + if ($defaults.action) { + $script:DlpDefaults.action = [string]$defaults.action + } + if ($defaults.severity) { + $script:DlpDefaults.severity = [string]$defaults.severity + } + } + + $loaded = @() + foreach ($rule in @($parsed.rules)) { + if (-not $rule) { continue } + $when = $rule.when + if (-not $when) { + $when = [pscustomobject]@{} + } + $loaded += [pscustomobject]@{ + id = [string]$rule.id + enabled = if ($rule.PSObject.Properties.Name -contains 'enabled') { [bool]$rule.enabled } else { $true } + action = if ($rule.action) { [string]$rule.action } else { [string]$script:DlpDefaults.action } + severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:DlpDefaults.severity } + message = if ($rule.message) { [string]$rule.message } else { "DLP rule matched: $($rule.id)" } + cooldownSeconds = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:DlpDefaults.cooldownSeconds } + when = [pscustomobject]@{ + domains = if ($when.PSObject.Properties.Name -contains 'domains') { @($when.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } + categoryGroups = if ($when.PSObject.Properties.Name -contains 'categoryGroups') { @($when.categoryGroups | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } + categories = if ($when.PSObject.Properties.Name -contains 'categories') { @($when.categories | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } + browsers = if ($when.PSObject.Properties.Name -contains 'browsers') { @($when.browsers | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } + urlRegex = if ($when.PSObject.Properties.Name -contains 'urlRegex' -and $when.urlRegex) { [string]$when.urlRegex } else { $null } + titleRegex = if ($when.PSObject.Properties.Name -contains 'titleRegex' -and $when.titleRegex) { [string]$when.titleRegex } else { $null } + hourFrom = if ($when.PSObject.Properties.Name -contains 'hourFrom') { [int]$when.hourFrom } else { $null } + hourTo = if ($when.PSObject.Properties.Name -contains 'hourTo') { [int]$when.hourTo } else { $null } + } + } + } + + $script:DlpRules = @($loaded) + Write-CollectorLog ("dlp policy loaded: enabled={0}, rules={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count) + } + catch { + Write-CollectorLog ("dlp policy parse failed: {0}" -f $_.Exception.Message) + } +} + +function Test-DlpRuleMatch { + param( + [pscustomobject]$Rule, + [string]$Domain, + [string]$RootDomain, + [string]$Url, + [string]$Title, + [string]$BrowserKey, + [string]$Category, + [string]$CategoryGroup + ) + + if (-not $Rule.enabled) { + return $false + } + + $when = $Rule.when + $currentHour = (Get-Date).Hour + if (-not (Test-DlpRuleTimeWindow -CurrentHour $currentHour -HourFrom $when.hourFrom -HourTo $when.hourTo)) { + return $false + } + + if ($when.domains.Count -gt 0) { + $domainMatched = (Test-DomainListMatch -DomainHost $Domain -Domains $when.domains) -or (Test-DomainListMatch -DomainHost $RootDomain -Domains $when.domains) + if (-not $domainMatched) { + return $false + } + } + + if ($when.categoryGroups.Count -gt 0 -and ($when.categoryGroups -notcontains $CategoryGroup.ToLowerInvariant())) { + return $false + } + + if ($when.categories.Count -gt 0 -and ($when.categories -notcontains $Category.ToLowerInvariant())) { + return $false + } + + if ($when.browsers.Count -gt 0 -and ($when.browsers -notcontains $BrowserKey.ToLowerInvariant())) { + return $false + } + + if ($when.urlRegex) { + if (-not ($Url -match $when.urlRegex)) { + return $false + } + } + + if ($when.titleRegex) { + if (-not ($Title -match $when.titleRegex)) { + return $false + } + } + + return $true +} + +function Get-DlpDecision { + param( + [string]$Domain, + [string]$RootDomain, + [string]$Url, + [string]$Title, + [string]$BrowserKey, + [string]$Category, + [string]$CategoryGroup + ) + + if (-not $script:DlpDefaults.enabled) { + return $null + } + + foreach ($rule in $script:DlpRules) { + if (Test-DlpRuleMatch -Rule $rule -Domain $Domain -RootDomain $RootDomain -Url $Url -Title $Title -BrowserKey $BrowserKey -Category $Category -CategoryGroup $CategoryGroup) { + return $rule + } + } + + return $null +} + +function Should-EmitIncident { + param( + [string]$Fingerprint, + [int]$CooldownSeconds + ) + + $now = (Get-Date).ToUniversalTime() + if ($script:IncidentState.ContainsKey($Fingerprint)) { + $last = [datetime]$script:IncidentState[$Fingerprint] + if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { + return $false + } + } + + $script:IncidentState[$Fingerprint] = $now + return $true +} + +function Send-DlpIncidentHeartbeat { + param( + [pscustomobject]$Decision, + [string]$Url, + [string]$Title, + [string]$BrowserKey, + [string]$ProcessName, + [string]$Domain, + [string]$RootDomain, + [string]$Category, + [string]$CategoryGroup + ) + + $bucketId = 'aw-dlp-incidents_' + $script:Hostname + Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' + + $captureData = @{} + if ($script:IncidentScreenshotEnabled) { + try { + $captureData = Capture-IncidentScreenshot -RuleId ([string]$Decision.id) -SignalType 'web' + } + catch { + } + } + + $event = @{ + timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + duration = 0 + data = @{ + ruleId = [string]$Decision.id + action = [string]$Decision.action + severity = [string]$Decision.severity + message = [string]$Decision.message + url = $Url + title = $Title + browser = $BrowserKey + app = "$ProcessName.exe" + domain = $Domain + rootDomain = $RootDomain + category = $Category + categoryGroup = $CategoryGroup + username = $env:USERNAME + hostname = $script:Hostname + sessionId = $script:SessionId + source = 'uia-native-dlp' + } + $captureData + } | ConvertTo-Json -Depth 5 -Compress + + Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event | Out-Null +} + +function Get-FileSha256Hex { + param([Parameter(Mandatory = $true)][string]$Path) + try { + $sha = [Security.Cryptography.SHA256]::Create() + $stream = [IO.File]::OpenRead($Path) + try { + ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' + } + finally { + $stream.Dispose() + $sha.Dispose() + } + } + catch { + return $null + } +} + +function Ensure-Directory { + param([Parameter(Mandatory = $true)][string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { + New-Item -Path $Path -ItemType Directory -Force | Out-Null + } +} + +function Get-IncidentScreenshotPath { + param( + [Parameter(Mandatory = $true)][string]$RuleId, + [Parameter(Mandatory = $true)][string]$SignalType + ) + + $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') + $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') + $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') + $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') + $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule + $file = '{0}_{1}' -f $stamp, $file + return (Join-Path $script:IncidentArtifactsRoot $file) +} + +function Ensure-ScreenshotTypesLoaded { + if ($script:ScreenshotTypesLoaded) { + return + } + Add-Type -AssemblyName System.Windows.Forms | Out-Null + Add-Type -AssemblyName System.Drawing | Out-Null + $script:ScreenshotTypesLoaded = $true +} + +function Capture-IncidentScreenshot { + param( + [Parameter(Mandatory = $true)][string]$RuleId, + [Parameter(Mandatory = $true)][string]$SignalType + ) + + try { + Ensure-Directory -Path $script:IncidentArtifactsRoot + Ensure-ScreenshotTypesLoaded + + $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen + $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) + $gfx = [System.Drawing.Graphics]::FromImage($bmp) + try { + $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) + $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType + $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) + } + finally { + $gfx.Dispose() + $bmp.Dispose() + } + + return @{ + screenshotPath = $path + screenshotFormat = 'png' + screenshotWidth = [int]$vs.Width + screenshotHeight = [int]$vs.Height + screenshotSha256 = (Get-FileSha256Hex -Path $path) + } + } + catch { + Write-CollectorLog ("screenshot capture failed: {0}" -f $_.Exception.Message) + return @{} + } +} + +function Get-ForegroundWindowContext { + $handle = [NativeAwMethods]::GetForegroundWindow() + if ($handle -eq [IntPtr]::Zero) { + return $null + } + + $processId = [uint32]0 + [void][NativeAwMethods]::GetWindowThreadProcessId($handle, [ref]$processId) + if (-not $processId) { + return $null + } + + $process = Get-Process -Id ([int]$processId) -ErrorAction SilentlyContinue + if (-not $process) { + return $null + } + + $textLength = [NativeAwMethods]::GetWindowTextLength($handle) + $builder = [Text.StringBuilder]::new([Math]::Max($textLength + 1, 260)) + [void][NativeAwMethods]::GetWindowText($handle, $builder, $builder.Capacity) + + return [pscustomobject]@{ + Handle = $handle + ProcessName = $process.ProcessName.ToLowerInvariant() + Title = $builder.ToString() + } +} + +function Get-BrowserUrlFromWindow { + param([IntPtr]$Handle) + + $root = [System.Windows.Automation.AutomationElement]::FromHandle($Handle) + if (-not $root) { + return $null + } + + $editCondition = [System.Windows.Automation.PropertyCondition]::new( + [System.Windows.Automation.AutomationElement]::ControlTypeProperty, + [System.Windows.Automation.ControlType]::Edit + ) + + $edits = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $editCondition) + foreach ($edit in $edits) { + $valuePattern = $null + if ($edit.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$valuePattern)) { + $candidate = ConvertTo-NormalizedUrl -Value $valuePattern.Current.Value + if ($candidate) { + return $candidate + } + } + + $candidateFromName = ConvertTo-NormalizedUrl -Value $edit.Current.Name + if ($candidateFromName) { + return $candidateFromName + } + } + + return $null +} + +function Ensure-Bucket { + param( + [string]$BucketId, + [string]$ClientName, + [string]$BucketType = 'web.tab.current' + ) + + if ($script:KnownBuckets.ContainsKey($BucketId)) { + return + } + + $body = @{ + client = $ClientName + type = $BucketType + hostname = $script:Hostname + } | ConvertTo-Json -Compress + + Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId" -ContentType 'application/json' -Body $body | Out-Null + $script:KnownBuckets[$BucketId] = $true +} + +function Send-Heartbeat { + param( + [string]$BucketId, + [string]$Url, + [string]$Title, + [string]$BrowserKey, + [string]$ProcessName + ) + + $event = @{ + timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + duration = 0 + data = @{ + url = $Url + title = $Title + browser = $BrowserKey + app = "$ProcessName.exe" + source = 'uia-native' + sessionId = $script:SessionId + } + } | ConvertTo-Json -Depth 4 -Compress + + Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event | Out-Null +} + +function Send-CategoryHeartbeat { + param( + [string]$Url, + [string]$Title, + [string]$BrowserKey, + [string]$ProcessName, + [string]$Domain, + [string]$RootDomain, + [string]$Category, + [string]$CategoryGroup, + [string]$CategoryRule + ) + + $bucketId = 'aw-detmir-web-category_' + $script:Hostname + Ensure-Bucket -BucketId $bucketId -ClientName 'aw-detmir-web-category' -BucketType 'aw.web.category' + + $event = @{ + timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + duration = 0 + data = @{ + url = $Url + title = $Title + browser = $BrowserKey + app = "$ProcessName.exe" + domain = $Domain + rootDomain = $RootDomain + category = $Category + categoryGroup = $CategoryGroup + categoryRule = $CategoryRule + source = 'uia-native' + sessionId = $script:SessionId + } + } | ConvertTo-Json -Depth 4 -Compress + + Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event | Out-Null +} + +Load-CustomCategoryRules -Path $resolvedRulesPath +Load-DlpPolicy -Path $resolvedPolicyPath +Write-CollectorLog ("collector started against {0}" -f $script:ApiBase) + +while ($true) { + try { + $context = Get-ForegroundWindowContext + if ($context -and $script:BrowserMap.ContainsKey($context.ProcessName)) { + $url = Get-BrowserUrlFromWindow -Handle $context.Handle + if ($url) { + $browserKey = $script:BrowserMap[$context.ProcessName] + $domain = Get-HostFromUrl -Url $url + if (-not $domain) { + $domain = 'unknown' + } + + $rootDomain = Get-RootDomain -DomainHost $domain + if (-not $rootDomain) { + $rootDomain = $domain + } + + $category = Get-WebCategory -DomainHost $domain + $bucketId = 'aw-watcher-web-{0}_{1}' -f $browserKey, $script:Hostname + Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey) + Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName + Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule + + $decision = Get-DlpDecision -Domain $domain -RootDomain $rootDomain -Url $url -Title $context.Title -BrowserKey $browserKey -Category $category.Name -CategoryGroup $category.Group + if ($decision) { + $fingerprint = '{0}|{1}|{2}|{3}' -f $decision.id, $browserKey, $rootDomain, $env:USERNAME + $cooldown = [Math]::Max([int]$decision.cooldownSeconds, 30) + if (Should-EmitIncident -Fingerprint $fingerprint -CooldownSeconds $cooldown) { + Write-DlpIncidentLog ("{0} {1} {2} {3}" -f $decision.severity, $decision.action, $decision.id, $url) + if (@('alert', 'block', 'quarantine') -contains ([string]$decision.action).ToLowerInvariant()) { + Send-DlpIncidentHeartbeat -Decision $decision -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group + } + } + } + } + } + } + catch { + Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message) + } + + Start-Sleep -Seconds $resolvedPollSeconds +} diff --git a/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 new file mode 100755 index 0000000..936ce3c --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 @@ -0,0 +1,107 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$ServerHost, + [string[]]$Users, + [string]$UserListPath, + [string]$Domain, + [int]$ServerPort = 5600, + [ValidateSet('http', 'https')] + [string]$ServerScheme = 'http', + [string]$Version = 'v0.13.2', + [string]$PackageUrl, + [string]$PackageZipPath, + [string]$InstallRoot = 'C:\Program Files\ActivityWatch', + [string]$StateRoot = 'C:\ProgramData\ActivityWatch', + [int]$PollSeconds = 5, + [int]$PulseSeconds = 30, + [int]$RecoveryIntervalSeconds = 180, + [bool]$AfkEnabled = $true, + [bool]$WindowEnabled = $true, + [bool]$LocalAgentLogsEnabled = $false, + [bool]$IncidentCaptureEnabled = $true, + [bool]$IncidentScreenshotEnabled = $true, + [string]$IncidentArtifactsRoot, + [bool]$LogonMarkerEnabled = $true, + [string]$CustomRulesPath, + [string]$CustomPolicyPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' +Import-Module $modulePath -Force + +Assert-Administrator + +$targetUsers = Normalize-ActivityWatchUsers -Users $Users -UserListPath $UserListPath -Domain $Domain +$workingRoot = Join-Path $env:TEMP 'activitywatch-windows-deploy' +$backupRoot = Join-Path $StateRoot 'backups' +$logsRoot = Join-Path $StateRoot 'logs' +$configPath = Join-Path $StateRoot 'deployment-config.json' +$launchScriptPath = Join-Path $StateRoot 'launch-watchers.ps1' +$recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1' +$collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1' +$endpointCollectorSource = Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1' +$exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json' +$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json' + +New-ActivityWatchDirectory -Path $StateRoot +New-ActivityWatchDirectory -Path $logsRoot + +$archivePath = Get-ActivityWatchArchive -PackageZipPath $PackageZipPath -PackageUrl $PackageUrl -Version $Version -WorkingRoot $workingRoot +Install-ActivityWatchPackage -ArchivePath $archivePath -InstallRoot $InstallRoot -WorkingRoot $workingRoot -BackupRoot $backupRoot | Out-Null +Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null + +$assetResult = Copy-ActivityWatchCollectorAssets ` + -CollectorScriptSource $collectorSource ` + -EndpointCollectorScriptSource $endpointCollectorSource ` + -ExampleRulesSource $exampleRulesSource ` + -ExamplePolicySource $examplePolicySource ` + -StateRoot $StateRoot ` + -CustomRulesSource $CustomRulesPath ` + -CustomPolicySource $CustomPolicyPath +$taskDefinitions = New-ActivityWatchUserTaskDefinitions -Users $targetUsers + +Write-ActivityWatchLaunchScript -Path $launchScriptPath -ConfigPath $configPath +Write-ActivityWatchRecoveryScript -Path $recoveryScriptPath -ConfigPath $configPath + +$config = New-ActivityWatchDeploymentConfig ` + -ServerHost $ServerHost ` + -ServerPort $ServerPort ` + -ServerScheme $ServerScheme ` + -InstallRoot $InstallRoot ` + -StateRoot $StateRoot ` + -LogsRoot $logsRoot ` + -CollectorScript $assetResult.CollectorScript ` + -EndpointCollectorScript $assetResult.EndpointCollectorScript ` + -RulesPath $assetResult.ActiveRules ` + -PolicyPath $assetResult.ActivePolicy ` + -PollSeconds $PollSeconds ` + -PulseSeconds $PulseSeconds ` + -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` + -AfkEnabled $AfkEnabled ` + -WindowEnabled $WindowEnabled ` + -LocalAgentLogsEnabled $LocalAgentLogsEnabled ` + -IncidentCaptureEnabled $IncidentCaptureEnabled ` + -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` + -IncidentArtifactsRoot $IncidentArtifactsRoot ` + -LogonMarkerEnabled $LogonMarkerEnabled ` + -LaunchScriptPath $launchScriptPath ` + -RecoveryScriptPath $recoveryScriptPath ` + -UserTasks $taskDefinitions ` + -PackageVersion $Version + +Write-ActivityWatchDeploymentConfig -Config $config -Path $configPath +Remove-LegacyActivityWatchEntries +Set-ActivityWatchAcl -InstallRoot $InstallRoot -StateRoot $StateRoot -LogsRoot $logsRoot +Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $launchScriptPath -ConfigPath $configPath +Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath +Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName + +Write-Host 'ActivityWatch deployed for users:' +$targetUsers | ForEach-Object { Write-Host " - $_" } +Write-Host "Server: ${ServerScheme}://$ServerHost`:$ServerPort" +Write-Host "State root: $StateRoot" +Write-Host "Policy file: $($assetResult.ActivePolicy)" diff --git a/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 new file mode 100644 index 0000000..129d226 --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 @@ -0,0 +1,137 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$ServerHost, + [string[]]$Users, + [string]$UserListPath, + [string]$Domain, + [int]$ServerPort = 5600, + [ValidateSet('http', 'https')] + [string]$ServerScheme = 'http', + [string]$Version = 'v0.13.2', + [string]$PackageUrl, + [string]$PackageZipPath, + [string]$InstallRoot = 'C:\Program Files\ActivityWatch', + [string]$StateRoot = 'C:\ProgramData\ActivityWatch', + [int]$PollSeconds = 5, + [int]$PulseSeconds = 30, + [int]$RecoveryIntervalSeconds = 180, + [bool]$AfkEnabled = $true, + [bool]$WindowEnabled = $true, + [bool]$LocalAgentLogsEnabled = $false, + [bool]$IncidentCaptureEnabled = $true, + [bool]$IncidentScreenshotEnabled = $true, + [string]$IncidentArtifactsRoot, + [bool]$LogonMarkerEnabled = $true, + [string]$CustomRulesPath, + [string]$CustomPolicyPath, + [string]$ReportPath, + [switch]$SkipHardening, + [switch]$ValidateAfterDeploy +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' +Import-Module $modulePath -Force + +Assert-Administrator + +$resolvedUsers = Normalize-ActivityWatchUsers -Users $Users -UserListPath $UserListPath -Domain $Domain +$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' +$effectiveReportPath = if ($ReportPath) { $ReportPath } else { Join-Path $StateRoot "ensemble-report-$timestamp.json" } +$deployScript = Join-Path $PSScriptRoot 'deploy-domain-users.ps1' +$hardeningScript = Join-Path $PSScriptRoot 'hardening-recovery.ps1' +$validationScript = Join-Path $PSScriptRoot 'validate-deployment.ps1' + +if (-not (Test-Path -LiteralPath $deployScript)) { + throw "Missing script: $deployScript" +} + +& $deployScript ` + -ServerHost $ServerHost ` + -Users $resolvedUsers ` + -ServerPort $ServerPort ` + -ServerScheme $ServerScheme ` + -Version $Version ` + -PackageUrl $PackageUrl ` + -PackageZipPath $PackageZipPath ` + -InstallRoot $InstallRoot ` + -StateRoot $StateRoot ` + -PollSeconds $PollSeconds ` + -PulseSeconds $PulseSeconds ` + -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` + -AfkEnabled $AfkEnabled ` + -WindowEnabled $WindowEnabled ` + -LocalAgentLogsEnabled $LocalAgentLogsEnabled ` + -IncidentCaptureEnabled $IncidentCaptureEnabled ` + -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` + -IncidentArtifactsRoot $IncidentArtifactsRoot ` + -LogonMarkerEnabled $LogonMarkerEnabled ` + -CustomRulesPath $CustomRulesPath ` + -CustomPolicyPath $CustomPolicyPath + +if (-not $SkipHardening) { + & $hardeningScript ` + -ConfigPath (Join-Path $StateRoot 'deployment-config.json') ` + -ServerHost $ServerHost ` + -ServerPort $ServerPort ` + -ServerScheme $ServerScheme ` + -Users $resolvedUsers ` + -InstallRoot $InstallRoot ` + -StateRoot $StateRoot ` + -PollSeconds $PollSeconds ` + -PulseSeconds $PulseSeconds ` + -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` + -AfkEnabled $AfkEnabled ` + -WindowEnabled $WindowEnabled ` + -LocalAgentLogsEnabled $LocalAgentLogsEnabled ` + -IncidentCaptureEnabled $IncidentCaptureEnabled ` + -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` + -IncidentArtifactsRoot $IncidentArtifactsRoot ` + -LogonMarkerEnabled $LogonMarkerEnabled ` + -CustomRulesPath $CustomRulesPath ` + -CustomPolicyPath $CustomPolicyPath +} + +$report = [ordered]@{ + generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') + server = [ordered]@{ + host = $ServerHost + port = $ServerPort + scheme = $ServerScheme + } + packageVersion = $Version + users = @($resolvedUsers) + paths = [ordered]@{ + installRoot = $InstallRoot + stateRoot = $StateRoot + configPath = Join-Path $StateRoot 'deployment-config.json' + } + collectors = [ordered]@{ + afkEnabled = $AfkEnabled + windowEnabled = $WindowEnabled + } + hardeningApplied = (-not $SkipHardening) +} + +if ($ValidateAfterDeploy) { + if (-not (Test-Path -LiteralPath $validationScript)) { + throw "Missing script: $validationScript" + } + + $validation = & $validationScript -ConfigPath (Join-Path $StateRoot 'deployment-config.json') + $report.validation = $validation +} + +$reportDirectory = Split-Path -Path $effectiveReportPath -Parent +if ($reportDirectory) { + New-ActivityWatchDirectory -Path $reportDirectory +} + +$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8 + +Write-Host 'ActivityWatch ensemble deploy completed.' +Write-Host "Users: $($resolvedUsers -join ', ')" +Write-Host "Report: $effectiveReportPath" diff --git a/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 new file mode 100755 index 0000000..d3eafef --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 @@ -0,0 +1,106 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$ServerHost, + [Parameter(Mandatory = $true)] + [string]$TargetUser, + [int]$ServerPort = 5600, + [ValidateSet('http', 'https')] + [string]$ServerScheme = 'http', + [string]$Version = 'v0.13.2', + [string]$PackageUrl, + [string]$PackageZipPath, + [string]$InstallRoot = 'C:\Program Files\ActivityWatch', + [string]$StateRoot = 'C:\ProgramData\ActivityWatch', + [int]$PollSeconds = 5, + [int]$PulseSeconds = 30, + [int]$RecoveryIntervalSeconds = 180, + [bool]$AfkEnabled = $true, + [bool]$WindowEnabled = $true, + [bool]$LocalAgentLogsEnabled = $false, + [bool]$IncidentCaptureEnabled = $true, + [bool]$IncidentScreenshotEnabled = $true, + [string]$IncidentArtifactsRoot, + [bool]$LogonMarkerEnabled = $true, + [string]$CustomRulesPath, + [string]$CustomPolicyPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' +Import-Module $modulePath -Force + +Assert-Administrator + +$workingRoot = Join-Path $env:TEMP 'activitywatch-windows-deploy' +$backupRoot = Join-Path $StateRoot 'backups' +$logsRoot = Join-Path $StateRoot 'logs' +$configPath = Join-Path $StateRoot 'deployment-config.json' +$launchScriptPath = Join-Path $StateRoot 'launch-watchers.ps1' +$recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1' +$collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1' +$endpointCollectorSource = Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1' +$exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json' +$examplePolicySource = Join-Path $PSScriptRoot 'dlp-policy.example.json' + +New-ActivityWatchDirectory -Path $StateRoot +New-ActivityWatchDirectory -Path $logsRoot + +$archivePath = Get-ActivityWatchArchive -PackageZipPath $PackageZipPath -PackageUrl $PackageUrl -Version $Version -WorkingRoot $workingRoot +Install-ActivityWatchPackage -ArchivePath $archivePath -InstallRoot $InstallRoot -WorkingRoot $workingRoot -BackupRoot $backupRoot | Out-Null +Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null + +$assetResult = Copy-ActivityWatchCollectorAssets ` + -CollectorScriptSource $collectorSource ` + -EndpointCollectorScriptSource $endpointCollectorSource ` + -ExampleRulesSource $exampleRulesSource ` + -ExamplePolicySource $examplePolicySource ` + -StateRoot $StateRoot ` + -CustomRulesSource $CustomRulesPath ` + -CustomPolicySource $CustomPolicyPath +$taskDefinitions = New-ActivityWatchUserTaskDefinitions -Users @($TargetUser) + +Write-ActivityWatchLaunchScript -Path $launchScriptPath -ConfigPath $configPath +Write-ActivityWatchRecoveryScript -Path $recoveryScriptPath -ConfigPath $configPath + +$config = New-ActivityWatchDeploymentConfig ` + -ServerHost $ServerHost ` + -ServerPort $ServerPort ` + -ServerScheme $ServerScheme ` + -InstallRoot $InstallRoot ` + -StateRoot $StateRoot ` + -LogsRoot $logsRoot ` + -CollectorScript $assetResult.CollectorScript ` + -EndpointCollectorScript $assetResult.EndpointCollectorScript ` + -RulesPath $assetResult.ActiveRules ` + -PolicyPath $assetResult.ActivePolicy ` + -PollSeconds $PollSeconds ` + -PulseSeconds $PulseSeconds ` + -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` + -AfkEnabled $AfkEnabled ` + -WindowEnabled $WindowEnabled ` + -LocalAgentLogsEnabled $LocalAgentLogsEnabled ` + -IncidentCaptureEnabled $IncidentCaptureEnabled ` + -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` + -IncidentArtifactsRoot $IncidentArtifactsRoot ` + -LogonMarkerEnabled $LogonMarkerEnabled ` + -LaunchScriptPath $launchScriptPath ` + -RecoveryScriptPath $recoveryScriptPath ` + -UserTasks $taskDefinitions ` + -PackageVersion $Version + +Write-ActivityWatchDeploymentConfig -Config $config -Path $configPath +Remove-LegacyActivityWatchEntries +Set-ActivityWatchAcl -InstallRoot $InstallRoot -StateRoot $StateRoot -LogsRoot $logsRoot +Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $launchScriptPath -ConfigPath $configPath +Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath +Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName + +Write-Host "ActivityWatch deployed for $TargetUser" +Write-Host "Server: ${ServerScheme}://$ServerHost`:$ServerPort" +Write-Host "Install root: $InstallRoot" +Write-Host "State root: $StateRoot" +Write-Host "Rules file: $($assetResult.ActiveRules)" +Write-Host "Policy file: $($assetResult.ActivePolicy)" diff --git a/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 b/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 new file mode 100644 index 0000000..8ee44f5 --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 @@ -0,0 +1,796 @@ +[CmdletBinding()] +param( + [string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json', + [string]$ServerHost, + [int]$ServerPort, + [ValidateSet('http', 'https')] + [string]$ServerScheme, + [string]$PolicyPath, + [string]$LogPath, + [int]$PollSeconds +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Get-DeploymentConfig { + param([string]$Path) + if ($Path -and (Test-Path -LiteralPath $Path)) { + return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json + } + return $null +} + +function Write-EndpointLog { + param([string]$Message) + if (-not $script:LocalAgentLogsEnabled) { + return + } + try { + Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) + } + catch { + } +} + +function Invoke-AwJsonPost { + param( + [Parameter(Mandatory = $true)][string]$Uri, + [Parameter(Mandatory = $true)][string]$Json + ) + + $bytes = [Text.Encoding]::UTF8.GetBytes($Json) + Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null +} + +function Ensure-Bucket { + param( + [string]$BucketId, + [string]$ClientName, + [string]$BucketType + ) + + if ($script:KnownBuckets.ContainsKey($BucketId)) { + return + } + + $body = @{ + client = $ClientName + type = $BucketType + hostname = $script:Hostname + } | ConvertTo-Json -Compress + + Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body + $script:KnownBuckets[$BucketId] = $true +} + +function Send-EndpointSignalHeartbeat { + param( + [string]$SignalType, + [hashtable]$Data + ) + + $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname + Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' + + $payload = @{ + timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + duration = 0 + data = @{ + signalType = $SignalType + username = $env:USERNAME + sessionId = $script:SessionId + hostname = $script:Hostname + source = 'endpoint-signals-phase2' + } + $Data + } | ConvertTo-Json -Depth 6 -Compress + + Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload +} + +function Send-DlpIncidentHeartbeat { + param( + [string]$RuleId, + [string]$Action, + [string]$Severity, + [string]$Message, + [string]$SignalType, + [hashtable]$Data + ) + + $bucketId = 'aw-dlp-incidents_' + $script:Hostname + Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' + + $captureData = @{} + if ($script:IncidentScreenshotEnabled) { + try { + $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType + } + catch { + } + } + + $payload = @{ + timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + duration = 0 + data = @{ + ruleId = $RuleId + action = $Action + severity = $Severity + message = $Message + signalType = $SignalType + username = $env:USERNAME + sessionId = $script:SessionId + hostname = $script:Hostname + source = 'endpoint-signals-phase2' + } + $Data + $captureData + } | ConvertTo-Json -Depth 7 -Compress + + Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload +} + +function Get-FileSha256Hex { + param([Parameter(Mandatory = $true)][string]$Path) + try { + $sha = [Security.Cryptography.SHA256]::Create() + $stream = [IO.File]::OpenRead($Path) + try { + ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' + } + finally { + $stream.Dispose() + $sha.Dispose() + } + } + catch { + return $null + } +} + +function Ensure-Directory { + param([Parameter(Mandatory = $true)][string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { + New-Item -Path $Path -ItemType Directory -Force | Out-Null + } +} + +function Get-IncidentScreenshotPath { + param( + [Parameter(Mandatory = $true)][string]$RuleId, + [Parameter(Mandatory = $true)][string]$SignalType + ) + + $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') + $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') + $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') + $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') + $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule + $file = '{0}_{1}' -f $stamp, $file + return (Join-Path $script:IncidentArtifactsRoot $file) +} + +function Ensure-ScreenshotTypesLoaded { + if ($script:ScreenshotTypesLoaded) { + return + } + Add-Type -AssemblyName System.Windows.Forms | Out-Null + Add-Type -AssemblyName System.Drawing | Out-Null + $script:ScreenshotTypesLoaded = $true +} + +function Capture-IncidentScreenshot { + param( + [Parameter(Mandatory = $true)][string]$RuleId, + [Parameter(Mandatory = $true)][string]$SignalType + ) + + try { + Ensure-Directory -Path $script:IncidentArtifactsRoot + Ensure-ScreenshotTypesLoaded + + $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen + $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) + $gfx = [System.Drawing.Graphics]::FromImage($bmp) + try { + $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) + $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType + $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) + } + finally { + $gfx.Dispose() + $bmp.Dispose() + } + + return @{ + screenshotPath = $path + screenshotFormat = 'png' + screenshotWidth = [int]$vs.Width + screenshotHeight = [int]$vs.Height + screenshotSha256 = (Get-FileSha256Hex -Path $path) + } + } + catch { + Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) + return @{} + } +} + +function Get-StringHash { + param([AllowNull()][string]$Value) + if ($null -eq $Value) { return $null } + $bytes = [Text.Encoding]::UTF8.GetBytes($Value) + $sha = [Security.Cryptography.SHA256]::Create() + try { + ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' + } + finally { + $sha.Dispose() + } +} + +function Load-DlpPolicy { + param([string]$Path) + + $script:Policy = [ordered]@{ + defaults = [ordered]@{ + enabled = $true + cooldownSeconds = 300 + action = 'alert' + severity = 'medium' + } + endpoint = [ordered]@{ + clipboard = @() + usb = @() + print = @() + } + } + + if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { + Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) + return + } + + try { + $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json + if ($raw.defaults) { + if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } + if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } + if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } + if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } + } + + if ($raw.endpoint) { + if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } + if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } + if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } + } + } + catch { + Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) + } +} + +function Should-EmitByCooldown { + param( + [string]$Fingerprint, + [int]$CooldownSeconds + ) + + $now = (Get-Date).ToUniversalTime() + if ($script:Cooldown.ContainsKey($Fingerprint)) { + $last = [datetime]$script:Cooldown[$Fingerprint] + if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { + return $false + } + } + + $script:Cooldown[$Fingerprint] = $now + return $true +} + +function Evaluate-ClipboardRules { + param( + [string]$ClipboardText, + [string]$ClipboardHash + ) + + foreach ($rule in @($script:Policy.endpoint.clipboard)) { + if (-not $rule) { continue } + if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } + $ruleId = [string]$rule.id + if (-not $ruleId) { continue } + $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } + $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } + if ($ClipboardText.Length -lt $minLength) { continue } + + $matched = $false + foreach ($pattern in $regexPatterns) { + if ($ClipboardText -match [string]$pattern) { + $matched = $true + break + } + } + + if (-not $matched) { continue } + + $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } + $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" + if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } + + $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } + $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } + $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } + + Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ + clipboardHash = $ClipboardHash + clipboardLength = $ClipboardText.Length + } + Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2}" -f $ruleId, $action, $severity) + } +} + +function Evaluate-UsbRules { + param( + [string]$DriveLetter, + [string]$VolumeName + ) + + foreach ($rule in @($script:Policy.endpoint.usb)) { + if (-not $rule) { continue } + if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } + $ruleId = [string]$rule.id + if (-not $ruleId) { continue } + + $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } + $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" + if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } + + $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } + $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } + $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } + + Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ + driveLetter = $DriveLetter + volumeName = $VolumeName + } + Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3}" -f $ruleId, $action, $severity, $DriveLetter) + } +} + +function Evaluate-PrintRules { + param( + [string]$PrinterName, + [string]$DocumentName, + [string]$Owner + ) + + foreach ($rule in @($script:Policy.endpoint.print)) { + if (-not $rule) { continue } + if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } + $ruleId = [string]$rule.id + if (-not $ruleId) { continue } + + $match = $true + if ($rule.printerRegex) { + $match = $match -and ($PrinterName -match [string]$rule.printerRegex) + } + if ($rule.documentRegex) { + $match = $match -and ($DocumentName -match [string]$rule.documentRegex) + } + if (-not $match) { continue } + + $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } + $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" + if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } + + $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } + $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } + $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } + + Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ + printerName = $PrinterName + documentName = $DocumentName + owner = $Owner + } + Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3}" -f $ruleId, $action, $severity, $PrinterName) + } +} + +function Test-LooksLikeMojibakeQuestionMarks { + param([AllowNull()][string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { return $true } + return $Value -match '\?{2,}' +} + +function Normalize-OwnerForMatch { + param([AllowNull()][string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { return '' } + $normalized = $Value.Trim().ToLowerInvariant() + if ($normalized -match '[\\/]') { + $parts = $normalized -split '[\\/]' + if ($parts.Count -gt 0) { + $normalized = [string]$parts[$parts.Count - 1] + } + } + if ($normalized -match '@') { + $parts = $normalized -split '@' + if ($parts.Count -gt 0) { + $normalized = [string]$parts[0] + } + } + return $normalized +} + +function Test-OwnerLooseMatch { + param( + [string]$Expected, + [string]$Actual + ) + $expectedNorm = Normalize-OwnerForMatch -Value $Expected + $actualNorm = Normalize-OwnerForMatch -Value $Actual + if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { + return $false + } + return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) +} + +function Normalize-PrinterForMatch { + param([AllowNull()][string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { return '' } + $normalized = $Value.Trim().ToLowerInvariant() + if ($normalized.Contains(',')) { + $normalized = ($normalized -split ',', 2)[0].Trim() + } + if ($normalized -match '\son\s') { + $normalized = ($normalized -split '\son\s', 2)[0].Trim() + } + return $normalized +} + +function Test-PrinterLooseMatch { + param( + [string]$Expected, + [string]$Actual + ) + $expectedNorm = Normalize-PrinterForMatch -Value $Expected + $actualNorm = Normalize-PrinterForMatch -Value $Actual + if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { + return $false + } + return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) +} + +function Get-PrintServiceEventSummary { + param([Parameter(Mandatory = $true)]$Event) + + $props = @($Event.Properties) + $propertyValues = @() + foreach ($prop in $props) { + $propertyValues += [string]$prop.Value + } + + [pscustomobject]@{ + RecordId = [string]$Event.RecordId + TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } + PropertyCount = $props.Count + DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } + Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } + PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } + PropertyValues = $propertyValues + } +} + +function Get-PrintServiceDocumentFallback { + param( + [Parameter(Mandatory = $true)]$EventSummary, + [string]$Owner, + [string]$PrinterName + ) + + $preferred = [string]$EventSummary.DocumentName + if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { + return $preferred + } + + $pathCandidates = New-Object System.Collections.Generic.List[string] + $textCandidates = New-Object System.Collections.Generic.List[string] + + foreach ($value in @($EventSummary.PropertyValues)) { + $candidate = [string]$value + if ([string]::IsNullOrWhiteSpace($candidate)) { continue } + if ($candidate -eq $preferred) { continue } + if ($Owner -and $candidate -like "*$Owner*") { continue } + if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } + if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } + + if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { + $pathCandidates.Add($candidate) + continue + } + + if ($candidate -match '^[0-9]+$') { + continue + } + + $textCandidates.Add($candidate) + } + + foreach ($candidate in @($pathCandidates)) { + $leaf = Split-Path -Path $candidate -Leaf + if (-not [string]::IsNullOrWhiteSpace($leaf)) { + return $leaf + } + return $candidate + } + + foreach ($candidate in @($textCandidates)) { + return $candidate + } + + return $null +} + +function Write-PrintServiceEventTrace { + param( + [Parameter(Mandatory = $true)]$EventSummary, + [string]$Phase, + [string]$MatchReason, + [string]$ResolvedDocument + ) + + $properties = if ($EventSummary.PropertyValues) { + ($EventSummary.PropertyValues -join ' | ') + } + else { + '' + } + + Write-EndpointLog ( + 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f + $Phase, + $EventSummary.RecordId, + $EventSummary.TimeCreated, + $EventSummary.Owner, + $EventSummary.PrinterName, + $EventSummary.DocumentName, + $ResolvedDocument, + $properties, + $MatchReason + ) +} + +function Get-BetterDocumentNameFromPrintServiceEvents { + param( + [string]$Owner, + [string]$PrinterName + ) + + try { + $startTime = (Get-Date).AddMinutes(-15) + $events = Get-WinEvent -FilterHashtable @{ + LogName = 'Microsoft-Windows-PrintService/Operational' + Id = 307 + StartTime = $startTime + } -MaxEvents 200 -ErrorAction Stop + + foreach ($pass in @('strict', 'relaxed')) { + foreach ($event in @($events)) { + $summary = Get-PrintServiceEventSummary -Event $event + $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName + + $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } + $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } + + if ($pass -eq 'strict') { + if ($Owner -and -not $ownerMatches) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument + continue + } + if ($PrinterName -and -not $printerMatches) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument + continue + } + } + else { + if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument + continue + } + } + + if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' + continue + } + + $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument + return $resolvedDocument + } + } + } + catch { + } + + return $null +} + +$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath +$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } +$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } +$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } +$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\ActivityWatch\dlp-policy.json' } +$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } +$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\ActivityWatch\logs' } +$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } +$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } +$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'ActivityWatch-Phase2\\incident-artifacts' } +$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } + +if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { + New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null +} + +$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort +$script:Hostname = $env:COMPUTERNAME +$script:SessionId = (Get-Process -Id $PID).SessionId +$script:KnownBuckets = @{} +$script:Cooldown = @{} +$script:SeenUsb = @{} +$script:SeenPrintJob = @{} +$script:SeenPrintEvent = @{} +$script:LastClipboardHash = $null +$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) +$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled +$script:LogPath = $resolvedLogPath +$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot +$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled +$script:ScreenshotTypesLoaded = $false + +Load-DlpPolicy -Path $resolvedPolicyPath +Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) + +while ($true) { + try { + if (-not $script:Policy.defaults.enabled) { + Start-Sleep -Seconds $resolvedPollSeconds + continue + } + + try { + $clipboardText = Get-Clipboard -Raw -ErrorAction SilentlyContinue + if ($clipboardText) { + $clipboardHash = Get-StringHash -Value $clipboardText + if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { + $script:LastClipboardHash = $clipboardHash + Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ + clipboardHash = $clipboardHash + clipboardLength = $clipboardText.Length + } + Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash + } + } + } + catch { + } + + try { + $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue + $currentUsb = @{} + foreach ($drive in @($usbDrives)) { + $deviceId = [string]$drive.DeviceID + if (-not $deviceId) { continue } + $currentUsb[$deviceId] = $true + if (-not $script:SeenUsb.ContainsKey($deviceId)) { + $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() + $volumeName = [string]$drive.VolumeName + Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ + driveLetter = $deviceId + volumeName = $volumeName + } + Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName + } + } + + foreach ($known in @($script:SeenUsb.Keys)) { + if (-not $currentUsb.ContainsKey($known)) { + $script:SeenUsb.Remove($known) + } + } + } + catch { + } + + try { + $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue + foreach ($job in @($printJobs)) { + $jobId = [string]$job.JobId + if (-not $jobId) { continue } + if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } + $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() + + $printerName = [string]$job.Name + $documentName = [string]$job.Document + $owner = [string]$job.Owner + $documentNameOriginal = $documentName + + if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { + $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName + if ($eventDocumentName) { + $documentName = $eventDocumentName + } + } + + Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ + printerName = $printerName + documentName = $documentName + documentNameOriginal = $documentNameOriginal + owner = $owner + } + Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner + } + + $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) + foreach ($k in @($script:SeenPrintJob.Keys)) { + $ts = [datetime]$script:SeenPrintJob[$k] + if ($ts -lt $cleanupBefore) { + $script:SeenPrintJob.Remove($k) + } + } + } + catch { + } + + try { + $printEvents = Get-WinEvent -FilterHashtable @{ + LogName = 'Microsoft-Windows-PrintService/Operational' + Id = 307 + StartTime = (Get-Date).AddMinutes(-20) + } -MaxEvents 200 -ErrorAction SilentlyContinue + + foreach ($event in @($printEvents)) { + $recordId = [string]$event.RecordId + if (-not $recordId) { continue } + if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } + $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() + + $summary = Get-PrintServiceEventSummary -Event $event + $documentName = [string]$summary.DocumentName + $owner = [string]$summary.Owner + $printerName = [string]$summary.PrinterName + $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName + + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument + + if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { + continue + } + + Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ + printerName = $printerName + documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } + documentNameOriginal = $documentName + owner = $owner + eventRecordId = $recordId + eventSource = 'printservice-307' + } + Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner + } + + $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) + foreach ($k in @($script:SeenPrintEvent.Keys)) { + $ts = [datetime]$script:SeenPrintEvent[$k] + if ($ts -lt $cleanupBeforeEvent) { + $script:SeenPrintEvent.Remove($k) + } + } + } + catch { + } + } + catch { + Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) + } + + Start-Sleep -Seconds $resolvedPollSeconds +} diff --git a/install-kit-awindows-20260427-211240/windows/dlp-policy.example.json b/install-kit-awindows-20260427-211240/windows/dlp-policy.example.json new file mode 100644 index 0000000..5ed8ef6 --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/dlp-policy.example.json @@ -0,0 +1,99 @@ +{ + "version": 1, + "defaults": { + "enabled": true, + "cooldownSeconds": 300, + "action": "log", + "severity": "low" + }, + "rules": [ + { + "id": "personal-web-during-workhours", + "enabled": true, + "cooldownSeconds": 600, + "action": "alert", + "severity": "medium", + "message": "Личные ресурсы в рабочее время", + "when": { + "categoryGroups": ["personal"], + "hourFrom": 9, + "hourTo": 19 + } + }, + { + "id": "high-risk-cloud-storage", + "enabled": true, + "cooldownSeconds": 900, + "action": "alert", + "severity": "high", + "message": "Подозрительный доступ к облачному хранилищу", + "when": { + "domains": [ + "dropbox.com", + "drive.google.com", + "mega.nz", + "onedrive.live.com", + "disk.yandex.ru" + ] + } + }, + { + "id": "anonymizer-and-vpn-web", + "enabled": true, + "cooldownSeconds": 900, + "action": "alert", + "severity": "high", + "message": "Использование веб-анонимайзеров / VPN-сервисов", + "when": { + "domains": [ + "hidemy.name", + "2ip.ru", + "whoer.net", + "protonvpn.com", + "nordvpn.com" + ] + } + } + ], + "endpoint": { + "clipboard": [ + { + "id": "clipboard-sensitive-keywords", + "enabled": true, + "cooldownSeconds": 300, + "action": "alert", + "severity": "high", + "message": "В буфере обнаружены чувствительные ключевые слова", + "minLength": 20, + "regexPatterns": [ + "(?i)парол(ь|и)", + "(?i)password", + "(?i)secret", + "(?i)cvv", + "(?i)паспорт" + ] + } + ], + "usb": [ + { + "id": "usb-media-connected", + "enabled": true, + "cooldownSeconds": 300, + "action": "alert", + "severity": "medium", + "message": "Подключен съемный носитель" + } + ], + "print": [ + { + "id": "print-sensitive-docs", + "enabled": true, + "cooldownSeconds": 300, + "action": "alert", + "severity": "high", + "message": "Печать документа с признаками чувствительных данных", + "documentRegex": "(?i)(salary|зарплат|passport|паспорт|договор|contract)" + } + ] + } +} diff --git a/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 b/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 new file mode 100755 index 0000000..c908b2f --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 @@ -0,0 +1,144 @@ +[CmdletBinding()] +param( + [string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json', + [string]$ServerHost, + [int]$ServerPort, + [ValidateSet('http', 'https')] + [string]$ServerScheme, + [string[]]$Users, + [string]$UserListPath, + [string]$Domain, + [string]$InstallRoot, + [string]$StateRoot, + [int]$PollSeconds, + [int]$PulseSeconds, + [int]$RecoveryIntervalSeconds, + [bool]$AfkEnabled, + [bool]$WindowEnabled, + [bool]$LocalAgentLogsEnabled, + [bool]$IncidentCaptureEnabled, + [bool]$IncidentScreenshotEnabled, + [string]$IncidentArtifactsRoot, + [bool]$LogonMarkerEnabled, + [string]$CustomRulesPath, + [string]$CustomPolicyPath, + [switch]$RepairPackage, + [string]$Version, + [string]$PackageUrl, + [string]$PackageZipPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' +Import-Module $modulePath -Force + +Assert-Administrator + +$existingConfig = $null +if (Test-Path -LiteralPath $ConfigPath) { + $existingConfig = Read-ActivityWatchDeploymentConfig -Path $ConfigPath +} + +if (-not $existingConfig -and (-not $ServerHost)) { + throw 'deployment-config.json is missing. Provide -ServerHost and user parameters, or run a deploy script first.' +} + +$effectiveStateRoot = if ($StateRoot) { $StateRoot } elseif ($existingConfig) { [string]$existingConfig.paths.stateRoot } else { 'C:\ProgramData\ActivityWatch' } +$effectiveInstallRoot = if ($InstallRoot) { $InstallRoot } elseif ($existingConfig) { [string]$existingConfig.paths.installRoot } else { 'C:\Program Files\ActivityWatch' } +$effectiveLogsRoot = if ($existingConfig) { [string]$existingConfig.paths.logsRoot } else { Join-Path $effectiveStateRoot 'logs' } +$effectiveConfigPath = if ($ConfigPath) { $ConfigPath } else { Join-Path $effectiveStateRoot 'deployment-config.json' } +$effectiveLaunchScript = Join-Path $effectiveStateRoot 'launch-watchers.ps1' +$effectiveRecoveryScript = Join-Path $effectiveStateRoot 'recovery-loop.ps1' +$effectiveCollector = Join-Path $effectiveStateRoot 'browser-domains-native-collector.ps1' +$effectiveEndpointCollector = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$existingConfig.paths.endpointCollectorScript } else { Join-Path $effectiveStateRoot 'dlp-endpoint-signals-collector.ps1' } +$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' } + +$effectiveServerHost = if ($ServerHost) { $ServerHost } elseif ($existingConfig) { [string]$existingConfig.server.host } else { $null } +$effectiveServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($existingConfig) { [int]$existingConfig.server.port } else { 5600 } +$effectiveServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($existingConfig) { [string]$existingConfig.server.scheme } else { 'http' } +$effectivePollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($existingConfig) { [int]$existingConfig.collector.pollSeconds } else { 5 } +$effectivePulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($existingConfig) { [int]$existingConfig.collector.pulseSeconds } else { 30 } +$effectiveRecoveryInterval = if ($PSBoundParameters.ContainsKey('RecoveryIntervalSeconds')) { $RecoveryIntervalSeconds } elseif ($existingConfig) { [int]$existingConfig.recovery.intervalSeconds } else { 180 } +$effectiveAfkEnabled = if ($PSBoundParameters.ContainsKey('AfkEnabled')) { [bool]$AfkEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'collectors' -and $existingConfig.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$existingConfig.collectors.afkEnabled } else { $true } +$effectiveWindowEnabled = if ($PSBoundParameters.ContainsKey('WindowEnabled')) { [bool]$WindowEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'collectors' -and $existingConfig.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$existingConfig.collectors.windowEnabled } else { $true } +$effectiveLocalAgentLogsEnabled = if ($PSBoundParameters.ContainsKey('LocalAgentLogsEnabled')) { [bool]$LocalAgentLogsEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'logging' -and $existingConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$existingConfig.logging.localAgentLogsEnabled } else { $false } +$effectiveIncidentCaptureEnabled = if ($PSBoundParameters.ContainsKey('IncidentCaptureEnabled')) { [bool]$IncidentCaptureEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'enabled') { [bool]$existingConfig.incidentCapture.enabled } else { $true } +$effectiveIncidentScreenshotEnabled = if ($PSBoundParameters.ContainsKey('IncidentScreenshotEnabled')) { [bool]$IncidentScreenshotEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$existingConfig.incidentCapture.screenshotEnabled } else { $true } +$effectiveIncidentArtifactsRoot = if ($PSBoundParameters.ContainsKey('IncidentArtifactsRoot') -and $IncidentArtifactsRoot) { $IncidentArtifactsRoot } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$existingConfig.incidentCapture.artifactsRoot } else { Join-Path $effectiveStateRoot 'incident-artifacts' } +$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 } +$effectiveVersion = if ($Version) { $Version } elseif ($existingConfig) { [string]$existingConfig.package.version } else { 'v0.13.2' } + +$effectiveUsers = if ($Users -or $UserListPath) { + Normalize-ActivityWatchUsers -Users $Users -UserListPath $UserListPath -Domain $Domain +} +elseif ($existingConfig) { + @($existingConfig.userTasks | ForEach-Object { [string]$_.userId }) +} +else { + throw 'Target users are missing.' +} + +New-ActivityWatchDirectory -Path $effectiveStateRoot +New-ActivityWatchDirectory -Path $effectiveLogsRoot + +if ($RepairPackage) { + $workingRoot = Join-Path $env:TEMP 'activitywatch-windows-deploy' + $backupRoot = Join-Path $effectiveStateRoot 'backups' + $archivePath = Get-ActivityWatchArchive -PackageZipPath $PackageZipPath -PackageUrl $PackageUrl -Version $effectiveVersion -WorkingRoot $workingRoot + Install-ActivityWatchPackage -ArchivePath $archivePath -InstallRoot $effectiveInstallRoot -WorkingRoot $workingRoot -BackupRoot $backupRoot | Out-Null +} + +Get-ActivityWatchExecutableMap -InstallRoot $effectiveInstallRoot | Out-Null + +$assetResult = Copy-ActivityWatchCollectorAssets ` + -CollectorScriptSource (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') ` + -EndpointCollectorScriptSource (Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1') ` + -ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') ` + -ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') ` + -StateRoot $effectiveStateRoot ` + -CustomRulesSource $CustomRulesPath ` + -CustomPolicySource $CustomPolicyPath + +$taskDefinitions = New-ActivityWatchUserTaskDefinitions -Users $effectiveUsers +Write-ActivityWatchLaunchScript -Path $effectiveLaunchScript -ConfigPath $effectiveConfigPath +Write-ActivityWatchRecoveryScript -Path $effectiveRecoveryScript -ConfigPath $effectiveConfigPath + +$config = New-ActivityWatchDeploymentConfig ` + -ServerHost $effectiveServerHost ` + -ServerPort $effectiveServerPort ` + -ServerScheme $effectiveServerScheme ` + -InstallRoot $effectiveInstallRoot ` + -StateRoot $effectiveStateRoot ` + -LogsRoot $effectiveLogsRoot ` + -CollectorScript $effectiveCollector ` + -EndpointCollectorScript $effectiveEndpointCollector ` + -RulesPath $effectiveRules ` + -PolicyPath $effectivePolicy ` + -PollSeconds $effectivePollSeconds ` + -PulseSeconds $effectivePulseSeconds ` + -RecoveryIntervalSeconds $effectiveRecoveryInterval ` + -AfkEnabled $effectiveAfkEnabled ` + -WindowEnabled $effectiveWindowEnabled ` + -LocalAgentLogsEnabled $effectiveLocalAgentLogsEnabled ` + -IncidentCaptureEnabled $effectiveIncidentCaptureEnabled ` + -IncidentScreenshotEnabled $effectiveIncidentScreenshotEnabled ` + -IncidentArtifactsRoot $effectiveIncidentArtifactsRoot ` + -LogonMarkerEnabled $effectiveLogonMarkerEnabled ` + -LaunchScriptPath $effectiveLaunchScript ` + -RecoveryScriptPath $effectiveRecoveryScript ` + -UserTasks $taskDefinitions ` + -PackageVersion $effectiveVersion + +Write-ActivityWatchDeploymentConfig -Config $config -Path $effectiveConfigPath +Remove-LegacyActivityWatchEntries +Set-ActivityWatchAcl -InstallRoot $effectiveInstallRoot -StateRoot $effectiveStateRoot -LogsRoot $effectiveLogsRoot +Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $effectiveLaunchScript -ConfigPath $effectiveConfigPath +Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath +Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName + +Write-Host 'ActivityWatch hardening/recovery completed.' +Write-Host "Config: $effectiveConfigPath" +Write-Host "Users repaired: $($effectiveUsers -join ', ')" diff --git a/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 b/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 new file mode 100644 index 0000000..71e93a0 --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 @@ -0,0 +1,90 @@ +[CmdletBinding()] +param( + [string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' +Import-Module $modulePath -Force + +$config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath +$installRoot = [string]$config.paths.installRoot +$stateRoot = [string]$config.paths.stateRoot +$collectorScript = [string]$config.paths.collectorScript +$endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' } +$rulesPath = [string]$config.paths.rulesPath +$policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' } +$launchScript = [string]$config.paths.launchScript +$recoveryScript = [string]$config.paths.recoveryScript + +$requiredFiles = @( + (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe'), + (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'), + $collectorScript, + $endpointCollectorScript, + $rulesPath, + $policyPath, + $launchScript, + $recoveryScript, + $ConfigPath +) + +$missingFiles = @( + $requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) } +) + +$processNames = @('aw-watcher-afk', 'aw-watcher-window') +$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId + +$taskNames = @() +if ($config.userTasks) { + $taskNames += @($config.userTasks | ForEach-Object { [string]$_.launchTaskName }) +} +$taskNames += [string]$config.recovery.taskName +$taskNames = $taskNames | Sort-Object -Unique + +$tasks = foreach ($taskName in $taskNames) { + $task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1 + if ($task) { + [pscustomobject]@{ + taskName = $task.TaskName + state = [string]$task.State + present = $true + } + } + else { + [pscustomobject]@{ + taskName = $taskName + state = 'Missing' + present = $false + } + } +} + +$serverUrl = '{0}://{1}:{2}' -f [string]$config.server.scheme, [string]$config.server.host, [int]$config.server.port +$result = [ordered]@{ + generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') + configPath = $ConfigPath + serverUrl = $serverUrl + installRoot = $installRoot + stateRoot = $stateRoot + files = [ordered]@{ + required = $requiredFiles + missing = $missingFiles + ok = ($missingFiles.Count -eq 0) + } + tasks = [ordered]@{ + list = $tasks + ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present })) + } + processes = [ordered]@{ + list = @($runningProcesses) + ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2) + } +} + +$result.overallOk = [bool]($result.files.ok -and $result.tasks.ok -and $result.processes.ok) + +$result diff --git a/install-kit-awindows-20260427-211240/windows/web-category-rules.example.json b/install-kit-awindows-20260427-211240/windows/web-category-rules.example.json new file mode 100755 index 0000000..ab51f5a --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/web-category-rules.example.json @@ -0,0 +1,37 @@ +{ + "version": 1, + "description": "Override or extend built-in ActivityWatch web categorization rules.", + "rules": [ + { + "name": "work_crm", + "group": "work", + "domains": [ + "crm.example.com", + "portal.example.org" + ] + }, + { + "name": "work_erp", + "group": "work", + "domains": [ + "erp.example.com", + "bi.example.com" + ] + }, + { + "name": "neutral_training", + "group": "neutral", + "domains": [ + "wiki.example.net", + "kb.example.net" + ] + }, + { + "name": "personal_social", + "group": "personal", + "domains": [ + "social.example.net" + ] + } + ] +} diff --git a/windows/dlp-endpoint-signals-collector.ps1 b/windows/dlp-endpoint-signals-collector.ps1 index 7624c90..8ee44f5 100644 --- a/windows/dlp-endpoint-signals-collector.ps1 +++ b/windows/dlp-endpoint-signals-collector.ps1 @@ -402,6 +402,64 @@ function Test-LooksLikeMojibakeQuestionMarks { return $Value -match '\?{2,}' } +function Normalize-OwnerForMatch { + param([AllowNull()][string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { return '' } + $normalized = $Value.Trim().ToLowerInvariant() + if ($normalized -match '[\\/]') { + $parts = $normalized -split '[\\/]' + if ($parts.Count -gt 0) { + $normalized = [string]$parts[$parts.Count - 1] + } + } + if ($normalized -match '@') { + $parts = $normalized -split '@' + if ($parts.Count -gt 0) { + $normalized = [string]$parts[0] + } + } + return $normalized +} + +function Test-OwnerLooseMatch { + param( + [string]$Expected, + [string]$Actual + ) + $expectedNorm = Normalize-OwnerForMatch -Value $Expected + $actualNorm = Normalize-OwnerForMatch -Value $Actual + if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { + return $false + } + return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) +} + +function Normalize-PrinterForMatch { + param([AllowNull()][string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { return '' } + $normalized = $Value.Trim().ToLowerInvariant() + if ($normalized.Contains(',')) { + $normalized = ($normalized -split ',', 2)[0].Trim() + } + if ($normalized -match '\son\s') { + $normalized = ($normalized -split '\son\s', 2)[0].Trim() + } + return $normalized +} + +function Test-PrinterLooseMatch { + param( + [string]$Expected, + [string]$Actual + ) + $expectedNorm = Normalize-PrinterForMatch -Value $Expected + $actualNorm = Normalize-PrinterForMatch -Value $Actual + if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { + return $false + } + return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) +} + function Get-PrintServiceEventSummary { param([Parameter(Mandatory = $true)]$Event) @@ -515,32 +573,40 @@ function Get-BetterDocumentNameFromPrintServiceEvents { StartTime = $startTime } -MaxEvents 200 -ErrorAction Stop - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $matchReason = 'scan' - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName + foreach ($pass in @('strict', 'relaxed')) { + foreach ($event in @($events)) { + $summary = Get-PrintServiceEventSummary -Event $event + $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - if ($Owner -and $summary.Owner -and ($summary.Owner -notlike "*$Owner*")) { - $matchReason = 'owner-mismatch' - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason $matchReason -ResolvedDocument $resolvedDocument - continue + $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } + $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } + + if ($pass -eq 'strict') { + if ($Owner -and -not $ownerMatches) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument + continue + } + if ($PrinterName -and -not $printerMatches) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument + continue + } + } + else { + if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument + continue + } + } + + if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' + continue + } + + $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } + Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument + return $resolvedDocument } - - if ($PrinterName -and $summary.PrinterName -and ($summary.PrinterName -notlike "*$PrinterName*")) { - $matchReason = 'printer-mismatch' - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason $matchReason -ResolvedDocument $resolvedDocument - continue - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - $matchReason = 'no-document-candidate' - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason $matchReason -ResolvedDocument '' - continue - } - - $matchReason = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason $matchReason -ResolvedDocument $resolvedDocument - return $resolvedDocument } } catch {