fix(aw): harden collectors and grafana exports
This commit is contained in:
@@ -94,6 +94,89 @@
|
||||
- "{{ aw_rus_health_validation_dir }}"
|
||||
- "{{ aw_server_log_dir }}"
|
||||
|
||||
- name: Установить prune script для локального state
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-prune-local-state.sh"
|
||||
dest: /usr/local/bin/aw-prune-local-state.sh
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Ограничить рост journald на aw-server
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/journald.conf.d/aw-rus-retention.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Journal]
|
||||
SystemMaxUse={{ aw_server_journal_system_max_use }}
|
||||
RuntimeMaxUse={{ aw_server_journal_runtime_max_use }}
|
||||
SystemKeepFree={{ aw_server_journal_system_keep_free }}
|
||||
register: aw_journald_dropin
|
||||
|
||||
- name: Установить systemd service prune локального state
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/aw-prune-local-state.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Prune ActivityWatch local backups and temp state
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=AW_DATA_DIR={{ aw_server_data_dir }}
|
||||
Environment=AW_BACKUP_RETENTION_DAYS={{ aw_server_backup_retention_days }}
|
||||
Environment=AW_BACKUP_KEEP_LAST_DB={{ aw_server_backup_keep_last_db }}
|
||||
Environment=AW_BACKUP_KEEP_LAST_JSON={{ aw_server_backup_keep_last_json }}
|
||||
ExecStart=/usr/local/bin/aw-prune-local-state.sh
|
||||
|
||||
- name: Установить systemd timer prune локального state
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/aw-prune-local-state.timer
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Daily prune of ActivityWatch local backups and temp state
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 04:40:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
|
||||
- name: Перечитать systemd после retention unit/drop-in
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Включить и запустить timer prune локального state
|
||||
ansible.builtin.systemd:
|
||||
name: aw-prune-local-state.timer
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Применить journald retention без простоя
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemctl
|
||||
- restart
|
||||
- systemd-journald
|
||||
when: aw_journald_dropin.changed
|
||||
failed_when: false
|
||||
|
||||
- name: Сжать существующий journald до нового лимита
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- journalctl
|
||||
- --vacuum-size={{ aw_server_journal_system_max_use }}
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
||||
- name: (Check mode) Пропустить установку релиза ActivityWatch
|
||||
ansible.builtin.debug:
|
||||
msg: "ansible_check_mode=true: download/unarchive/install of ActivityWatch release is skipped."
|
||||
@@ -115,6 +198,11 @@
|
||||
remote_src: true
|
||||
extra_opts: ["-o"]
|
||||
|
||||
- name: Удалить временный архив ActivityWatch после распаковки
|
||||
ansible.builtin.file:
|
||||
path: "{{ aw_archive_path }}"
|
||||
state: absent
|
||||
|
||||
- name: Найти распакованный каталог ActivityWatch
|
||||
ansible.builtin.find:
|
||||
paths: "{{ aw_release_dir }}"
|
||||
@@ -288,6 +376,22 @@
|
||||
dest: /opt/activitywatch/aw-server/apply_webui_ru_patch.sh
|
||||
mode: "0755"
|
||||
|
||||
- name: Проверить Influx token для AW worktime exporter
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- aw_worktime_influx_token is defined
|
||||
- aw_worktime_influx_token | length > 0
|
||||
fail_msg: "aw_worktime_influx_enabled=true, но aw_worktime_influx_token пуст. Exporter будет падать и Grafana не получит worktime-ряды."
|
||||
when: aw_worktime_influx_enabled | default(false) | bool
|
||||
|
||||
- name: Проверить Influx token для AW DLP exporter
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- aw_dlp_influx_token is defined
|
||||
- aw_dlp_influx_token | length > 0
|
||||
fail_msg: "aw_dlp_influx_enabled=true, но aw_dlp_influx_token пуст. Exporter будет падать и Grafana не получит DLP-ряды."
|
||||
when: aw_dlp_influx_enabled | default(false) | bool
|
||||
|
||||
- name: Записать /etc/activitywatch/aw-server.env перед хотфиксами
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/activitywatch/aw-server.env
|
||||
@@ -922,7 +1026,6 @@
|
||||
ansible.builtin.systemd:
|
||||
name: aw-worktime-influx-exporter.service
|
||||
state: started
|
||||
failed_when: false
|
||||
when: aw_worktime_influx_enabled | default(false) | bool
|
||||
|
||||
- name: Включить и перезапустить AW DLP Influx exporter timer
|
||||
@@ -936,7 +1039,6 @@
|
||||
ansible.builtin.systemd:
|
||||
name: aw-dlp-influx-exporter.service
|
||||
state: started
|
||||
failed_when: false
|
||||
when: aw_dlp_influx_enabled | default(false) | bool
|
||||
|
||||
- name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper)
|
||||
@@ -1514,6 +1616,11 @@
|
||||
remote_src: true
|
||||
creates: "{{ aw_hayabusa_release_dir }}/{{ aw_hayabusa_binary_name }}"
|
||||
|
||||
- name: Удалить временный архив Hayabusa после распаковки
|
||||
ansible.builtin.file:
|
||||
path: "{{ aw_hayabusa_archive_path }}"
|
||||
state: absent
|
||||
|
||||
- name: Нормализовать права release Hayabusa
|
||||
ansible.builtin.file:
|
||||
path: "{{ aw_hayabusa_release_dir }}"
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
|
||||
aw_windows_package_zip_path: ""
|
||||
aw_windows_domain: "SHARKON2025"
|
||||
aw_windows_builtin_administrator_name: "Администратор"
|
||||
aw_windows_users:
|
||||
- Администратор
|
||||
- user1
|
||||
@@ -49,6 +50,7 @@
|
||||
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
|
||||
aw_windows_forensics_root: "{{ aw_windows_state_root }}\\forensics\\evtx-exports"
|
||||
aw_windows_logon_marker_enabled: true
|
||||
aw_windows_process_events_enabled: true
|
||||
aw_windows_skip_hardening: false
|
||||
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
|
||||
aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
|
||||
@@ -137,6 +139,8 @@
|
||||
- aw_windows_server_port is defined
|
||||
- aw_windows_server_scheme is defined
|
||||
- aw_windows_domain is defined
|
||||
- aw_windows_builtin_administrator_name is defined
|
||||
- aw_windows_builtin_administrator_name | length > 0
|
||||
- aw_windows_users_effective | length > 0
|
||||
- aw_windows_install_root is defined
|
||||
- aw_windows_state_root is defined
|
||||
@@ -228,6 +232,7 @@
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$env:AWATCH_RUS_BUILTIN_ADMINISTRATOR_NAME = "{{ aw_windows_builtin_administrator_name }}"
|
||||
$params = @{
|
||||
ServerScheme = "{{ aw_windows_server_scheme }}"
|
||||
ServerHost = "{{ aw_windows_server_host_effective }}"
|
||||
@@ -247,6 +252,7 @@
|
||||
EvtxExportRoot = "{{ aw_windows_forensics_root }}"
|
||||
EvtxRetentionDays = {{ aw_windows_evtx_retention_days | int }}
|
||||
LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
|
||||
ProcessEventsEnabled = {{ '$true' if (aw_windows_process_events_enabled | bool) else '$false' }}
|
||||
PolicyMode = "{{ aw_windows_policy_mode }}"
|
||||
PolicyEngineEnabled = {{ '$true' if (aw_windows_policy_engine_enabled | bool) else '$false' }}
|
||||
PolicyEngineHost = "{{ aw_windows_policy_engine_host_effective }}"
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
gather_facts: true
|
||||
|
||||
vars:
|
||||
tsj_bot_user: "codex"
|
||||
tsj_bot_runtime_root: "/opt/infra-admin"
|
||||
tsj_bot_user: "root"
|
||||
tsj_bot_group: "admin"
|
||||
tsj_bot_root: "/home/codex/infra-admin/tsj-bot"
|
||||
tsj_bot_root: "{{ tsj_bot_runtime_root }}/tsj-bot"
|
||||
tsj_bot_script_name: "tsj_guardian_bot.py"
|
||||
tsj_bot_script_dest: "{{ tsj_bot_root }}/{{ tsj_bot_script_name }}"
|
||||
tsj_bot_source_local_path: "{{ aw_repo_root }}/proxmox/tsj_guardian_bot.py"
|
||||
@@ -16,8 +17,8 @@
|
||||
tsj_bot_openvpn_helper_source_local_path: "{{ aw_repo_root }}/proxmox/{{ tsj_bot_openvpn_helper_name }}"
|
||||
tsj_bot_service_name: "tsj-guardian-bot.service"
|
||||
tsj_bot_env_path: "{{ tsj_bot_root }}/.env"
|
||||
tsj_bot_state_dir: "/home/codex/infra-admin/.state"
|
||||
tsj_bot_logs_dir: "/home/codex/infra-admin/logs"
|
||||
tsj_bot_state_dir: "{{ tsj_bot_runtime_root }}/.state"
|
||||
tsj_bot_logs_dir: "{{ tsj_bot_runtime_root }}/logs"
|
||||
tsj_bot_default_chat_id: "{{ telegram_default_chat_id | default(telegram_allowed_chat_ids.split(',')[0]) }}"
|
||||
|
||||
pre_tasks:
|
||||
@@ -130,15 +131,16 @@
|
||||
NODE_13_URL={{ tsj_bot_node_13_url | default('http://10.10.10.13:5600/api/0/info') }}
|
||||
NODE_16_URL={{ tsj_bot_node_16_url | default('http://10.10.10.16/') }}
|
||||
NODE_16_ENABLED={{ tsj_bot_node_16_enabled | default('false') }}
|
||||
CHECK_SCRIPT={{ tsj_bot_check_script | default('/home/codex/infra-admin/scripts/system_self_support.sh --check') }}
|
||||
HEAL_SCRIPT={{ tsj_bot_heal_script | default('/home/codex/infra-admin/scripts/system_self_support.sh --heal') }}
|
||||
INFRA_ADMIN_ROOT={{ tsj_bot_runtime_root }}
|
||||
CHECK_SCRIPT={{ tsj_bot_check_script | default(tsj_bot_runtime_root + '/scripts/system_self_support.sh --check') }}
|
||||
HEAL_SCRIPT={{ tsj_bot_heal_script | default(tsj_bot_runtime_root + '/scripts/system_self_support.sh --heal') }}
|
||||
FS_WARN_PCT={{ tsj_bot_fs_warn_pct | default(85) }}
|
||||
FS_CRIT_PCT={{ tsj_bot_fs_crit_pct | default(92) }}
|
||||
FS_TARGETS={{ tsj_bot_fs_targets | default('host,200,201,202,203') }}
|
||||
FS_EXCLUDE_TYPES={{ tsj_bot_fs_exclude_types | default('tmpfs,devtmpfs,proc,sysfs,cgroup,cgroup2,overlay,squashfs,nsfs,tracefs,debugfs,securityfs,configfs,fusectl,mqueue,hugetlbfs,ramfs') }}
|
||||
STATE_FILE={{ tsj_bot_state_file | default('/home/codex/infra-admin/.state/tsj_guardian_state.json') }}
|
||||
LOG_FILE={{ tsj_bot_log_file | default('/home/codex/infra-admin/logs/tsj_guardian_bot.log') }}
|
||||
HEARTBEAT_FILE={{ tsj_bot_heartbeat_file | default('/home/codex/infra-admin/.state/tsj_guardian_heartbeat') }}
|
||||
STATE_FILE={{ tsj_bot_state_file | default(tsj_bot_runtime_root + '/.state/tsj_guardian_state.json') }}
|
||||
LOG_FILE={{ tsj_bot_log_file | default(tsj_bot_runtime_root + '/logs/tsj_guardian_bot.log') }}
|
||||
HEARTBEAT_FILE={{ tsj_bot_heartbeat_file | default(tsj_bot_runtime_root + '/.state/tsj_guardian_heartbeat') }}
|
||||
CHECK_INTERVAL_SEC={{ tsj_bot_check_interval_sec | default(60) }}
|
||||
OPERATOR_TIMEOUT_SEC={{ tsj_bot_operator_timeout_sec | default(900) }}
|
||||
RETRY_AUTORECOVERY_EVERY_SEC={{ tsj_bot_retry_autorecovery_every_sec | default(300) }}
|
||||
@@ -170,8 +172,8 @@
|
||||
OPENVPN_EXPIRY_WARN_TIMEOUT_SEC={{ tsj_bot_openvpn_expiry_warn_timeout_sec | default(120) }}
|
||||
OPENVPN_EXPIRY_WARN_INTERVAL_SEC={{ tsj_bot_openvpn_expiry_warn_interval_sec | default(21600) }}
|
||||
PFSENSE_MCP_BEARER={{ tsj_bot_pfsense_mcp_bearer | default(pfsense_mcp_bearer | default('')) }}
|
||||
SERVER_FALLBACK_COMMANDS={{ tsj_bot_server_fallback_commands | default('/home/codex/infra-admin/scripts/system_self_support.sh --heal') }}
|
||||
UPDATES_SCRIPT={{ tsj_bot_updates_script | default('/usr/bin/python3 /home/codex/infra-admin/scripts/proxmox_lxc_critical_updates.py') }}
|
||||
SERVER_FALLBACK_COMMANDS={{ tsj_bot_server_fallback_commands | default(tsj_bot_runtime_root + '/scripts/system_self_support.sh --heal') }}
|
||||
UPDATES_SCRIPT={{ tsj_bot_updates_script | default('/usr/bin/python3 ' + tsj_bot_runtime_root + '/scripts/proxmox_lxc_critical_updates.py') }}
|
||||
UPDATE_TARGETS={{ tsj_bot_update_targets | default('auto') }}
|
||||
AW_RUS_API_BASE={{ tsj_bot_aw_rus_api_base | default('http://10.10.10.13:5600/api/0') }}
|
||||
AW_RUS_WORKTIME_BASE={{ tsj_bot_aw_rus_worktime_base | default('http://10.10.10.13:5610') }}
|
||||
@@ -257,7 +259,7 @@
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/home/codex/infra-admin
|
||||
WorkingDirectory={{ tsj_bot_runtime_root }}
|
||||
EnvironmentFile={{ tsj_bot_env_path }}
|
||||
ExecStart=/usr/bin/python3 {{ tsj_bot_script_dest }}
|
||||
Restart=always
|
||||
|
||||
@@ -12,21 +12,24 @@ aw_server_inventory_host: "{{ (groups['aw_server'] | default([]) | first) | defa
|
||||
aw_server_public_host: "{{ (hostvars[aw_server_inventory_host].ansible_host | default(aw_server_inventory_host, true)) if (aw_server_inventory_host | length) > 0 else 'aw-server' }}"
|
||||
aw_worktime_report_base: "http://{{ aw_server_public_host }}:5610"
|
||||
aw_worktime_timezone: "Europe/Moscow"
|
||||
aw_worktime_influx_enabled: false
|
||||
aw_worktime_influx_enabled: true
|
||||
aw_worktime_influx_url: "http://10.10.10.10:8086"
|
||||
aw_worktime_influx_org: "proxmox"
|
||||
aw_worktime_influx_bucket: "aw_metrics"
|
||||
aw_worktime_influx_hosts: "SHARKON2025"
|
||||
aw_worktime_influx_days: "today,yesterday"
|
||||
aw_dlp_influx_enabled: false
|
||||
aw_worktime_influx_token: "{{ lookup('env', 'AW_WORKTIME_INFLUX_TOKEN') }}"
|
||||
aw_dlp_influx_enabled: true
|
||||
aw_dlp_influx_url: "http://10.10.10.10:8086"
|
||||
aw_dlp_influx_org: "proxmox"
|
||||
aw_dlp_influx_bucket: "aw_metrics"
|
||||
aw_dlp_influx_hosts: "SHARKON2025"
|
||||
aw_dlp_influx_lookback_days: 30
|
||||
aw_dlp_influx_event_limit: 2000
|
||||
aw_dlp_influx_token: "{{ lookup('env', 'AW_DLP_INFLUX_TOKEN') }}"
|
||||
aw_monitored_windows_host: "192.168.100.18"
|
||||
aw_monitored_windows_hostname: "SHARKON2025"
|
||||
aw_rus_health_worktime_api_base: "http://127.0.0.1:5610"
|
||||
aw_rus_health_state_dir: "{{ aw_server_data_dir }}/health"
|
||||
aw_rus_health_validation_dir: "{{ aw_rus_health_state_dir }}/windows-validation"
|
||||
aw_hayabusa_auto_case_enabled: true
|
||||
|
||||
@@ -30,6 +30,12 @@ aw_hayabusa_staging_dir: "{{ aw_hayabusa_inbox_dir }}/staging"
|
||||
aw_hayabusa_archive_packages_dir: "{{ aw_hayabusa_archive_dir }}/packages"
|
||||
aw_hayabusa_archive_extracted_dir: "{{ aw_hayabusa_archive_dir }}/extracted"
|
||||
aw_hayabusa_logs_dir: "{{ aw_hayabusa_state_dir }}/logs"
|
||||
aw_server_backup_retention_days: 7
|
||||
aw_server_backup_keep_last_db: 2
|
||||
aw_server_backup_keep_last_json: 2
|
||||
aw_server_journal_system_max_use: "100M"
|
||||
aw_server_journal_runtime_max_use: "50M"
|
||||
aw_server_journal_system_keep_free: "500M"
|
||||
aw_dlp_policy_engine_enabled: true
|
||||
aw_dlp_policy_engine_bind_host: "0.0.0.0"
|
||||
aw_dlp_policy_engine_port: 5601
|
||||
|
||||
@@ -15,6 +15,7 @@ aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases
|
||||
aw_windows_package_zip_path: ""
|
||||
|
||||
aw_windows_domain: "SHARKON2025"
|
||||
aw_windows_builtin_administrator_name: "Администратор"
|
||||
aw_windows_users:
|
||||
- Администратор
|
||||
- user1
|
||||
@@ -58,6 +59,7 @@ aw_windows_evtx_channels:
|
||||
- Microsoft-Windows-TerminalServices-LocalSessionManager/Operational
|
||||
- Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational
|
||||
aw_windows_logon_marker_enabled: true
|
||||
aw_windows_process_events_enabled: true
|
||||
aw_windows_skip_hardening: false
|
||||
|
||||
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
|
||||
|
||||
@@ -4,6 +4,7 @@ telegram_default_chat_id: 123456789
|
||||
|
||||
# Path on controller (this machine) to source bot script for deployment.
|
||||
tsj_bot_source_local_path: "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/proxmox/tsj_guardian_bot.py"
|
||||
tsj_bot_runtime_root: "/opt/infra-admin"
|
||||
|
||||
# Optional bot tuning
|
||||
tsj_bot_check_interval_sec: 60
|
||||
@@ -37,8 +38,8 @@ tsj_bot_openvpn_expiry_warn_enabled: "false"
|
||||
tsj_bot_openvpn_expiry_warn_days: 30
|
||||
tsj_bot_openvpn_expiry_warn_timeout_sec: 120
|
||||
tsj_bot_openvpn_expiry_warn_interval_sec: 21600
|
||||
tsj_bot_server_fallback_commands: "/home/codex/infra-admin/scripts/system_self_support.sh --heal"
|
||||
tsj_bot_updates_script: "/usr/bin/python3 /home/codex/infra-admin/scripts/proxmox_lxc_critical_updates.py"
|
||||
tsj_bot_server_fallback_commands: "/opt/infra-admin/scripts/system_self_support.sh --heal"
|
||||
tsj_bot_updates_script: "/usr/bin/python3 /opt/infra-admin/scripts/proxmox_lxc_critical_updates.py"
|
||||
tsj_bot_update_targets: "auto"
|
||||
tsj_bot_pfsense_mcp_bearer: "CHANGE_ME"
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ aw_windows_package_version: "v0.13.2"
|
||||
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
|
||||
aw_windows_package_zip_path: ""
|
||||
aw_windows_domain: "SHARKON2025"
|
||||
# Localized name of the built-in local Administrator account (SID ending in -500).
|
||||
# On the current Russian Windows host this must stay "Администратор";
|
||||
# do not replace it with "Administrator" unless the target OS account is actually named that way.
|
||||
aw_windows_builtin_administrator_name: "Администратор"
|
||||
aw_windows_users:
|
||||
- Администратор
|
||||
- user1
|
||||
@@ -46,6 +50,7 @@ aw_windows_evtx_channels:
|
||||
- Microsoft-Windows-TerminalServices-LocalSessionManager/Operational
|
||||
- Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational
|
||||
aw_windows_logon_marker_enabled: true
|
||||
aw_windows_process_events_enabled: true
|
||||
aw_windows_skip_hardening: false
|
||||
|
||||
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
AW_DATA_DIR="${AW_DATA_DIR:-/var/lib/activitywatch}"
|
||||
BACKUP_DIR="${AW_BACKUP_DIR:-${AW_DATA_DIR}/backups}"
|
||||
KEEP_DAYS="${AW_BACKUP_RETENTION_DAYS:-7}"
|
||||
KEEP_LAST_DB="${AW_BACKUP_KEEP_LAST_DB:-2}"
|
||||
KEEP_LAST_JSON="${AW_BACKUP_KEEP_LAST_JSON:-2}"
|
||||
|
||||
prune_group() {
|
||||
local keep_last="$1"
|
||||
local keep_days="$2"
|
||||
shift 2
|
||||
local files=()
|
||||
local idx=0
|
||||
local cutoff
|
||||
cutoff="$(date -d "-${keep_days} days" +%s)"
|
||||
mapfile -t files < <(find "$@" -maxdepth 1 -type f -printf '%T@ %p\n' 2>/dev/null | sort -nr | awk '{ $1=""; sub(/^ /,""); print }')
|
||||
for path in "${files[@]}"; do
|
||||
idx=$((idx + 1))
|
||||
if [ "$idx" -le "$keep_last" ]; then
|
||||
continue
|
||||
fi
|
||||
[ -f "$path" ] || continue
|
||||
if [ "$(stat -c %Y "$path")" -lt "$cutoff" ]; then
|
||||
rm -f -- "$path"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
prune_group "$KEEP_LAST_DB" "$KEEP_DAYS" "${BACKUP_DIR}/db"
|
||||
prune_group "$KEEP_LAST_JSON" "$KEEP_DAYS" "$BACKUP_DIR"
|
||||
|
||||
find /tmp -maxdepth 1 -type f \
|
||||
\( -name 'activitywatch-*.zip' -o -name 'hayabusa-*.zip' -o -name 'aw-hayabusa-profiles.txt' \) \
|
||||
-mtime +0 -delete 2>/dev/null || true
|
||||
|
||||
find /tmp -maxdepth 1 -type f \
|
||||
\( -name 'aw-worktime-ui-bridge.py' -o -name 'views-default.json' -o -name 'apply_webui_ru_patch.out' \) \
|
||||
-mtime +1 -delete 2>/dev/null || true
|
||||
+24
-11
@@ -24,6 +24,7 @@
|
||||
["Tools", "Инструменты"],
|
||||
["Raw Data", "Сырые данные"],
|
||||
["Summary", "Сводка"],
|
||||
["Worktime", "Рабочее время"],
|
||||
["All", "Все"],
|
||||
["None", "Нет"],
|
||||
["Date", "Дата"],
|
||||
@@ -236,6 +237,7 @@
|
||||
['Common words in "Uncategorized" events', 'Частые слова в событиях "Без категории"'],
|
||||
["No words with significant duration. You're good to go!", "Нет слов со значимой длительностью. Здесь всё в порядке."],
|
||||
["Top apps", "Топ приложений"],
|
||||
["Top Applications", "Топ приложений"],
|
||||
["Top titles", "Топ заголовков"],
|
||||
["Top URLs", "Топ URL"],
|
||||
["Top domains", "Топ доменов"],
|
||||
@@ -596,8 +598,8 @@
|
||||
}, "");
|
||||
|
||||
center.innerHTML =
|
||||
'<h4>RDP summary</h4>' +
|
||||
'<p>Этот блок строится из server-side worktime отчёта и не зависит от того, жива ли локальная интерактивная RDP-сессия.</p>' +
|
||||
'<h4>RDP сводка</h4>' +
|
||||
'<p>Этот блок строится из bucket <code>aw-worktime-sessions</code> через AW API и показывает сводку по RDP-сессиям выбранного хоста.</p>' +
|
||||
'<div class="aw-ru-rdp-grid">' +
|
||||
'<section class="aw-ru-rdp-card"><h5>Активное время</h5><div class="aw-ru-rdp-value">' + escapeHtml(formatDurationSeconds(totalActiveSeconds)) + '</div></section>' +
|
||||
'<section class="aw-ru-rdp-card"><h5>Активных пользователей</h5><div class="aw-ru-rdp-value">' + escapeHtml(String(activeUsers.length)) + '</div></section>' +
|
||||
@@ -844,9 +846,15 @@
|
||||
}
|
||||
|
||||
function removeBadDlpLinks(root) {
|
||||
const badLinks = root.querySelectorAll("a[href*='/view/DLP']");
|
||||
badLinks.forEach(function (link) {
|
||||
const links = Array.from(root.querySelectorAll("a[href], [role='link']"));
|
||||
links.forEach(function (link) {
|
||||
const href = String(link.getAttribute("href") || "");
|
||||
const label = normalizeText(link.textContent || "");
|
||||
const isBrokenActivityDlpLink = /\/view\/dlp(?:[/?#]|$)/i.test(href);
|
||||
const isActivityTabDlpLabel = label === "DLP" && !!link.closest("li");
|
||||
if (!isBrokenActivityDlpLink && !isActivityTabDlpLabel) return;
|
||||
const item = link.closest("li") || link;
|
||||
if (item && item.getAttribute && item.getAttribute("data-aw-ru-dlp-item") === "1") return;
|
||||
item.remove();
|
||||
});
|
||||
}
|
||||
@@ -2113,6 +2121,16 @@
|
||||
});
|
||||
}
|
||||
|
||||
function applyTextAndNavigationPatches(root) {
|
||||
if (!root) return;
|
||||
walk(root);
|
||||
translateAttributes(root);
|
||||
hideNoiseNavigation(root);
|
||||
hidePveAuditTabForRegularHost(root);
|
||||
patchActivityHeading(root);
|
||||
patchCategoryBuilderHostLabel(root);
|
||||
}
|
||||
|
||||
function detachObserver() {
|
||||
if (!observerAttached) return;
|
||||
observer.disconnect();
|
||||
@@ -2147,6 +2165,7 @@
|
||||
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
|
||||
}
|
||||
}
|
||||
applyTextAndNavigationPatches(document.body);
|
||||
staticPatchRouteKey = routeKey;
|
||||
return;
|
||||
}
|
||||
@@ -2154,13 +2173,8 @@
|
||||
ensureSettingsHost();
|
||||
ensureHostGroupsData().catch(function () {});
|
||||
normalizeCategoryBuilderUnknownHostRefs();
|
||||
applyTextAndNavigationPatches(document.body);
|
||||
if (routeChanged) {
|
||||
walk(document.body);
|
||||
translateAttributes(document.body);
|
||||
hideNoiseNavigation(document.body);
|
||||
hidePveAuditTabForRegularHost(document.body);
|
||||
patchActivityHeading(document.body);
|
||||
patchCategoryBuilderHostLabel(document.body);
|
||||
staticPatchRouteKey = routeKey;
|
||||
}
|
||||
injectPveAuditCenter(document.body);
|
||||
@@ -2187,7 +2201,6 @@
|
||||
|
||||
const observer = new MutationObserver(function () {
|
||||
if (applyPatchInFlight) return;
|
||||
if (isDlpSignalBucketRoute() && document.body && document.body.querySelector("[data-aw-ru-dlp-center='1']")) return;
|
||||
scheduleApplyPatch();
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
@@ -46,8 +47,14 @@ MANAGER_CACHE_TTL_SECONDS = max(0, int(os.environ.get("AW_WORKTIME_MANAGER_CACHE
|
||||
MANAGER_CACHE_DIR = Path(os.environ.get("AW_WORKTIME_MANAGER_CACHE_DIR", "/var/lib/activitywatch/worktime-cache"))
|
||||
MANAGER_ALIASES_JSON = Path(os.environ.get("AW_WORKTIME_MANAGER_ALIASES_JSON", "/etc/activitywatch/worktime-manager-aliases.json"))
|
||||
MANAGER_EXCLUDE_USERS = {item.strip().lower() for item in os.environ.get("AW_WORKTIME_MANAGER_EXCLUDE_USERS", "").split(",") if item.strip()}
|
||||
EVENTS_CACHE_TTL_SECONDS = max(0, int(os.environ.get("AW_WORKTIME_EVENTS_CACHE_TTL_SECONDS", "30")))
|
||||
WORKTIME_EVENTS_LIMIT = max(1000, int(os.environ.get("AW_WORKTIME_EVENTS_LIMIT", "50000")))
|
||||
MODULE_PATH = Path(__file__).resolve()
|
||||
_ALIASES_CACHE = {"mtime": None, "users": {}, "owners": {}, "raw": {}}
|
||||
_EVENTS_CACHE_LOCK = threading.Lock()
|
||||
_EVENTS_CACHE = {}
|
||||
_MANAGEMENT_BUILD_LOCKS_LOCK = threading.Lock()
|
||||
_MANAGEMENT_BUILD_LOCKS = {}
|
||||
|
||||
|
||||
def get(u):
|
||||
@@ -274,6 +281,16 @@ def get_sessions_bucket_id(host):
|
||||
return f"aw-worktime-sessions_{resolve_host(host)}"
|
||||
|
||||
|
||||
def get_management_build_lock(host, report_date):
|
||||
key = (resolve_host(host), report_date.isoformat())
|
||||
with _MANAGEMENT_BUILD_LOCKS_LOCK:
|
||||
lock = _MANAGEMENT_BUILD_LOCKS.get(key)
|
||||
if lock is None:
|
||||
lock = threading.Lock()
|
||||
_MANAGEMENT_BUILD_LOCKS[key] = lock
|
||||
return lock
|
||||
|
||||
|
||||
def resolve_report_date(day=None, date_text=None):
|
||||
now_local = datetime.now(REPORT_TZ)
|
||||
if date_text:
|
||||
@@ -512,17 +529,31 @@ def aggregate_hourly_rows(events, start, end, host):
|
||||
def fetch_events_for_date(host, report_date):
|
||||
bounds = get_report_bounds(report_date)
|
||||
bucket_id = get_sessions_bucket_id(host)
|
||||
events = fetch_bucket_events(bucket_id, host)
|
||||
return bounds, events
|
||||
|
||||
|
||||
def fetch_bucket_events(bucket_id, host):
|
||||
now = now_utc()
|
||||
if EVENTS_CACHE_TTL_SECONDS > 0:
|
||||
with _EVENTS_CACHE_LOCK:
|
||||
cached = _EVENTS_CACHE.get(bucket_id)
|
||||
if cached is not None and age_seconds(cached["stored_at"], now=now) <= EVENTS_CACHE_TTL_SECONDS:
|
||||
return cached["events"]
|
||||
try:
|
||||
get(f"{AW}/buckets/{bucket_id}")
|
||||
except Exception:
|
||||
log_warning(f"bucket lookup failed for host={host} bucket={bucket_id} aw_base={AW}")
|
||||
return bounds, []
|
||||
return []
|
||||
try:
|
||||
events = get(f"{AW}/buckets/{bucket_id}/events?limit=50000")
|
||||
events = get(f"{AW}/buckets/{bucket_id}/events?limit={WORKTIME_EVENTS_LIMIT}")
|
||||
except Exception:
|
||||
log_warning(f"events fetch failed for host={host} bucket={bucket_id} aw_base={AW}")
|
||||
return bounds, []
|
||||
return bounds, events
|
||||
return []
|
||||
if EVENTS_CACHE_TTL_SECONDS > 0:
|
||||
with _EVENTS_CACHE_LOCK:
|
||||
_EVENTS_CACHE[bucket_id] = {"stored_at": now, "events": events}
|
||||
return events
|
||||
|
||||
|
||||
def build_report_summary(rows):
|
||||
@@ -971,9 +1002,10 @@ def load_management_cache(host, report_date):
|
||||
path = management_cache_path(host, report_date)
|
||||
if not path.exists():
|
||||
return None
|
||||
age = age_seconds(datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc))
|
||||
if age is None or age > MANAGER_CACHE_TTL_SECONDS:
|
||||
return None
|
||||
if report_date >= datetime.now(REPORT_TZ).date():
|
||||
age = age_seconds(datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc))
|
||||
if age is None or age > MANAGER_CACHE_TTL_SECONDS:
|
||||
return None
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
@@ -1360,27 +1392,42 @@ def _build_management_core(rows, host, report_date, owner_filter="", department_
|
||||
}
|
||||
|
||||
|
||||
def build_management_trend(host, anchor_date, owner_filter="", department_filter=""):
|
||||
def _management_trend_item(payload, report_date):
|
||||
summary = payload["summary"]
|
||||
return {
|
||||
"report_date": report_date.isoformat(),
|
||||
"users_count": summary["users_count"],
|
||||
"active_users": summary["active_users"],
|
||||
"inactive_users": summary["inactive_users"],
|
||||
"workday_total_active_seconds": summary["workday_total_active_seconds"],
|
||||
"workday_total_active_hhmm": summary["workday_total_active_hhmm"],
|
||||
"portfolio_coverage_pct": summary["portfolio_coverage_pct"],
|
||||
"actions_count": summary["actions_count"],
|
||||
"critical_actions_count": summary["critical_actions_count"],
|
||||
}
|
||||
|
||||
|
||||
def build_management_trend(host, anchor_date, owner_filter="", department_filter="", precomputed_payloads=None):
|
||||
trend = []
|
||||
precomputed_payloads = precomputed_payloads or {}
|
||||
for offset in range(MANAGER_TREND_DAYS - 1, -1, -1):
|
||||
current_date = anchor_date - timedelta(days=offset)
|
||||
bounds, events = fetch_events_for_date(host, current_date)
|
||||
rows = aggregate_rows_with_intervals(events, bounds["start"], bounds["end"], host)
|
||||
payload = _build_management_core(rows, host, current_date, owner_filter=owner_filter, department_filter=department_filter)
|
||||
summary = payload["summary"]
|
||||
trend.append(
|
||||
{
|
||||
"report_date": current_date.isoformat(),
|
||||
"users_count": summary["users_count"],
|
||||
"active_users": summary["active_users"],
|
||||
"inactive_users": summary["inactive_users"],
|
||||
"workday_total_active_seconds": summary["workday_total_active_seconds"],
|
||||
"workday_total_active_hhmm": summary["workday_total_active_hhmm"],
|
||||
"portfolio_coverage_pct": summary["portfolio_coverage_pct"],
|
||||
"actions_count": summary["actions_count"],
|
||||
"critical_actions_count": summary["critical_actions_count"],
|
||||
}
|
||||
)
|
||||
payload = precomputed_payloads.get(current_date)
|
||||
if payload is None:
|
||||
payload = load_management_cache(host, current_date)
|
||||
if payload is None:
|
||||
bounds, events = fetch_events_for_date(host, current_date)
|
||||
rows = aggregate_rows_with_intervals(events, bounds["start"], bounds["end"], host)
|
||||
payload = _build_management_core(rows, host, current_date, owner_filter=owner_filter, department_filter=department_filter)
|
||||
elif owner_filter or department_filter:
|
||||
payload = apply_management_filters_to_payload(
|
||||
payload,
|
||||
owner_filter=owner_filter,
|
||||
department_filter=department_filter,
|
||||
include_sources=False,
|
||||
include_source_actions=False,
|
||||
)
|
||||
trend.append(_management_trend_item(payload, current_date))
|
||||
return trend
|
||||
|
||||
|
||||
@@ -1423,7 +1470,13 @@ def build_management_payload(rows, host, report_date, owner_filter="", departmen
|
||||
payload = _build_management_core(rows, host, report_date, owner_filter=owner_filter, department_filter=department_filter)
|
||||
source_freshness, source_actions = build_source_freshness(resolve_host(host))
|
||||
payload["sources"] = source_freshness
|
||||
payload["trend"] = build_management_trend(resolve_host(host), report_date, owner_filter=owner_filter, department_filter=department_filter)
|
||||
payload["trend"] = build_management_trend(
|
||||
resolve_host(host),
|
||||
report_date,
|
||||
owner_filter=owner_filter,
|
||||
department_filter=department_filter,
|
||||
precomputed_payloads={report_date: payload},
|
||||
)
|
||||
payload["trend_scope"] = "portfolio"
|
||||
if source_actions:
|
||||
payload["actions"].extend(filter_management_actions(source_actions, payload["rows"], owner_filter=owner_filter, department_filter=department_filter))
|
||||
@@ -1461,11 +1514,16 @@ def management_report_for_date(host, report_date, owner_filter="", department_fi
|
||||
cached = load_management_cache(host, report_date)
|
||||
if cached is not None:
|
||||
return cached
|
||||
bounds, events = fetch_events_for_date(host, report_date)
|
||||
rows = aggregate_rows_with_intervals(events, bounds["start"], bounds["end"], host)
|
||||
payload = build_management_payload(rows, host, report_date)
|
||||
save_management_cache(host, report_date, payload)
|
||||
return payload
|
||||
lock = get_management_build_lock(host, report_date)
|
||||
with lock:
|
||||
cached = load_management_cache(host, report_date)
|
||||
if cached is not None:
|
||||
return cached
|
||||
bounds, events = fetch_events_for_date(host, report_date)
|
||||
rows = aggregate_rows_with_intervals(events, bounds["start"], bounds["end"], host)
|
||||
payload = build_management_payload(rows, host, report_date)
|
||||
save_management_cache(host, report_date, payload)
|
||||
return payload
|
||||
|
||||
|
||||
def report_today(host):
|
||||
|
||||
@@ -8,7 +8,7 @@ WORKTIME_HEALTH_URL="${WORKTIME_HEALTH_URL:-http://127.0.0.1:5610/health}"
|
||||
WORKTIME_REPORT_TIMEOUT_SECONDS="${WORKTIME_REPORT_TIMEOUT_SECONDS:-20}"
|
||||
WORKTIME_MANAGEMENT_WARM_ENABLED="${WORKTIME_MANAGEMENT_WARM_ENABLED:-1}"
|
||||
WORKTIME_MANAGEMENT_WARM_URL="${WORKTIME_MANAGEMENT_WARM_URL:-http://127.0.0.1:5610/reports/worktime/management?day=today&format=json}"
|
||||
WORKTIME_MANAGEMENT_WARM_TIMEOUT_SECONDS="${WORKTIME_MANAGEMENT_WARM_TIMEOUT_SECONDS:-25}"
|
||||
WORKTIME_MANAGEMENT_WARM_TIMEOUT_SECONDS="${WORKTIME_MANAGEMENT_WARM_TIMEOUT_SECONDS:-60}"
|
||||
WORKTIME_TODAY_PROBE_ENABLED="${WORKTIME_TODAY_PROBE_ENABLED:-1}"
|
||||
WORKTIME_TODAY_PROBE_URL="${WORKTIME_TODAY_PROBE_URL:-http://127.0.0.1:5610/reports/worktime/today?day=today&format=json}"
|
||||
WORKTIME_TODAY_PROBE_TIMEOUT_SECONDS="${WORKTIME_TODAY_PROBE_TIMEOUT_SECONDS:-20}"
|
||||
@@ -28,7 +28,6 @@ probe_url() {
|
||||
|
||||
probe_reports() {
|
||||
probe_url "$WORKTIME_HEALTH_URL" "$WORKTIME_REPORT_TIMEOUT_SECONDS" || return 1
|
||||
probe_url "$WORKTIME_MANAGEMENT_WARM_URL" "$WORKTIME_MANAGEMENT_WARM_TIMEOUT_SECONDS" || return 1
|
||||
if [[ "$WORKTIME_TODAY_PROBE_ENABLED" == "1" ]]; then
|
||||
probe_url "$WORKTIME_TODAY_PROBE_URL" "$WORKTIME_TODAY_PROBE_TIMEOUT_SECONDS" || return 1
|
||||
fi
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
(function () {
|
||||
var reportBase = "__AW_WORKTIME_REPORT_BASE__";
|
||||
function defaultDayQuery() {
|
||||
var now = new Date();
|
||||
return now.getHours() < 6 ? "day=yesterday" : "day=today";
|
||||
return "day=today";
|
||||
}
|
||||
|
||||
var dayQuery = defaultDayQuery();
|
||||
|
||||
@@ -12,7 +12,7 @@ STATE_PATH = os.environ.get(
|
||||
"AW_WORKTIME_UI_BRIDGE_STATE",
|
||||
"/var/lib/activitywatch/aw-worktime-ui-bridge-state.json",
|
||||
)
|
||||
TIMEOUT = float(os.environ.get("AW_WORKTIME_UI_BRIDGE_TIMEOUT", "20"))
|
||||
TIMEOUT = float(os.environ.get("AW_WORKTIME_UI_BRIDGE_TIMEOUT", "60"))
|
||||
WATCHER_FALLBACK_ENABLED = os.environ.get("AW_WORKTIME_UI_BRIDGE_WATCHER_FALLBACK", "1").strip().lower() not in {
|
||||
"0",
|
||||
"false",
|
||||
@@ -29,6 +29,8 @@ WATCHER_AFK_BUCKET = f"aw-watcher-afk_{HOST}"
|
||||
WATCHER_WINDOW_BUCKET = f"aw-watcher-window_{HOST}"
|
||||
WEB_CATEGORY_BUCKET = f"aw-detmir-web-category_{HOST}"
|
||||
COLLECTOR_HEALTH_MAX_AGE_SECONDS = float(os.environ.get("AW_WORKTIME_UI_BRIDGE_COLLECTOR_HEALTH_MAX_AGE_SECONDS", "300"))
|
||||
COLLECTOR_HEALTH_QUERY_LIMIT = int(os.environ.get("AW_WORKTIME_UI_BRIDGE_COLLECTOR_HEALTH_QUERY_LIMIT", "200"))
|
||||
FOREGROUND_CONTEXT_CACHE_SECONDS = float(os.environ.get("AW_WORKTIME_UI_BRIDGE_FOREGROUND_CACHE_SECONDS", "900"))
|
||||
|
||||
|
||||
def _req(method: str, path: str, payload=None):
|
||||
@@ -100,6 +102,8 @@ def watcher_window_needs_bridge_sync(now_utc: datetime):
|
||||
source = str(data.get("source", "")).strip().lower()
|
||||
app = str(data.get("app", "")).strip()
|
||||
title = str(data.get("title", "")).strip()
|
||||
if source == "aw-worktime-ui-bridge":
|
||||
return True
|
||||
if source != "aw-worktime-ui-bridge":
|
||||
return False
|
||||
if app.upper() == "RDP":
|
||||
@@ -148,22 +152,54 @@ def build_window_title(users, active_count):
|
||||
return f"RDP active ({active_count}): " + ", ".join(users)
|
||||
|
||||
|
||||
def get_latest_foreground_context(now_utc: datetime):
|
||||
def get_latest_active_session_ids(events):
|
||||
grouped = {}
|
||||
for event in events:
|
||||
ts = event.get("timestamp")
|
||||
if not ts:
|
||||
continue
|
||||
grouped.setdefault(ts, []).append(event)
|
||||
if not grouped:
|
||||
return set()
|
||||
latest_ts = max(grouped.keys(), key=lambda item: parse_iso_utc(item))
|
||||
active_session_ids = set()
|
||||
for event in grouped.get(latest_ts, []):
|
||||
data = event.get("data") or {}
|
||||
if not _is_session_active(data):
|
||||
continue
|
||||
try:
|
||||
active_session_ids.add(int(data.get("sessionId")))
|
||||
except Exception:
|
||||
continue
|
||||
return active_session_ids
|
||||
|
||||
|
||||
def _normalize_foreground_context(data):
|
||||
foreground_process = str(data.get("foregroundProcess", "")).strip()
|
||||
foreground_title = str(data.get("foregroundTitle", "")).strip()
|
||||
if not foreground_process and not foreground_title:
|
||||
return None
|
||||
return {
|
||||
"app": foreground_process if foreground_process.endswith(".exe") else f"{foreground_process}.exe",
|
||||
"title": foreground_title or foreground_process,
|
||||
}
|
||||
|
||||
|
||||
def get_latest_foreground_context(now_utc: datetime, active_session_ids=None, state=None):
|
||||
try:
|
||||
events = _req("GET", f"/api/0/buckets/{WEB_CATEGORY_BUCKET}/events?limit=20") or []
|
||||
events = _req("GET", f"/api/0/buckets/{WEB_CATEGORY_BUCKET}/events?limit={COLLECTOR_HEALTH_QUERY_LIMIT}") or []
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
return None
|
||||
raise
|
||||
events = []
|
||||
else:
|
||||
raise
|
||||
|
||||
for event in events:
|
||||
active_session_ids = set(active_session_ids or [])
|
||||
recent_candidates = []
|
||||
for event in reversed(events):
|
||||
data = event.get("data") or {}
|
||||
if str(data.get("signalType", "")).strip().lower() != "collector_health":
|
||||
continue
|
||||
foreground_process = str(data.get("foregroundProcess", "")).strip()
|
||||
foreground_title = str(data.get("foregroundTitle", "")).strip()
|
||||
if not foreground_process and not foreground_title:
|
||||
continue
|
||||
ts = event.get("timestamp")
|
||||
if not ts:
|
||||
continue
|
||||
@@ -173,11 +209,43 @@ def get_latest_foreground_context(now_utc: datetime):
|
||||
continue
|
||||
if (now_utc - event_dt).total_seconds() > COLLECTOR_HEALTH_MAX_AGE_SECONDS:
|
||||
continue
|
||||
return {
|
||||
"app": foreground_process if foreground_process.endswith(".exe") else f"{foreground_process}.exe",
|
||||
"title": foreground_title or foreground_process,
|
||||
}
|
||||
normalized = _normalize_foreground_context(data)
|
||||
if not normalized:
|
||||
continue
|
||||
session_id = data.get("sessionId")
|
||||
try:
|
||||
session_id = int(session_id)
|
||||
except Exception:
|
||||
session_id = None
|
||||
recent_candidates.append((session_id, event_dt, normalized))
|
||||
|
||||
for session_id, event_dt, normalized in recent_candidates:
|
||||
if active_session_ids and session_id in active_session_ids:
|
||||
normalized["timestamp"] = to_iso_utc(event_dt.isoformat())
|
||||
return normalized
|
||||
|
||||
if recent_candidates:
|
||||
session_id, event_dt, normalized = recent_candidates[0]
|
||||
normalized["timestamp"] = to_iso_utc(event_dt.isoformat())
|
||||
return normalized
|
||||
|
||||
cached = (state or {}).get("last_foreground_context")
|
||||
if isinstance(cached, dict):
|
||||
cached_ts = str(cached.get("timestamp", "")).strip()
|
||||
if cached_ts:
|
||||
try:
|
||||
cached_dt = parse_iso_utc(cached_ts)
|
||||
except Exception:
|
||||
cached_dt = None
|
||||
if cached_dt and (now_utc - cached_dt).total_seconds() <= FOREGROUND_CONTEXT_CACHE_SECONDS:
|
||||
app = str(cached.get("app", "")).strip()
|
||||
title = str(cached.get("title", "")).strip()
|
||||
if app or title:
|
||||
return {
|
||||
"app": app or "RDP",
|
||||
"title": title or app or "RDP",
|
||||
"timestamp": cached_ts,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
@@ -229,13 +297,19 @@ def transform(events, foreground_context=None):
|
||||
rows = grouped[ts]
|
||||
src_duration = max(float(r.get("duration", 0.0)) for r in rows)
|
||||
duration = src_duration
|
||||
cur_dt = parsed_ts.get(ts)
|
||||
next_dt = parsed_ts.get(ordered_ts[idx + 1]) if idx + 1 < len(ordered_ts) else None
|
||||
next_gap = None
|
||||
if cur_dt and next_dt:
|
||||
next_gap = max(0.0, (next_dt - cur_dt).total_seconds())
|
||||
if duration <= 0:
|
||||
cur_dt = parsed_ts.get(ts)
|
||||
next_dt = parsed_ts.get(ordered_ts[idx + 1]) if idx + 1 < len(ordered_ts) else None
|
||||
if cur_dt and next_dt:
|
||||
duration = max(0.0, (next_dt - cur_dt).total_seconds())
|
||||
duration = next_gap or 0.0
|
||||
if duration <= 0:
|
||||
duration = 10.0
|
||||
elif next_gap is not None and next_gap > 0:
|
||||
# Do not let a sampled session interval extend past the next sample.
|
||||
duration = min(duration, next_gap)
|
||||
duration = min(duration, 30.0)
|
||||
active_users = []
|
||||
for r in rows:
|
||||
@@ -271,6 +345,22 @@ def transform(events, foreground_context=None):
|
||||
return out_afk, out_win, last_ts
|
||||
|
||||
|
||||
def normalize_watcher_window_events(win_events):
|
||||
normalized = []
|
||||
for event in win_events:
|
||||
cloned = dict(event)
|
||||
data = dict(event.get("data") or {})
|
||||
app = str(data.get("app") or "").strip()
|
||||
title = str(data.get("title") or "").strip()
|
||||
if not app or app.upper() == "RDP":
|
||||
continue
|
||||
if app and app.upper() != "RDP" and " | RDP active (" in title:
|
||||
data["title"] = title.split(" | RDP active (", 1)[0].strip()
|
||||
cloned["data"] = data
|
||||
normalized.append(cloned)
|
||||
return normalized
|
||||
|
||||
|
||||
def main():
|
||||
state = load_state()
|
||||
last_ts = state.get("last_ts", "1970-01-01T00:00:00Z")
|
||||
@@ -301,10 +391,12 @@ def main():
|
||||
if not events:
|
||||
return
|
||||
|
||||
foreground_context = get_latest_foreground_context(now_utc)
|
||||
active_session_ids = get_latest_active_session_ids(events)
|
||||
foreground_context = get_latest_foreground_context(now_utc, active_session_ids=active_session_ids, state=state)
|
||||
afk_events, win_events, new_last_ts = transform(events, foreground_context=foreground_context)
|
||||
if not afk_events or not win_events or not new_last_ts:
|
||||
return
|
||||
watcher_win_events = normalize_watcher_window_events(win_events)
|
||||
|
||||
_req("POST", f"/api/0/buckets/{AFK_BUCKET}/events", afk_events)
|
||||
_req("POST", f"/api/0/buckets/{WINDOW_BUCKET}/events", win_events)
|
||||
@@ -312,10 +404,19 @@ def main():
|
||||
if bucket_needs_fallback(WATCHER_AFK_BUCKET, now_utc, WATCHER_FALLBACK_STALE_SECONDS):
|
||||
ensure_bucket(WATCHER_AFK_BUCKET, "afkstatus", "aw-watcher-afk")
|
||||
_req("POST", f"/api/0/buckets/{WATCHER_AFK_BUCKET}/events", afk_events)
|
||||
if watcher_window_needs_bridge_sync(now_utc):
|
||||
if watcher_win_events and watcher_window_needs_bridge_sync(now_utc):
|
||||
ensure_bucket(WATCHER_WINDOW_BUCKET, "currentwindow", "aw-watcher-window")
|
||||
_req("POST", f"/api/0/buckets/{WATCHER_WINDOW_BUCKET}/events", win_events)
|
||||
save_state({"last_ts": new_last_ts})
|
||||
_req("POST", f"/api/0/buckets/{WATCHER_WINDOW_BUCKET}/events", watcher_win_events)
|
||||
next_state = {"last_ts": new_last_ts}
|
||||
if foreground_context:
|
||||
next_state["last_foreground_context"] = {
|
||||
"app": str(foreground_context.get("app") or ""),
|
||||
"title": str(foreground_context.get("title") or ""),
|
||||
"timestamp": str(foreground_context.get("timestamp") or to_iso_utc(now_utc.isoformat())),
|
||||
}
|
||||
elif isinstance(state.get("last_foreground_context"), dict):
|
||||
next_state["last_foreground_context"] = state["last_foreground_context"]
|
||||
save_state(next_state)
|
||||
print(f"posted_afk={len(afk_events)} posted_win={len(win_events)} last_ts={new_last_ts}")
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Description=AW Worktime UI bridge (sessions -> afk/window)
|
||||
After=network-online.target activitywatch-server.service
|
||||
Wants=network-online.target
|
||||
StartLimitBurst=3
|
||||
StartLimitBurst=20
|
||||
StartLimitIntervalSec=120
|
||||
|
||||
[Service]
|
||||
|
||||
@@ -16,11 +16,6 @@
|
||||
{ "type": "top_apps", "size": 3, "props": {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "DLP",
|
||||
"name": "DLP",
|
||||
"elements": []
|
||||
},
|
||||
{
|
||||
"id": "worktime",
|
||||
"name": "Worktime",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import importlib.util
|
||||
import json
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
@@ -225,7 +227,7 @@ def test_build_management_payload_applies_alias_and_executive_summary():
|
||||
}
|
||||
}
|
||||
MODULE.build_source_freshness = lambda host: ([], [])
|
||||
MODULE.build_management_trend = lambda host, report_date, owner_filter="", department_filter="": []
|
||||
MODULE.build_management_trend = lambda host, report_date, owner_filter="", department_filter="", **kwargs: []
|
||||
rows = [
|
||||
{
|
||||
"user": "user1",
|
||||
@@ -342,6 +344,81 @@ def test_build_management_payload_filters_by_owner():
|
||||
MODULE.build_source_freshness = original_sources
|
||||
|
||||
|
||||
def _minimal_management_payload(report_date, users_count=1):
|
||||
return {
|
||||
"summary": {
|
||||
"users_count": users_count,
|
||||
"active_users": users_count,
|
||||
"inactive_users": 0,
|
||||
"workday_total_active_seconds": 3600 * users_count,
|
||||
"workday_total_active_hhmm": f"0{users_count}:00",
|
||||
"portfolio_coverage_pct": 100.0,
|
||||
"actions_count": 0,
|
||||
"critical_actions_count": 0,
|
||||
},
|
||||
"rows": [],
|
||||
"actions": [],
|
||||
"sources": [],
|
||||
"filters": {"owner": "", "department": ""},
|
||||
}
|
||||
|
||||
|
||||
def test_load_management_cache_keeps_historical_reports_after_ttl():
|
||||
original_dir = MODULE.MANAGER_CACHE_DIR
|
||||
original_ttl = MODULE.MANAGER_CACHE_TTL_SECONDS
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
MODULE.MANAGER_CACHE_DIR = Path(tmp)
|
||||
MODULE.MANAGER_CACHE_TTL_SECONDS = 1
|
||||
report_date = datetime(2026, 5, 14, tzinfo=timezone.utc).date()
|
||||
payload = _minimal_management_payload(report_date)
|
||||
path = MODULE.management_cache_path("SHARKON2025", report_date)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
loaded = MODULE.load_management_cache("SHARKON2025", report_date)
|
||||
assert loaded["summary"]["users_count"] == 1
|
||||
finally:
|
||||
MODULE.MANAGER_CACHE_DIR = original_dir
|
||||
MODULE.MANAGER_CACHE_TTL_SECONDS = original_ttl
|
||||
|
||||
|
||||
def test_build_management_trend_reuses_precomputed_anchor_payload():
|
||||
original_trend_days = MODULE.MANAGER_TREND_DAYS
|
||||
original_load = MODULE.load_management_cache
|
||||
original_fetch = MODULE.fetch_events_for_date
|
||||
original_aggregate = MODULE.aggregate_rows_with_intervals
|
||||
fetch_dates = []
|
||||
try:
|
||||
MODULE.MANAGER_TREND_DAYS = 3
|
||||
anchor = datetime(2026, 5, 14, tzinfo=timezone.utc).date()
|
||||
|
||||
def fake_load(host, report_date):
|
||||
if report_date < anchor:
|
||||
return _minimal_management_payload(report_date)
|
||||
return None
|
||||
|
||||
def fake_fetch(host, report_date):
|
||||
fetch_dates.append(report_date)
|
||||
return MODULE.get_report_bounds(report_date), []
|
||||
|
||||
MODULE.load_management_cache = fake_load
|
||||
MODULE.fetch_events_for_date = fake_fetch
|
||||
MODULE.aggregate_rows_with_intervals = lambda events, start, end, host: []
|
||||
trend = MODULE.build_management_trend(
|
||||
"SHARKON2025",
|
||||
anchor,
|
||||
precomputed_payloads={anchor: _minimal_management_payload(anchor, users_count=2)},
|
||||
)
|
||||
assert [item["report_date"] for item in trend] == ["2026-05-12", "2026-05-13", "2026-05-14"]
|
||||
assert trend[-1]["users_count"] == 2
|
||||
assert fetch_dates == []
|
||||
finally:
|
||||
MODULE.MANAGER_TREND_DAYS = original_trend_days
|
||||
MODULE.load_management_cache = original_load
|
||||
MODULE.fetch_events_for_date = original_fetch
|
||||
MODULE.aggregate_rows_with_intervals = original_aggregate
|
||||
|
||||
|
||||
def test_render_management_html_contains_action_queue():
|
||||
payload = {
|
||||
"generated_at_utc": "2026-05-14T12:00:00Z",
|
||||
|
||||
@@ -47,6 +47,79 @@ class WorktimeUiBridgeTests(unittest.TestCase):
|
||||
self.assertEqual(ctx["app"], "totalcmd.exe")
|
||||
self.assertEqual(ctx["title"], "Total Commander 6.01 - HARVEST")
|
||||
|
||||
def test_get_latest_active_session_ids_uses_latest_timestamp_group(self):
|
||||
events = [
|
||||
{
|
||||
"timestamp": "2026-05-27T07:59:25Z",
|
||||
"data": {"sessionId": 2, "state": "Активно", "username": "администратор", "sessionName": "console"},
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-05-27T07:59:30Z",
|
||||
"data": {"sessionId": 2, "state": "Активно", "username": "администратор", "sessionName": "console"},
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-05-27T07:59:30Z",
|
||||
"data": {"sessionId": 3, "state": "Активно", "username": "user5", "sessionName": "rdp-tcp#0"},
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-05-27T07:59:30Z",
|
||||
"data": {"sessionId": 4, "state": "Диск", "username": "user1", "sessionName": ""},
|
||||
},
|
||||
]
|
||||
self.assertEqual(MODULE.get_latest_active_session_ids(events), {2, 3})
|
||||
|
||||
def test_get_latest_foreground_context_prefers_active_session(self):
|
||||
now = datetime(2026, 5, 27, 8, 0, 0, tzinfo=timezone.utc)
|
||||
recent_events = [
|
||||
{
|
||||
"timestamp": "2026-05-27T07:59:40Z",
|
||||
"data": {
|
||||
"signalType": "collector_health",
|
||||
"foregroundProcess": "explorer",
|
||||
"foregroundTitle": "Explorer",
|
||||
"sessionId": 9,
|
||||
},
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-05-27T07:59:45Z",
|
||||
"data": {
|
||||
"signalType": "collector_health",
|
||||
"foregroundProcess": "totalcmd",
|
||||
"foregroundTitle": "Total Commander 6.01 - HARVEST",
|
||||
"sessionId": 2,
|
||||
},
|
||||
},
|
||||
]
|
||||
with mock.patch.object(MODULE, "_req", return_value=recent_events):
|
||||
ctx = MODULE.get_latest_foreground_context(now, active_session_ids={2})
|
||||
self.assertEqual(ctx["app"], "totalcmd.exe")
|
||||
self.assertEqual(ctx["title"], "Total Commander 6.01 - HARVEST")
|
||||
|
||||
def test_get_latest_foreground_context_uses_cached_context_when_recent_events_are_blank(self):
|
||||
now = datetime(2026, 5, 27, 8, 5, 0, tzinfo=timezone.utc)
|
||||
recent_events = [
|
||||
{
|
||||
"timestamp": "2026-05-27T08:04:50Z",
|
||||
"data": {
|
||||
"signalType": "collector_health",
|
||||
"foregroundProcess": "",
|
||||
"foregroundTitle": "",
|
||||
"sessionId": 3,
|
||||
},
|
||||
}
|
||||
]
|
||||
state = {
|
||||
"last_foreground_context": {
|
||||
"app": "totalcmd.exe",
|
||||
"title": "Total Commander 6.01 - HARVEST",
|
||||
"timestamp": "2026-05-27T08:03:30Z",
|
||||
}
|
||||
}
|
||||
with mock.patch.object(MODULE, "_req", return_value=recent_events):
|
||||
ctx = MODULE.get_latest_foreground_context(now, active_session_ids={2, 3}, state=state)
|
||||
self.assertEqual(ctx["app"], "totalcmd.exe")
|
||||
self.assertEqual(ctx["title"], "Total Commander 6.01 - HARVEST")
|
||||
|
||||
def test_transform_uses_foreground_context_for_active_sessions(self):
|
||||
events = [
|
||||
{
|
||||
@@ -69,6 +142,61 @@ class WorktimeUiBridgeTests(unittest.TestCase):
|
||||
self.assertEqual(win_events[0]["data"]["title"], "Total Commander 6.01 - HARVEST")
|
||||
self.assertEqual(last_ts, "2026-05-27T07:59:30Z")
|
||||
|
||||
def test_transform_caps_duration_at_next_timestamp_gap(self):
|
||||
events = [
|
||||
{
|
||||
"timestamp": "2026-05-27T07:59:30Z",
|
||||
"duration": 5,
|
||||
"data": {
|
||||
"username": "user5",
|
||||
"state": "Активно",
|
||||
"sessionId": 3,
|
||||
"sessionName": "rdp-tcp#0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-05-27T07:59:31Z",
|
||||
"duration": 5,
|
||||
"data": {
|
||||
"username": "user5",
|
||||
"state": "Активно",
|
||||
"sessionId": 3,
|
||||
"sessionName": "rdp-tcp#0",
|
||||
},
|
||||
},
|
||||
]
|
||||
_, win_events, _ = MODULE.transform(
|
||||
events,
|
||||
foreground_context={"app": "totalcmd.exe", "title": "Total Commander 6.01 - HARVEST"},
|
||||
)
|
||||
self.assertEqual(win_events[0]["duration"], 1.0)
|
||||
self.assertEqual(win_events[1]["duration"], 5)
|
||||
|
||||
def test_normalize_watcher_window_events_strips_rdp_suffix_for_real_apps(self):
|
||||
events = [
|
||||
{
|
||||
"timestamp": "2026-05-27T08:10:00Z",
|
||||
"duration": 5,
|
||||
"data": {
|
||||
"app": "totalcmd.exe",
|
||||
"title": "Total Commander 6.01 - HARVEST | RDP active (2): user5, администратор",
|
||||
"source": "aw-worktime-ui-bridge",
|
||||
},
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-05-27T08:10:05Z",
|
||||
"duration": 5,
|
||||
"data": {
|
||||
"app": "RDP",
|
||||
"title": "RDP active (2): user5, администратор",
|
||||
"source": "aw-worktime-ui-bridge",
|
||||
},
|
||||
},
|
||||
]
|
||||
normalized = MODULE.normalize_watcher_window_events(events)
|
||||
self.assertEqual(normalized[0]["data"]["title"], "Total Commander 6.01 - HARVEST")
|
||||
self.assertEqual(len(normalized), 1)
|
||||
|
||||
def test_watcher_window_needs_bridge_sync_for_generic_bridge_rdp(self):
|
||||
now = datetime(2026, 5, 27, 8, 5, 0, tzinfo=timezone.utc)
|
||||
latest_event = {
|
||||
@@ -83,6 +211,20 @@ class WorktimeUiBridgeTests(unittest.TestCase):
|
||||
mock.patch.object(MODULE, "get_latest_bucket_event_ts", return_value=datetime(2026, 5, 27, 8, 4, 40, tzinfo=timezone.utc)):
|
||||
self.assertTrue(MODULE.watcher_window_needs_bridge_sync(now))
|
||||
|
||||
def test_watcher_window_needs_bridge_sync_for_non_generic_bridge_event(self):
|
||||
now = datetime(2026, 5, 27, 8, 5, 0, tzinfo=timezone.utc)
|
||||
latest_event = {
|
||||
"timestamp": "2026-05-27T08:04:40Z",
|
||||
"data": {
|
||||
"app": "totalcmd.exe",
|
||||
"title": "Total Commander 6.01 - HARVEST",
|
||||
"source": "aw-worktime-ui-bridge",
|
||||
},
|
||||
}
|
||||
with mock.patch.object(MODULE, "get_latest_bucket_event", return_value=latest_event), \
|
||||
mock.patch.object(MODULE, "get_latest_bucket_event_ts", return_value=datetime(2026, 5, 27, 8, 4, 40, tzinfo=timezone.utc)):
|
||||
self.assertTrue(MODULE.watcher_window_needs_bridge_sync(now))
|
||||
|
||||
def test_watcher_window_does_not_override_real_watcher_stream(self):
|
||||
now = datetime(2026, 5, 27, 8, 5, 0, tzinfo=timezone.utc)
|
||||
latest_event = {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Дата фиксации: `2026-05-24`
|
||||
|
||||
Последнее runtime-уточнение: `2026-05-28`
|
||||
|
||||
Этот файл предназначен как единая рабочая опора по `DetMir`: что именно входит в систему, где это живет, каким инструментарием проект надо планировать и сопровождать, и какой операционный контур считать промышленным.
|
||||
|
||||
Если старые документы расходятся с этим файлом по адресам или runtime-ролям, для текущей эксплуатации приоритет у этого файла.
|
||||
@@ -43,6 +45,75 @@
|
||||
- операторский и gateway-контур должен считаться `10.10.10.2`;
|
||||
- Windows production-host для `DetMir` сейчас `192.168.100.18`, а не старые упоминания `192.168.100.21`.
|
||||
|
||||
### 2.1 Runtime snapshot после полной проверки 2026-05-28
|
||||
|
||||
Проверка выполнялась как production-contour test, а не только как HTTP ping.
|
||||
Покрыты:
|
||||
|
||||
- `AW-rus` API/WebUI на `10.10.10.13:5600`;
|
||||
- worktime/management API на `10.10.10.13:5610`;
|
||||
- Windows/RDP host `192.168.100.18` через WinRM/SSH/Scheduled Tasks;
|
||||
- `1C/file analytics` backend на `10.10.10.2:8710`;
|
||||
- Proxmox/nginx gateway на `10.10.10.2`;
|
||||
- Grafana на `10.10.10.11:3000`;
|
||||
- browser smoke через Playwright по operator-facing страницам.
|
||||
|
||||
Фактический результат после стабилизации:
|
||||
|
||||
| Проверка | Результат |
|
||||
|---|---|
|
||||
| `./check-aw-full.sh` | `FRESH=8 STALE=0 DEAD=0` |
|
||||
| `aw-rus-healthd.py --json` | `ok=13 warn=0 fail=0` |
|
||||
| `dlp-health-check --json` | `ok=20 warn=0 fail=0` |
|
||||
| `systemctl --failed` на `10.10.10.13` | `0 loaded units listed` |
|
||||
| Playwright browser smoke | `14/14` страниц открылись |
|
||||
| Grafana authenticated API/UI smoke | login OK, `19` dashboards в `/api/search`, все ключевые `1C File`/`DetMir` dashboards открылись |
|
||||
| Grafana datasource health | `OK` для `clickhouse-1c`, `InfluxDB-AW`, `loki`, Proxmox/pfSense Influx datasources |
|
||||
| Python unit tests по AW server/worktime/DLP/exporters | `36 passed` |
|
||||
| `proxmox.test_tsj_guardian_bot` | `25 tests OK` |
|
||||
| Windows `ActivityWatch Recovery` | `Last Result: 0` |
|
||||
|
||||
Ключевые runtime-факты на момент фиксации:
|
||||
|
||||
- `activitywatch-server`, `aw-worktime-api`, `aw-worktime-ui-bridge.timer`, `aw-worktime-autoheal.timer` активны;
|
||||
- свежие buckets: `aw-watcher-afk_*`, `aw-watcher-window_*`, `aw-worktime-sessions_*`, `aw-dlp-endpoint-signals_*`;
|
||||
- `aw-dlp-incidents_*`, `aw-dlp-review_*`, `aw-dlp-rules_*` могут быть event-driven и не обязаны двигаться каждую минуту;
|
||||
- Grafana dashboards без авторизации корректно редиректят на login, это не считается отказом; с сохраненной admin-учеткой проверены фактические страницы и datasource health;
|
||||
- gateway `/go/file1c-brief`, `/go/file1c-actions`, `/go/aw-ui` ведет на рабочие внутренние surface.
|
||||
|
||||
### 2.2 Стабилизация management report, bridge и recovery от 2026-05-28
|
||||
|
||||
До стабилизации слабые места были такими:
|
||||
|
||||
- холодный `management report` на `:5610` занимал примерно `38-58s`;
|
||||
- `aw-worktime-autoheal` мог считать тяжелый management warm частью health-check и перезапускать `aw-worktime-api`;
|
||||
- `aw-worktime-ui-bridge.service` периодически ловил `start-limit-hit`, хотя затем восстанавливался;
|
||||
- Windows task `ActivityWatch Recovery` оставался с `Last Result: 1`, несмотря на зеленый основной сбор данных.
|
||||
|
||||
Что изменено:
|
||||
|
||||
| Компонент | Файл | Решение |
|
||||
|---|---|---|
|
||||
| Management report API | `aw-server/aw-worktime-api.py` | Добавлены in-process events cache, build lock на `(host, report_date)`, переиспользование уже построенного payload для trend и чтение historical cache без TTL. |
|
||||
| Autoheal | `aw-server/aw-worktime-autoheal.sh` | Management warm больше не является обязательным health probe; timeout warm увеличен до `60s`. |
|
||||
| Worktime UI bridge | `aw-server/aw-worktime-ui-bridge.service` | `StartLimitBurst` поднят до `20`, чтобы штатные timer-запуски не переводили unit в `start-limit-hit`. |
|
||||
| Windows recovery | `windows/ActivityWatch.Windows.Common.psm1` | Усилен hidden wrapper, добавлен fallback через `schtasks.exe`, recovery task выбирает live interactive user, если SYSTEM path на хосте проблемный. |
|
||||
|
||||
Измеренный эффект:
|
||||
|
||||
| Сценарий | До | После |
|
||||
|---|---:|---:|
|
||||
| Cold/cold-ish management JSON | `38-58s` | около `11s` на сервере |
|
||||
| Повторный management JSON из cache | нестабильно | около `0.006s` на сервере |
|
||||
| Внешний первый request | до `58s` | около `18.9s` |
|
||||
| Внешний повторный request | нестабильно | около `0.215s` |
|
||||
|
||||
Операционное ограничение:
|
||||
|
||||
- полный `hardening-recovery.ps1` на Windows host может упираться в CIM/ScheduledTasks `message filter`;
|
||||
- для текущего production recovery закреплен рабочий путь через `schtasks.exe` и live interactive admin principal;
|
||||
- не запускать полный hardening-прогон без причины, если buckets свежие и `ActivityWatch Recovery` уже `Last Result: 0`.
|
||||
|
||||
## 3. Полный функциональный состав DetMir
|
||||
|
||||
### 3.1 Ядро AW-rus
|
||||
|
||||
@@ -25,6 +25,8 @@ Collector ownership in this model:
|
||||
- `file-operations-collector.ps1`: user-session path
|
||||
- `dlp-endpoint-signals-collector.ps1`: user-session path
|
||||
- `worktime-session-collector.ps1`: single global process under recovery path
|
||||
- publishes session presence and `process_start` / `process_stop` events for all visible user sessions, including `Disc`
|
||||
- this is session/process telemetry, not a replacement for per-user foreground window watchers
|
||||
|
||||
### 2. Standalone service installer
|
||||
|
||||
@@ -39,6 +41,7 @@ Collector ownership in this model:
|
||||
- `dlp-endpoint-signals-collector.ps1`: allowed
|
||||
- `file-operations-collector.ps1`: allowed
|
||||
- `worktime-session-collector.ps1`: allowed
|
||||
- still provides session/process telemetry, but not interactive foreground-window truth
|
||||
- `browser-domains-native-collector.ps1`: not reliable in Session 0
|
||||
- `email-outbound-collector.ps1`: not reliable in Session 0
|
||||
- `aw-watcher-afk` / `aw-watcher-window`: not a standalone Session 0 primitive
|
||||
|
||||
@@ -331,6 +331,7 @@ class TSJGuardianBot:
|
||||
BTN_AW_DFIR_LEGACY = "Hayabusa DFIR"
|
||||
BTN_AI_CHAT_ALIASES = ("AI чат", "Чат с поддержкой", "Техподдержка", "Тех поддержка")
|
||||
BTN_OVPN_CERTS_ALIASES = ("OpenVPN certs", "OpenVPN cert", "OpenVPN серты", "OpenVPN сертификат")
|
||||
INFRA_ADMIN_ROOT = "/opt/infra-admin"
|
||||
PFSENSE_ENV_PATH = "/home/igor/.config/tsj-bot/pfsense.env.readonly"
|
||||
PFSENSE_INVENTORY_PATH = "/home/igor/.config/tsj-bot/inventory.md"
|
||||
|
||||
@@ -358,8 +359,9 @@ class TSJGuardianBot:
|
||||
self.allowed_chats = {int(x.strip()) for x in chats_raw.split(",") if x.strip()}
|
||||
|
||||
self.default_chat_id = int(os.getenv("TELEGRAM_DEFAULT_CHAT_ID", str(min(self.allowed_chats))))
|
||||
self.infra_admin_root = os.getenv("INFRA_ADMIN_ROOT", self.INFRA_ADMIN_ROOT).strip() or self.INFRA_ADMIN_ROOT
|
||||
self.check_script = os.getenv(
|
||||
"CHECK_SCRIPT", "/home/codex/infra-admin/scripts/system_self_support.sh --check"
|
||||
"CHECK_SCRIPT", f"{self.infra_admin_root}/scripts/system_self_support.sh --check"
|
||||
)
|
||||
self.aw_rus_api_base = os.getenv("AW_RUS_API_BASE", "http://10.10.10.13:5600/api/0").strip()
|
||||
self.aw_rus_worktime_base = os.getenv("AW_RUS_WORKTIME_BASE", "http://10.10.10.13:5610").strip()
|
||||
@@ -411,16 +413,16 @@ class TSJGuardianBot:
|
||||
r"C:\ProgramData\AWatch-rus\email-outbound-collector.ps1",
|
||||
).strip() or r"C:\ProgramData\AWatch-rus\email-outbound-collector.ps1"
|
||||
self.heal_script = os.getenv(
|
||||
"HEAL_SCRIPT", "/home/codex/infra-admin/scripts/system_self_support.sh --heal"
|
||||
"HEAL_SCRIPT", f"{self.infra_admin_root}/scripts/system_self_support.sh --heal"
|
||||
)
|
||||
self.state_file = os.getenv(
|
||||
"STATE_FILE", "/home/codex/infra-admin/.state/tsj_guardian_state.json"
|
||||
"STATE_FILE", f"{self.infra_admin_root}/.state/tsj_guardian_state.json"
|
||||
)
|
||||
self.log_file = os.getenv(
|
||||
"LOG_FILE", "/home/codex/infra-admin/logs/tsj_guardian_bot.log"
|
||||
"LOG_FILE", f"{self.infra_admin_root}/logs/tsj_guardian_bot.log"
|
||||
)
|
||||
self.heartbeat_file = os.getenv(
|
||||
"HEARTBEAT_FILE", "/home/codex/infra-admin/.state/tsj_guardian_heartbeat"
|
||||
"HEARTBEAT_FILE", f"{self.infra_admin_root}/.state/tsj_guardian_heartbeat"
|
||||
)
|
||||
self.check_interval = env_int("CHECK_INTERVAL_SEC", 60)
|
||||
self.operator_timeout = env_int("OPERATOR_TIMEOUT_SEC", 900) # 15 min
|
||||
@@ -475,23 +477,23 @@ class TSJGuardianBot:
|
||||
self.server_fallback_commands = [
|
||||
x.strip() for x in os.getenv(
|
||||
"SERVER_FALLBACK_COMMANDS",
|
||||
"/home/codex/infra-admin/scripts/system_self_support.sh --heal"
|
||||
f"{self.infra_admin_root}/scripts/system_self_support.sh --heal"
|
||||
).split(";;") if x.strip()
|
||||
]
|
||||
self.updates_script = os.getenv(
|
||||
"UPDATES_SCRIPT",
|
||||
"/usr/bin/python3 /home/codex/infra-admin/scripts/proxmox_lxc_critical_updates.py",
|
||||
f"/usr/bin/python3 {self.infra_admin_root}/scripts/proxmox_lxc_critical_updates.py",
|
||||
)
|
||||
self.updates_status_file = Path(
|
||||
os.getenv(
|
||||
"UPDATES_STATUS_FILE",
|
||||
"/home/codex/infra-admin/.state/proxmox_lxc_critical_updates.json",
|
||||
f"{self.infra_admin_root}/.state/proxmox_lxc_critical_updates.json",
|
||||
)
|
||||
)
|
||||
self.updates_rollback_file = Path(
|
||||
os.getenv(
|
||||
"UPDATES_ROLLBACK_FILE",
|
||||
"/home/codex/infra-admin/.state/proxmox_lxc_pending_rollback.json",
|
||||
f"{self.infra_admin_root}/.state/proxmox_lxc_pending_rollback.json",
|
||||
)
|
||||
)
|
||||
self.proxmox_selection_ttl_sec = env_int("PROXMOX_SELECTION_TTL_SEC", 900)
|
||||
|
||||
@@ -295,6 +295,11 @@ function Get-ActivityWatchBuiltInAdministratorName {
|
||||
return $script:ActivityWatchBuiltInAdministratorName
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:AWATCH_RUS_BUILTIN_ADMINISTRATOR_NAME)) {
|
||||
$script:ActivityWatchBuiltInAdministratorName = [string]$env:AWATCH_RUS_BUILTIN_ADMINISTRATOR_NAME
|
||||
return $script:ActivityWatchBuiltInAdministratorName
|
||||
}
|
||||
|
||||
try {
|
||||
$account = Get-CimInstance Win32_UserAccount -Filter "LocalAccount=True" -ErrorAction Stop |
|
||||
Where-Object { [string]$_.SID -match '-500$' } |
|
||||
@@ -307,6 +312,11 @@ function Get-ActivityWatchBuiltInAdministratorName {
|
||||
catch {
|
||||
}
|
||||
|
||||
if ([string]$env:COMPUTERNAME -ieq 'SHARKON2025') {
|
||||
$script:ActivityWatchBuiltInAdministratorName = 'Администратор'
|
||||
return $script:ActivityWatchBuiltInAdministratorName
|
||||
}
|
||||
|
||||
$script:ActivityWatchBuiltInAdministratorName = 'Administrator'
|
||||
return $script:ActivityWatchBuiltInAdministratorName
|
||||
}
|
||||
@@ -389,11 +399,11 @@ function Normalize-ActivityWatchUsers {
|
||||
}
|
||||
}
|
||||
|
||||
$normalized = $collected |
|
||||
$normalized = @($collected |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||
ForEach-Object { Normalize-ActivityWatchUserId -UserId $_ -Domain $Domain } |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||
Sort-Object -Unique
|
||||
Sort-Object -Unique)
|
||||
|
||||
if (-not $normalized -or $normalized.Count -eq 0) {
|
||||
throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.'
|
||||
@@ -767,6 +777,7 @@ function New-ActivityWatchDeploymentConfig {
|
||||
[int]$EvtxRetentionDays = 14,
|
||||
[string[]]$EvtxChannels = @(),
|
||||
[bool]$LogonMarkerEnabled = $true,
|
||||
[bool]$ProcessEventsEnabled = $true,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LaunchScriptPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
@@ -888,8 +899,9 @@ function New-ActivityWatchDeploymentConfig {
|
||||
}
|
||||
}
|
||||
sessionEvents = [pscustomobject]@{
|
||||
logonEnabled = $LogonMarkerEnabled
|
||||
bucketPrefix = 'aw-session-events'
|
||||
logonEnabled = $LogonMarkerEnabled
|
||||
processEventsEnabled = $ProcessEventsEnabled
|
||||
bucketPrefix = 'aw-session-events'
|
||||
}
|
||||
recovery = [pscustomobject]@{
|
||||
intervalSeconds = $RecoveryIntervalSeconds
|
||||
@@ -964,7 +976,9 @@ Set-StrictMode -Version Latest
|
||||
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
|
||||
Add-Type -AssemblyName System.Net.Http
|
||||
`$script:MaxCollectorPowerShellProcesses = 24
|
||||
`$script:MaxCollectorPowerShellProcesses = 48
|
||||
`$script:CollectorProcessSnapshotLoaded = `$false
|
||||
`$script:CollectorProcessSnapshot = @()
|
||||
|
||||
function Get-DeploymentConfig {
|
||||
param([string]`$Path)
|
||||
@@ -980,6 +994,41 @@ function Test-ProcessInSession {
|
||||
return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1)
|
||||
}
|
||||
|
||||
function Get-CollectorProcessSnapshot {
|
||||
if (`$script:CollectorProcessSnapshotLoaded) {
|
||||
return @(`$script:CollectorProcessSnapshot)
|
||||
}
|
||||
|
||||
`$script:CollectorProcessSnapshotLoaded = `$true
|
||||
`$script:CollectorProcessSnapshot = @()
|
||||
`$job = `$null
|
||||
try {
|
||||
`$job = Start-Job -ScriptBlock {
|
||||
Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe' OR Name = 'pwsh.exe'" -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
`$_.CommandLine -match 'AWatch-rus' -and
|
||||
`$_.CommandLine -match '\.ps1'
|
||||
} |
|
||||
Select-Object ProcessId, SessionId, CommandLine
|
||||
}
|
||||
|
||||
if (Wait-Job -Job `$job -Timeout 4) {
|
||||
`$script:CollectorProcessSnapshot = @(Receive-Job -Job `$job -ErrorAction SilentlyContinue)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
`$script:CollectorProcessSnapshot = @()
|
||||
}
|
||||
finally {
|
||||
if (`$job) {
|
||||
Stop-Job -Job `$job -ErrorAction SilentlyContinue | Out-Null
|
||||
Remove-Job -Job `$job -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
return @(`$script:CollectorProcessSnapshot)
|
||||
}
|
||||
|
||||
function Test-CollectorRunning {
|
||||
param(
|
||||
[string]`$ScriptPath,
|
||||
@@ -987,9 +1036,8 @@ function Test-CollectorRunning {
|
||||
)
|
||||
|
||||
`$escapedCollector = [Regex]::Escape(`$ScriptPath)
|
||||
`$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||
`$processes = Get-CollectorProcessSnapshot |
|
||||
Where-Object {
|
||||
(`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and
|
||||
`$_.SessionId -eq `$SessionId -and
|
||||
`$_.CommandLine -match `$escapedCollector
|
||||
}
|
||||
@@ -998,14 +1046,7 @@ function Test-CollectorRunning {
|
||||
}
|
||||
|
||||
function Get-CollectorPowerShellProcessCount {
|
||||
`$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
(`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and
|
||||
`$_.CommandLine -match 'AWatch-rus' -and
|
||||
`$_.CommandLine -match '\.ps1'
|
||||
}
|
||||
|
||||
return @(`$processes).Count
|
||||
return @(Get-CollectorProcessSnapshot).Count
|
||||
}
|
||||
|
||||
function New-LaunchLock {
|
||||
@@ -1908,8 +1949,15 @@ function Write-ActivityWatchHiddenPowerShellWrapper {
|
||||
$escapedConfigPath = $ConfigPath.Replace('"', '""')
|
||||
|
||||
$content = @"
|
||||
On Error Resume Next
|
||||
Set shell = CreateObject("WScript.Shell")
|
||||
shell.Run """$escapedPowerShellExe"" -NoProfile -ExecutionPolicy Bypass -File ""$escapedScriptPath"" -ConfigPath ""$escapedConfigPath""", 0, False
|
||||
q = Chr(34)
|
||||
command = q & "$escapedPowerShellExe" & q & " -NoProfile -ExecutionPolicy Bypass -File " & q & "$escapedScriptPath" & q & " -ConfigPath " & q & "$escapedConfigPath" & q
|
||||
shell.Run command, 0, False
|
||||
If Err.Number <> 0 Then
|
||||
WScript.Quit 1
|
||||
End If
|
||||
WScript.Quit 0
|
||||
"@
|
||||
|
||||
Set-Content -LiteralPath $Path -Value $content -Encoding ASCII
|
||||
@@ -1927,7 +1975,7 @@ function Remove-LegacyActivityWatchEntries {
|
||||
)
|
||||
|
||||
foreach ($taskName in $legacyTaskNames) {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Remove-ActivityWatchScheduledTask -TaskName $taskName
|
||||
}
|
||||
|
||||
$legacyTaskPatterns = @(
|
||||
@@ -1943,7 +1991,15 @@ function Remove-LegacyActivityWatchEntries {
|
||||
'ActivityWatch File1C Upload'
|
||||
)
|
||||
|
||||
foreach ($task in @(Get-ScheduledTask -ErrorAction SilentlyContinue)) {
|
||||
$scheduledTasks = @()
|
||||
try {
|
||||
$scheduledTasks = @(Get-ScheduledTask -ErrorAction Stop)
|
||||
}
|
||||
catch {
|
||||
$scheduledTasks = @()
|
||||
}
|
||||
|
||||
foreach ($task in $scheduledTasks) {
|
||||
$taskName = [string]$task.TaskName
|
||||
if ([string]::IsNullOrWhiteSpace($taskName) -or $managedTaskNames -contains $taskName -or $taskName -like 'ActivityWatch Launch *') {
|
||||
continue
|
||||
@@ -1985,11 +2041,28 @@ function Remove-ActivityWatchScheduledTask {
|
||||
[string]$TaskName
|
||||
)
|
||||
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
& cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for ($attempt = 0; $attempt -lt 10; $attempt++) {
|
||||
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
$task = $null
|
||||
try {
|
||||
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
& cmd.exe /c "schtasks /Query /TN `"$TaskName`" >nul 2>&1" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (-not $task) {
|
||||
return
|
||||
}
|
||||
@@ -2138,11 +2211,45 @@ function Register-ActivityWatchRecoveryTask {
|
||||
$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
|
||||
$sessionRecords = @()
|
||||
try {
|
||||
$sessionRecords = @(Get-ActivityWatchSessionRecords)
|
||||
}
|
||||
catch {
|
||||
$sessionRecords = @()
|
||||
}
|
||||
$liveSession = @(Get-ActivityWatchLiveInteractiveSessions -SessionRecords $sessionRecords) | Select-Object -First 1
|
||||
$interactiveUserId = $null
|
||||
if ($liveSession -and -not [string]::IsNullOrWhiteSpace([string]$liveSession.UserName)) {
|
||||
$rawUser = [string]$liveSession.UserName
|
||||
$interactiveUserId = if ($rawUser -match '^[^\\]+\\') { $rawUser } else { ('{0}\{1}' -f $env:COMPUTERNAME, $rawUser) }
|
||||
}
|
||||
|
||||
if ($interactiveUserId) {
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $interactiveUserId
|
||||
$principal = New-ScheduledTaskPrincipal -UserId $interactiveUserId -LogonType Interactive -RunLevel Highest
|
||||
}
|
||||
else {
|
||||
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
}
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0)
|
||||
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
|
||||
try {
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -ErrorAction Stop | Out-Null
|
||||
}
|
||||
catch {
|
||||
$taskCommand = ('"{0}" {1}' -f $wscriptExe, $action.Arguments)
|
||||
if ($interactiveUserId) {
|
||||
& schtasks.exe /Create /TN $TaskName /SC ONLOGON /RU $interactiveUserId /IT /RL HIGHEST /F /TR $taskCommand | Out-Null
|
||||
}
|
||||
else {
|
||||
& schtasks.exe /Create /TN $TaskName /SC ONSTART /RU SYSTEM /RL HIGHEST /F /TR $taskCommand | Out-Null
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Register-ActivityWatchHayabusaAutoUploadTask {
|
||||
|
||||
@@ -27,6 +27,7 @@ param(
|
||||
[int]$EvtxRetentionDays = 14,
|
||||
[string[]]$EvtxChannels = @(),
|
||||
[bool]$LogonMarkerEnabled = $true,
|
||||
[bool]$ProcessEventsEnabled = $true,
|
||||
[string]$AwHostname,
|
||||
[string]$CustomRulesPath,
|
||||
[string]$CustomPolicyPath,
|
||||
@@ -140,6 +141,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-EvtxRetentionDays $EvtxRetentionDays `
|
||||
-EvtxChannels $EvtxChannels `
|
||||
-LogonMarkerEnabled $LogonMarkerEnabled `
|
||||
-ProcessEventsEnabled $ProcessEventsEnabled `
|
||||
-AwHostname $AwHostname `
|
||||
-PolicyMode $PolicyMode `
|
||||
-PolicyEngineEnabled $PolicyEngineEnabled `
|
||||
|
||||
@@ -27,6 +27,7 @@ param(
|
||||
[int]$EvtxRetentionDays = 14,
|
||||
[string[]]$EvtxChannels = @(),
|
||||
[bool]$LogonMarkerEnabled = $true,
|
||||
[bool]$ProcessEventsEnabled = $true,
|
||||
[string]$AwHostname,
|
||||
[string]$CustomRulesPath,
|
||||
[string]$CustomPolicyPath,
|
||||
@@ -99,6 +100,7 @@ if (-not (Test-Path -LiteralPath $deployScript)) {
|
||||
-EvtxRetentionDays $EvtxRetentionDays `
|
||||
-EvtxChannels $EvtxChannels `
|
||||
-LogonMarkerEnabled $LogonMarkerEnabled `
|
||||
-ProcessEventsEnabled $ProcessEventsEnabled `
|
||||
-AwHostname $AwHostname `
|
||||
-CustomRulesPath $CustomRulesPath `
|
||||
-CustomPolicyPath $CustomPolicyPath `
|
||||
@@ -145,6 +147,7 @@ if (-not $SkipHardening) {
|
||||
-EvtxRetentionDays $EvtxRetentionDays `
|
||||
-EvtxChannels $EvtxChannels `
|
||||
-LogonMarkerEnabled $LogonMarkerEnabled `
|
||||
-ProcessEventsEnabled $ProcessEventsEnabled `
|
||||
-AwHostname $AwHostname `
|
||||
-CustomRulesPath $CustomRulesPath `
|
||||
-CustomPolicyPath $CustomPolicyPath `
|
||||
|
||||
@@ -25,6 +25,7 @@ param(
|
||||
[int]$EvtxRetentionDays = 14,
|
||||
[string[]]$EvtxChannels = @(),
|
||||
[bool]$LogonMarkerEnabled = $true,
|
||||
[bool]$ProcessEventsEnabled = $true,
|
||||
[string]$AwHostname,
|
||||
[string]$CustomRulesPath,
|
||||
[string]$CustomPolicyPath
|
||||
@@ -102,6 +103,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-EvtxRetentionDays $EvtxRetentionDays `
|
||||
-EvtxChannels $EvtxChannels `
|
||||
-LogonMarkerEnabled $LogonMarkerEnabled `
|
||||
-ProcessEventsEnabled $ProcessEventsEnabled `
|
||||
-AwHostname $AwHostname `
|
||||
-LaunchScriptPath $launchScriptPath `
|
||||
-RecoveryScriptPath $recoveryScriptPath `
|
||||
|
||||
@@ -170,10 +170,19 @@ function Get-1CFileInfobases {
|
||||
}
|
||||
|
||||
function Get-HostSample {
|
||||
$os = Get-CimInstance Win32_OperatingSystem
|
||||
$cpuSample = Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue |
|
||||
Measure-Object -Property LoadPercentage -Average
|
||||
$cpu = if ($cpuSample.Count -gt 0 -and $null -ne $cpuSample.Average) { [double]$cpuSample.Average } else { 0 }
|
||||
$cpu = 0.0
|
||||
$ramPct = 0.0
|
||||
try {
|
||||
Add-Type -AssemblyName Microsoft.VisualBasic -ErrorAction Stop
|
||||
$computerInfo = New-Object Microsoft.VisualBasic.Devices.ComputerInfo
|
||||
$totalMemory = [double]$computerInfo.TotalPhysicalMemory
|
||||
$availableMemory = [double]$computerInfo.AvailablePhysicalMemory
|
||||
if ($totalMemory -gt 0) {
|
||||
$ramPct = (($totalMemory - $availableMemory) / $totalMemory) * 100
|
||||
}
|
||||
} catch {
|
||||
Write-RunLog ("warning: host memory sample fallback reason={0}" -f $_.Exception.Message)
|
||||
}
|
||||
$disk = Get-PSDrive -Name E -ErrorAction SilentlyContinue
|
||||
$rdp = (quser 2>$null | Select-Object -Skip 1 | Measure-Object).Count
|
||||
|
||||
@@ -181,7 +190,7 @@ function Get-HostSample {
|
||||
ts = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
||||
host = $env:COMPUTERNAME
|
||||
cpu_pct = [math]::Round($cpu, 2)
|
||||
ram_pct = [math]::Round((($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize) * 100, 2)
|
||||
ram_pct = [math]::Round($ramPct, 2)
|
||||
disk_free_gb = if ($disk) { [math]::Round($disk.Free / 1GB, 2) } else { 0 }
|
||||
disk_latency_ms = 0
|
||||
smb_errors = 0
|
||||
|
||||
@@ -24,6 +24,7 @@ param(
|
||||
[int]$EvtxRetentionDays,
|
||||
[string[]]$EvtxChannels,
|
||||
[bool]$LogonMarkerEnabled,
|
||||
[bool]$ProcessEventsEnabled,
|
||||
[string]$AwHostname,
|
||||
[string]$CustomRulesPath,
|
||||
[string]$CustomPolicyPath,
|
||||
@@ -93,6 +94,7 @@ $effectiveEvtxExportRoot = if ($PSBoundParameters.ContainsKey('EvtxExportRoot')
|
||||
$effectiveEvtxRetentionDays = if ($PSBoundParameters.ContainsKey('EvtxRetentionDays')) { [int]$EvtxRetentionDays } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'retentionDays') { [int]$existingConfig.forensics.retentionDays } else { 14 }
|
||||
$effectiveEvtxChannels = if ($PSBoundParameters.ContainsKey('EvtxChannels')) { @($EvtxChannels) } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'evtxChannels') { @($existingConfig.forensics.evtxChannels) } else { @() }
|
||||
$effectiveLogonMarkerEnabled = if ($PSBoundParameters.ContainsKey('LogonMarkerEnabled')) { [bool]$LogonMarkerEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'sessionEvents' -and $existingConfig.sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$existingConfig.sessionEvents.logonEnabled } else { $true }
|
||||
$effectiveProcessEventsEnabled = if ($PSBoundParameters.ContainsKey('ProcessEventsEnabled')) { [bool]$ProcessEventsEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'sessionEvents' -and $existingConfig.sessionEvents.PSObject.Properties.Name -contains 'processEventsEnabled') { [bool]$existingConfig.sessionEvents.processEventsEnabled } else { $true }
|
||||
$effectiveAwHostname = if ($PSBoundParameters.ContainsKey('AwHostname') -and -not [string]::IsNullOrWhiteSpace($AwHostname)) { [string]$AwHostname } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$existingConfig.awHostname)) { [string]$existingConfig.awHostname } else { [string]$env:COMPUTERNAME }
|
||||
$effectiveVersion = if ($Version) { $Version } elseif ($existingConfig) { [string]$existingConfig.package.version } else { 'v0.13.2' }
|
||||
$effectivePolicyMode = if ($PSBoundParameters.ContainsKey('PolicyMode') -and $PolicyMode) { [string]$PolicyMode } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'policyEngine' -and $existingConfig.policyEngine.PSObject.Properties.Name -contains 'mode') { [string]$existingConfig.policyEngine.mode } else { 'local' }
|
||||
@@ -202,6 +204,7 @@ $config = New-ActivityWatchDeploymentConfig `
|
||||
-EvtxRetentionDays $effectiveEvtxRetentionDays `
|
||||
-EvtxChannels $effectiveEvtxChannels `
|
||||
-LogonMarkerEnabled $effectiveLogonMarkerEnabled `
|
||||
-ProcessEventsEnabled $effectiveProcessEventsEnabled `
|
||||
-AwHostname $effectiveAwHostname `
|
||||
-PolicyMode $effectivePolicyMode `
|
||||
-PolicyEngineEnabled $effectivePolicyEngineEnabled `
|
||||
|
||||
@@ -36,6 +36,15 @@ $queueMaxDepth = 1000
|
||||
$afkExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true }
|
||||
$windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true }
|
||||
$fileOpsExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]$config.collectors.fileOpsEnabled } else { $true }
|
||||
$sessionEventsConfig = if ($config.PSObject.Properties.Name -contains 'sessionEvents') { $config.sessionEvents } else { $null }
|
||||
$sessionLogonEnabled = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$sessionEventsConfig.logonEnabled } else { $false }
|
||||
$sessionProcessEventsEnabled = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'processEventsEnabled') { [bool]$sessionEventsConfig.processEventsEnabled } else { $true }
|
||||
$sessionEventsBucketId = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]$sessionEventsConfig.bucketPrefix)) {
|
||||
('{0}_{1}' -f [string]$sessionEventsConfig.bucketPrefix, $awHostname)
|
||||
}
|
||||
else {
|
||||
'aw-session-events_' + $awHostname
|
||||
}
|
||||
|
||||
function Get-LoggedOnUsers {
|
||||
param(
|
||||
@@ -535,6 +544,12 @@ $result = [ordered]@{
|
||||
jobTitlePolicyEnabled = $printJobTitlePolicyEnabled
|
||||
ok = [bool]($printServiceOperationalEnabled -and $printJobTitlePolicyEnabled)
|
||||
}
|
||||
sessionEvents = [ordered]@{
|
||||
bucketId = $sessionEventsBucketId
|
||||
logonEnabled = [bool]$sessionLogonEnabled
|
||||
processEventsEnabled = [bool]$sessionProcessEventsEnabled
|
||||
ok = $true
|
||||
}
|
||||
forensics = [ordered]@{
|
||||
evtxExportRoot = if ($config.PSObject.Properties.Name -contains 'forensics' -and $config.forensics.PSObject.Properties.Name -contains 'evtxExportRoot') { [string]$config.forensics.evtxExportRoot } else { $null }
|
||||
retentionDays = if ($config.PSObject.Properties.Name -contains 'forensics' -and $config.forensics.PSObject.Properties.Name -contains 'retentionDays') { [int]$config.forensics.retentionDays } else { $null }
|
||||
|
||||
@@ -94,11 +94,13 @@ function Ensure-Bucket {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||
[Parameter(Mandatory = $true)][string]$HostnameValue,
|
||||
[string]$ClientName = 'aw-worktime-session-collector',
|
||||
[string]$BucketType = 'aw.worktime.session'
|
||||
)
|
||||
try { Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" -ErrorAction Stop | Out-Null; return } catch { Write-Verbose "Bucket not found, creating: $BucketId" }
|
||||
|
||||
$body = @{ client='aw-worktime-session-collector'; type='aw.worktime.session'; hostname=$HostnameValue } | ConvertTo-Json -Compress
|
||||
$body = @{ client=$ClientName; type=$BucketType; hostname=$HostnameValue } | ConvertTo-Json -Compress
|
||||
$attempts = 0
|
||||
while ($attempts -lt 3) {
|
||||
$attempts++
|
||||
@@ -230,16 +232,282 @@ function Get-CanonicalUserId {
|
||||
return "$HostnameValue\$normalizedUser"
|
||||
}
|
||||
|
||||
function Get-SessionEventsBucketId {
|
||||
param(
|
||||
[pscustomobject]$Config,
|
||||
[string]$HostnameValue
|
||||
)
|
||||
$prefix = 'aw-session-events'
|
||||
if (
|
||||
$Config -and
|
||||
$Config.PSObject.Properties.Name -contains 'sessionEvents' -and
|
||||
$Config.sessionEvents -and
|
||||
$Config.sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and
|
||||
-not [string]::IsNullOrWhiteSpace([string]$Config.sessionEvents.bucketPrefix)
|
||||
) {
|
||||
$prefix = [string]$Config.sessionEvents.bucketPrefix
|
||||
}
|
||||
return ('{0}_{1}' -f $prefix, $HostnameValue)
|
||||
}
|
||||
|
||||
function Test-SessionProcessEventsEnabled {
|
||||
param([pscustomobject]$Config)
|
||||
if (
|
||||
$Config -and
|
||||
$Config.PSObject.Properties.Name -contains 'sessionEvents' -and
|
||||
$Config.sessionEvents -and
|
||||
$Config.sessionEvents.PSObject.Properties.Name -contains 'processEventsEnabled'
|
||||
) {
|
||||
return [bool]$Config.sessionEvents.processEventsEnabled
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
function Get-ProcessStatePath {
|
||||
param([pscustomobject]$Config)
|
||||
$stateRoot = ''
|
||||
if ($Config -and $Config.PSObject.Properties.Name -contains 'paths' -and $Config.paths) {
|
||||
if ($Config.paths.PSObject.Properties.Name -contains 'stateRoot') {
|
||||
$stateRoot = [string]$Config.paths.stateRoot
|
||||
}
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($stateRoot)) {
|
||||
$stateRoot = 'C:\ProgramData\AWatch-rus'
|
||||
}
|
||||
return (Join-Path $stateRoot 'session-process-state.json')
|
||||
}
|
||||
|
||||
function Load-ProcessState {
|
||||
param([string]$Path)
|
||||
$map = @{}
|
||||
try {
|
||||
if (Test-Path -LiteralPath $Path) {
|
||||
$raw = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
|
||||
if (-not [string]::IsNullOrWhiteSpace($raw)) {
|
||||
$obj = $raw | ConvertFrom-Json -ErrorAction Stop
|
||||
foreach ($item in @($obj.processes)) {
|
||||
if (-not $item) { continue }
|
||||
$key = [string]$item.key
|
||||
if ([string]::IsNullOrWhiteSpace($key)) { continue }
|
||||
$map[$key] = $item
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "Process state load error: $($_.Exception.Message)"
|
||||
}
|
||||
return $map
|
||||
}
|
||||
|
||||
function Save-ProcessState {
|
||||
param(
|
||||
[string]$Path,
|
||||
[hashtable]$Map
|
||||
)
|
||||
try {
|
||||
$dir = Split-Path -Path $Path -Parent
|
||||
if ($dir -and -not (Test-Path -LiteralPath $dir)) {
|
||||
New-Item -Path $dir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
$items = @()
|
||||
foreach ($entry in $Map.GetEnumerator()) {
|
||||
$value = $entry.Value
|
||||
if ($null -eq $value) { continue }
|
||||
$items += [pscustomobject]@{
|
||||
key = [string]$entry.Key
|
||||
processId = [int]$value.processId
|
||||
sessionId = [int]$value.sessionId
|
||||
username = [string]$value.username
|
||||
userId = [string]$value.userId
|
||||
state = [string]$value.state
|
||||
processName = [string]$value.processName
|
||||
commandLine = [string]$value.commandLine
|
||||
createdAt = [string]$value.createdAt
|
||||
hostname = [string]$value.hostname
|
||||
}
|
||||
}
|
||||
$payload = [pscustomobject]@{ processes = $items }
|
||||
$payload | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $Path -Encoding UTF8
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "Process state save error: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-ExcludedSessionProcess {
|
||||
param(
|
||||
[string]$Name,
|
||||
[string]$CommandLine
|
||||
)
|
||||
$n = [string]$Name
|
||||
if ([string]::IsNullOrWhiteSpace($n)) { return $true }
|
||||
if ($n -match '^(Idle|System|Registry|svchost|services|lsass|winlogon|csrss|fontdrvhost|dwm|taskhostw|sihost|explorer)\.exe$') { return $true }
|
||||
if ($n -match '^(aw-watcher-afk|aw-watcher-window|conhost)\.exe$') { return $true }
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-SessionProcessSnapshot {
|
||||
param(
|
||||
[pscustomobject]$Config,
|
||||
[string]$HostnameValue,
|
||||
[object[]]$SessionRecords
|
||||
)
|
||||
$bySession = @{}
|
||||
foreach ($rec in @($SessionRecords)) {
|
||||
if ($null -eq $rec) { continue }
|
||||
$sid = [int]$rec.sessionId
|
||||
$bySession[$sid] = [pscustomobject]@{
|
||||
username = [string]$rec.username
|
||||
userId = Get-CanonicalUserId -Config $Config -HostnameValue $HostnameValue -Username ([string]$rec.username)
|
||||
state = [string]$rec.state
|
||||
}
|
||||
}
|
||||
|
||||
$snapshot = @{}
|
||||
if ($bySession.Count -eq 0) {
|
||||
return $snapshot
|
||||
}
|
||||
|
||||
try {
|
||||
$procs = Get-Process -ErrorAction Stop | Where-Object { $bySession.ContainsKey([int]$_.SessionId) }
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "Process snapshot error: $($_.Exception.Message)"
|
||||
return $snapshot
|
||||
}
|
||||
|
||||
foreach ($proc in @($procs)) {
|
||||
try {
|
||||
$sid = [int]$proc.SessionId
|
||||
}
|
||||
catch {
|
||||
continue
|
||||
}
|
||||
if (-not $bySession.ContainsKey($sid)) { continue }
|
||||
|
||||
$name = [string]$proc.ProcessName
|
||||
if ($name -and $name -notmatch '\.exe$') {
|
||||
$name = "$name.exe"
|
||||
}
|
||||
$commandLine = ''
|
||||
if (Test-ExcludedSessionProcess -Name $name -CommandLine $commandLine) { continue }
|
||||
|
||||
$createdAt = ''
|
||||
try {
|
||||
if ($proc.StartTime) {
|
||||
$createdAt = $proc.StartTime.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$createdAt = ''
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($createdAt)) {
|
||||
$createdAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||
}
|
||||
|
||||
$key = ('{0}|{1}|{2}' -f $sid, [int]$proc.Id, $createdAt)
|
||||
$sessionMeta = $bySession[$sid]
|
||||
$snapshot[$key] = [pscustomobject]@{
|
||||
processId = [int]$proc.Id
|
||||
sessionId = $sid
|
||||
username = [string]$sessionMeta.username
|
||||
userId = [string]$sessionMeta.userId
|
||||
state = [string]$sessionMeta.state
|
||||
processName = $name
|
||||
commandLine = $commandLine
|
||||
createdAt = $createdAt
|
||||
hostname = $HostnameValue
|
||||
}
|
||||
}
|
||||
return $snapshot
|
||||
}
|
||||
|
||||
function Publish-SessionProcessEvents {
|
||||
param(
|
||||
[string]$ApiBase,
|
||||
[string]$BucketId,
|
||||
[hashtable]$Previous,
|
||||
[hashtable]$Current
|
||||
)
|
||||
foreach ($entry in $Current.GetEnumerator()) {
|
||||
if ($Previous.ContainsKey($entry.Key)) { continue }
|
||||
$item = $entry.Value
|
||||
$payload = [pscustomobject]@{
|
||||
timestamp = [string]$item.createdAt
|
||||
duration = 0
|
||||
data = [pscustomobject]@{
|
||||
eventType = 'process_start'
|
||||
username = [string]$item.username
|
||||
userId = [string]$item.userId
|
||||
sessionId = [int]$item.sessionId
|
||||
state = [string]$item.state
|
||||
processId = [int]$item.processId
|
||||
processName = [string]$item.processName
|
||||
commandLine = [string]$item.commandLine
|
||||
createdAt = [string]$item.createdAt
|
||||
hostname = [string]$item.hostname
|
||||
source = 'worktime-session-collector'
|
||||
}
|
||||
} | ConvertTo-Json -Depth 6 -Compress
|
||||
try {
|
||||
[void](Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId/heartbeat?pulsetime=1" -Json $payload)
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "Process start publish error: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
$nowUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||
foreach ($entry in $Previous.GetEnumerator()) {
|
||||
if ($Current.ContainsKey($entry.Key)) { continue }
|
||||
$item = $entry.Value
|
||||
$payload = [pscustomobject]@{
|
||||
timestamp = $nowUtc
|
||||
duration = 0
|
||||
data = [pscustomobject]@{
|
||||
eventType = 'process_stop'
|
||||
username = [string]$item.username
|
||||
userId = [string]$item.userId
|
||||
sessionId = [int]$item.sessionId
|
||||
state = [string]$item.state
|
||||
processId = [int]$item.processId
|
||||
processName = [string]$item.processName
|
||||
commandLine = [string]$item.commandLine
|
||||
createdAt = [string]$item.createdAt
|
||||
hostname = [string]$item.hostname
|
||||
source = 'worktime-session-collector'
|
||||
}
|
||||
} | ConvertTo-Json -Depth 6 -Compress
|
||||
try {
|
||||
[void](Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId/heartbeat?pulsetime=1" -Json $payload)
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "Process stop publish error: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Main
|
||||
$cfg = Get-Config -Path $ConfigPath
|
||||
$hostValue = if ($Hostname -and $Hostname.Trim()) { $Hostname.Trim() } elseif ($cfg -and $cfg.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$cfg.awHostname)) { [string]$cfg.awHostname } elseif ($cfg -and $cfg.awHostname) { [string]$cfg.awHostname } else { [string]$env:COMPUTERNAME }
|
||||
try { $apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port } catch { throw 'Invalid server configuration in config file.' }
|
||||
|
||||
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||
$sessionEventsBucketId = Get-SessionEventsBucketId -Config $cfg -HostnameValue $hostValue
|
||||
$processEventsEnabled = Test-SessionProcessEventsEnabled -Config $cfg
|
||||
$processStatePath = Get-ProcessStatePath -Config $cfg
|
||||
$sleepSec = if ($PollSeconds -gt 0) { $PollSeconds } elseif ($cfg.collector -and $cfg.collector.pollSeconds) { [int]$cfg.collector.pollSeconds } else { 30 }
|
||||
$pulse = [Math]::Max($sleepSec * 3, 30)
|
||||
|
||||
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||
if ($processEventsEnabled) {
|
||||
Ensure-Bucket -ApiBase $apiBase -BucketId $sessionEventsBucketId -HostnameValue $hostValue -ClientName 'aw-session-events' -BucketType 'aw.session.event'
|
||||
$previousProcessState = Load-ProcessState -Path $processStatePath
|
||||
}
|
||||
else {
|
||||
$previousProcessState = @{}
|
||||
}
|
||||
|
||||
while ($true) {
|
||||
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||
@@ -265,6 +533,13 @@ while ($true) {
|
||||
)
|
||||
}
|
||||
|
||||
if ($processEventsEnabled) {
|
||||
$currentProcessState = Get-SessionProcessSnapshot -Config $cfg -HostnameValue $hostValue -SessionRecords $records
|
||||
Publish-SessionProcessEvents -ApiBase $apiBase -BucketId $sessionEventsBucketId -Previous $previousProcessState -Current $currentProcessState
|
||||
Save-ProcessState -Path $processStatePath -Map $currentProcessState
|
||||
$previousProcessState = $currentProcessState
|
||||
}
|
||||
|
||||
foreach ($rec in $records) {
|
||||
$canonicalUserId = Get-CanonicalUserId -Config $cfg -HostnameValue $hostValue -Username ([string]$rec.username)
|
||||
$payloadObj = [PSCustomObject]@{
|
||||
|
||||
Reference in New Issue
Block a user