diff --git a/scripts/aw-contour-diag.sh b/scripts/aw-contour-diag.sh new file mode 100644 index 0000000..0373e6d --- /dev/null +++ b/scripts/aw-contour-diag.sh @@ -0,0 +1,507 @@ +#!/usr/bin/env bash +# aw-contour-diag.sh - Диагностика всего контура ActivityWatch-Russian +# Запускать с машины администратора (где есть доступ по SSH/curl ко всем узлам). +# +# Использование: +# ./scripts/aw-contour-diag.sh # полная диагностика +# ./scripts/aw-contour-diag.sh --quick # быстрая (только AW server + buckets) +# ./scripts/aw-contour-diag.sh --skip-windows # без RDP/WinRM проверок +# +# При красных проверках в скрипте указаны разделы 'REMEDIATION: ...' +# с конкретными командами для восстановления. + +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ANSIBLE_DIR="$REPO_ROOT/ansible" +INVENTORY="$ANSIBLE_DIR/inventory.ini" +AW_SERVER="http://10.10.10.13:5600" +AW_WORKTIME_API="http://10.10.10.13:5610" +INFLUXDB_URL="http://10.10.10.10:8086" +GRAFANA_URL="http://10.10.10.11:3000" +PROXMOX_HOST="10.10.10.2" +AW_HOST="10.10.10.13" +GRAFANA_HOST="10.10.10.11" +INFLUXDB_HOST="10.10.10.10" +WINDOWS_HOST="192.168.100.18" +CLICKHOUSE_HOST="10.10.10.2" +SOURCE_HOSTNAME="SHARKON2025" + +QUICK_MODE=0 +SKIP_WINDOWS=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --quick) QUICK_MODE=1; shift ;; + --skip-windows) SKIP_WINDOWS=1; shift ;; + -h|--help) + echo "Usage: $(basename "$0") [--quick] [--skip-windows]" + exit 0 ;; + *) echo "Unknown: $1"; exit 2 ;; + esac +done + +export no_proxy="localhost,127.0.0.1,$PROXMOX_HOST,$AW_HOST,$GRAFANA_HOST,$INFLUXDB_HOST,$WINDOWS_HOST,$CLICKHOUSE_HOST,10.10.10.0/24,192.168.100.0/24" +export NO_PROXY="$no_proxy" + +OK_COUNT=0; WARN_COUNT=0; FAIL_COUNT=0; SKIP_COUNT=0 + +if [ -t 1 ]; then + RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' +else + RED=''; GREEN=''; YELLOW=''; CYAN=''; NC='' +fi + +pass() { OK_COUNT=$((OK_COUNT+1)); printf "%b[OK]%b %s\n" "$GREEN" "$NC" "$*"; } +warn() { WARN_COUNT=$((WARN_COUNT+1)); printf "%b[WARN]%b %s\n" "$YELLOW" "$NC" "$*"; } +fail() { FAIL_COUNT=$((FAIL_COUNT+1)); printf "%b[FAIL]%b %s\n" "$RED" "$NC" "$*"; } +skip() { SKIP_COUNT=$((SKIP_COUNT+1)); printf "%b[SKIP]%b %s\n" "$YELLOW" "$NC" "$*"; } +section() { printf "\n%b=== %s ===%b\n" "$CYAN" "$*" "$NC"; } +have() { command -v "$1" >/dev/null 2>&1; } + +check_tcp() { + local name="$1" host="$2" port="$3" + if timeout 4 bash -c ":/dev/null 2>&1; then + pass "TCP $host:$port ($name)" + else + fail "TCP $host:$port ($name)" + echo " REMEDIATION: Проверьте, запущен ли сервис на $host:$port." + echo " Для systemd: ssh igor@$host 'systemctl status '" + echo " Для Docker: ssh igor@$PROXMOX_HOST 'sudo docker ps | grep '" + fi +} + +check_http_code() { + local name="$1" url="$2" expected="${3:-^2[0-9][0-9]$}" + local tmp code + tmp="$(mktemp)" + code="$(curl -k -sS --connect-timeout 5 --max-time 15 -o "$tmp" -w '%{http_code}' "$url" 2>"$tmp.err")" + if printf "%s" "$code" | grep -Eq "$expected"; then + pass "HTTP $code $url ($name)" + else + fail "HTTP $code $url ($name)" + sed 's/^/ /' "$tmp.err" "$tmp" 2>/dev/null | head -20 + fi + rm -f "$tmp" "$tmp.err" +} + +check_http_json_key() { + local name="$1" url="$2" jq_filter="$3" remediation="$4" + local tmp + tmp="$(mktemp)" + if curl -k -fsS --connect-timeout 5 --max-time 20 "$url" -o "$tmp" 2>"$tmp.err" && jq -e "$jq_filter" "$tmp" >/dev/null 2>&1; then + pass "$name" + else + fail "$name" + sed 's/^/ /' "$tmp.err" "$tmp" 2>/dev/null | head -10 + echo " REMEDIATION: $remediation" + fi + rm -f "$tmp" "$tmp.err" +} + +check_bucket_freshness() { + local bucket="$1" label="$2" remediation="$3" + local bucket_id="${bucket}_${SOURCE_HOSTNAME}" + local tmp last_ts event_epoch now age_sec + tmp="$(mktemp)" + if ! curl -fsS --connect-timeout 5 --max-time 15 "$AW_SERVER/api/0/buckets/$bucket_id/events?limit=1" -o "$tmp" 2>"$tmp.err"; then + fail "bucket $label ($bucket_id) — запрос не удался" + sed 's/^/ /' "$tmp.err" | head -5 + echo " REMEDIATION: $remediation" + rm -f "$tmp" "$tmp.err" + return + fi + last_ts="$(jq -r '.[0].timestamp // empty' "$tmp" 2>/dev/null)" + rm -f "$tmp" + if [ -z "$last_ts" ]; then + warn "bucket $label ($bucket_id) — нет событий" + echo " REMEDIATION: $remediation" + return + fi + event_epoch="$(date -d "$last_ts" +%s 2>/dev/null || echo 0)" + now="$(date -u +%s)" + age_sec=$((now - event_epoch)) + + case "$bucket" in + aw-dlp-incidents|aw-dlp-review|aw-dlp-rules|aw-session-events) + if [ "$age_sec" -lt 86400 ]; then + pass "bucket $label — ${age_sec}s назад" + else + warn "bucket $label — ${age_sec}s назад (event-driven)" + fi ;; + aw-watcher-window|aw-dlp-endpoint-signals) + if [ "$age_sec" -lt 7200 ]; then + pass "bucket $label — ${age_sec}s назад" + else + warn "bucket $label — ${age_sec}s назад (INACTIVE)" + fi ;; + *) + if [ "$age_sec" -lt 3600 ]; then + pass "bucket $label — ${age_sec}s назад" + elif [ "$age_sec" -lt 86400 ]; then + warn "bucket $label — ${age_sec}s назад (STALE)" + echo " REMEDIATION: $remediation" + else + fail "bucket $label — ${age_sec}s назад (DEAD)" + echo " REMEDIATION: $remediation" + fi ;; + esac +} + +check_ansible_shell() { + local name="$1" group="$2" command="$3" + if ! have ansible; then + skip "$name (ansible not available)" + return + fi + if [ ! -f "$INVENTORY" ]; then + skip "$name (inventory not found: $INVENTORY)" + return + fi + local tmp + tmp="$(mktemp)" + if ANSIBLE_NOCOLOR=1 ansible "$group" -i "$INVENTORY" -m shell -a "$command" >"$tmp" 2>&1; then + pass "$name" + else + fail "$name" + sed 's/^/ /' "$tmp" | head -20 + fi + rm -f "$tmp" +} + +check_ansible_win_shell() { + local name="$1" command="$2" + if ! have ansible; then skip "$name (ansible not available)"; return; fi + if [ ! -f "$INVENTORY" ]; then skip "$name (inventory not found)"; return; fi + local tmp + tmp="$(mktemp)" + if ANSIBLE_NOCOLOR=1 ansible aw_windows -i "$INVENTORY" -m win_shell -a "$command" >"$tmp" 2>&1; then + pass "$name" + else + fail "$name" + sed 's/^/ /' "$tmp" | head -20 + fi + rm -f "$tmp" +} + +check_ansible_module() { + local name="$1" group="$2" module="$3" args="${4:-}" + if ! have ansible; then skip "$name (ansible not available)"; return; fi + if [ ! -f "$INVENTORY" ]; then skip "$name (inventory not found)"; return; fi + local tmp + tmp="$(mktemp)" + if ANSIBLE_NOCOLOR=1 ansible "$group" -i "$INVENTORY" -m "$module" ${args:+-a "$args"} >"$tmp" 2>&1; then + pass "$name" + else + fail "$name" + sed 's/^/ /' "$tmp" | head -20 + fi + rm -f "$tmp" +} + +ssh_with_diag_password() { + local host="$1" + shift + if [[ -z "${AW_DIAG_SSH_PASSWORD:-}" ]]; then + return 125 + fi + sshpass -p "$AW_DIAG_SSH_PASSWORD" ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no "igor@$host" "$@" +} + +ssh_aw() { ssh_with_diag_password 10.10.10.13 "$@"; } +ssh_pve() { ssh_with_diag_password 10.10.10.2 "$@"; } + +check_service_remote() { + local name="$1" host="$2" unit="$3" remediation="$4" + local result + result=$(ssh_with_diag_password "$host" "systemctl is-active $unit 2>/dev/null || echo not_found" 2>/dev/null) + local rc=$? + if [ "$rc" -eq 125 ]; then + skip "$name ($unit on $host — set AW_DIAG_SSH_PASSWORD for SSH checks)" + return + fi + if [ "$rc" -ne 0 ] || [ "$result" = "not_found" ]; then + skip "$name ($unit on $host — не удалось проверить)" + return + fi + if [ "$result" = "active" ]; then + pass "$name ($unit active on $host)" + else + fail "$name ($unit $result on $host)" + echo " REMEDIATION: $remediation" + fi +} + +printf "%b=== ActivityWatch-Russian: Диагностика контура ===%b\n" "$CYAN" "$NC" +echo " $(date -u '+%Y-%m-%d %H:%M:%S UTC')" +echo "" + +# ============================================================ +section "1. Локальные предусловия" +# ============================================================ +for cmd in bash curl jq timeout ssh sshpass; do + if have "$cmd"; then pass "утилита $cmd найдена"; else fail "утилита $cmd не найдена (установите: apt install $cmd)"; fi +done +echo "" + +# ============================================================ +section "2. TCP доступность узлов" +# ============================================================ +check_tcp "AW Server" "$AW_HOST" 5600 +check_tcp "Worktime API" "$AW_HOST" 5610 +check_tcp "RDP WinRM" "$WINDOWS_HOST" 5985 +check_tcp "Proxmox SSH" "$PROXMOX_HOST" 22 +check_tcp "Proxmox HTTPS" "$PROXMOX_HOST" 443 +check_tcp "1C Company API" "$PROXMOX_HOST" 8710 +check_tcp "ClickHouse HTTP" "$CLICKHOUSE_HOST" 8123 +check_tcp "ClickHouse Native" "$CLICKHOUSE_HOST" 9000 +check_tcp "InfluxDB" "$INFLUXDB_HOST" 8086 +check_tcp "Grafana" "$GRAFANA_HOST" 3000 + +if [ "$QUICK_MODE" = "1" ]; then + # В быстром режиме проверяем только AW Server и buckets + echo "" + section "3. AW Server (быстрый режим)" + check_http_code "AW Server info" "$AW_SERVER/api/0/info" '^200$' + check_http_code "AW Server CORS" "$AW_SERVER/api/0/settings/" '^200$' + check_http_code "Worktime API health" "$AW_WORKTIME_API/health" '^200$' + + section "4. Buckets (быстрый режим)" + for entry in \ + "aw-watcher-afk|AFK watcher|Запустите на RDP: schtasks /Run /TN \"ActivityWatch Recovery\" или schtasks /Run /TN \"ActivityWatch Launch [SHARKON2025_Администратор]\"" \ + "aw-watcher-window|Window watcher|Запустите через ansible: ansible aw_windows -i $INVENTORY -m win_shell -a 'Start-Process -FilePath \"C:\\Program Files\\AWatch-rus\\bin\\aw-watcher-window\\aw-watcher-window.exe\" -ArgumentList @(\"--host\", \"10.10.10.13\", \"--port\", \"5600\") -WindowStyle Hidden'" \ + "aw-worktime-sessions|Worktime sessions|Проверьте работу worktime-api: systemctl status aw-worktime-api на AW сервере" \ + "aw-session-events|Session events|Проверьте collector-guard и aw-session-events-collector на RDP" \ + "aw-dlp-endpoint-signals|DLP signals|Запустите: ansible aw_windows -i $INVENTORY -m win_shell -a 'schtasks /Run /TN \"ActivityWatch Launch [SHARKON2025_Администратор]\"; Start-Sleep 30'" \ + "aw-dlp-incidents|DLP incidents|Проверьте aw-detmir-dlp-collector.ps1 на RDP (логи: C:\\ProgramData\\AWatch-rus\\logs\\)" \ + ; do + bucket="${entry%%|*}"; rest="${entry#*|}" + label="${rest%%|*}"; remediation="${rest#*|}" + check_bucket_freshness "$bucket" "$label" "$remediation" + done + echo "" + echo "=== Быстрая диагностика завершена ===" + printf "OK=%s WARN=%s FAIL=%s SKIP=%s\n" "$OK_COUNT" "$WARN_COUNT" "$FAIL_COUNT" "$SKIP_COUNT" + [ "$FAIL_COUNT" -gt 0 ] && exit 2 || exit 0 +fi + +# ============================================================ +section "3. AW Server (10.10.10.13)" +# ============================================================ +check_http_json_key "AW Server info" "$AW_SERVER/api/0/info" \ + '.version' \ + "Проверьте: ssh igor@$AW_HOST 'systemctl status activitywatch-server'" +check_http_code "AW Server CORS" "$AW_SERVER/api/0/settings/" '^200$' +check_http_code "AW WebUI" "$AW_SERVER/" '^200$' +check_http_json_key "Worktime API health" "$AW_WORKTIME_API/health" \ + '.status // .ok' \ + "Проверьте: ssh igor@$AW_HOST 'systemctl status aw-worktime-api && journalctl -u aw-worktime-api -n 20'" + +# ============================================================ +section "4. Buckets (свежесть данных)" +# ============================================================ +for entry in \ + "aw-watcher-afk|AFK watcher|Запустите на RDP: ansible aw_windows -i $INVENTORY -m win_shell -a 'schtasks /Run /TN \"ActivityWatch Recovery\"'" \ + "aw-watcher-window|Window watcher|Запустите: ansible aw_windows -i $INVENTORY -m win_shell -a 'schtasks /Run /TN \"ActivityWatch Launch [SHARKON2025_Администратор]\"; Start-Process -FilePath \"C:\\Program Files\\AWatch-rus\\bin\\aw-watcher-window\\aw-watcher-window.exe\" -ArgumentList @(\"--host\", \"10.10.10.13\", \"--port\", \"5600\") -WindowStyle Hidden'" \ + "aw-worktime-sessions|Worktime sessions|Проверьте: ssh igor@$AW_HOST 'systemctl status aw-worktime-api && journalctl -u aw-worktime-api -n 20'" \ + "aw-session-events|Session events|Проверьте collector-guard на RDP: ansible aw_windows -i $INVENTORY -m win_shell -a 'Get-Process -Name aw-session-events-* -ErrorAction SilentlyContinue'" \ + "aw-dlp-endpoint-signals|DLP endpoint signals|Запустите: ansible aw_windows -i $INVENTORY -m win_shell -a 'schtasks /Run /TN \"ActivityWatch Launch [SHARKON2025_Администратор]\"; Start-Sleep 60'" \ + "aw-dlp-incidents|DLP incidents|Проверьте: ansible aw_windows -i $INVENTORY -m win_shell -a \"Get-Content 'C:\\ProgramData\\AWatch-rus\\logs\\dlp-*.log' -Tail 20\"" \ + "aw-dlp-review|DLP review|Проверьте: ssh igor@$AW_HOST 'journalctl -u aw-dlp-policy-engine.service -n 20 --no-pager'" \ + "aw-dlp-rules|DLP rules|Проверьте: ssh igor@$AW_HOST 'journalctl -u aw-dlp-ioc-refresh.service -n 20 --no-pager'" \ +; do + bucket="${entry%%|*}"; rest="${entry#*|}" + label="${rest%%|*}"; remediation="${rest#*|}" + check_bucket_freshness "$bucket" "$label" "$remediation" +done + +# ============================================================ +section "5. InfluxDB (10.10.10.10:8086)" +# ============================================================ +check_http_json_key "InfluxDB health" "$INFLUXDB_URL/health" \ + '.status == "pass"' \ + "Проверьте InfluxDB на LXC 200: ssh igor@$PROXMOX_HOST 'sudo pct exec 200 -- systemctl status influxdb'" + +# ============================================================ +section "6. Grafana (10.10.10.11:3000)" +# ============================================================ +check_http_json_key "Grafana health" "$GRAFANA_URL/api/health" \ + '.database == "ok"' \ + "Проверьте: ssh igor@$PROXMOX_HOST 'sudo pct exec 201 -- systemctl status grafana-server'" +check_http_code "Grafana datasources API" "$GRAFANA_URL/api/datasources" '^200$|^302$|^401$' + +# ============================================================ +section "7. ClickHouse (10.10.10.2:8123)" +# ============================================================ +# Проверяем через прямой HTTP — AUTHENTICATION_FAILED = сервер жив +local_ch_ok=0 +ch_code=$(curl -sS --max-time 5 "http://$CLICKHOUSE_HOST:8123/?query=SELECT%201" 2>/dev/null | head -1) +if echo "$ch_code" | grep -q "AUTHENTICATION_FAILED"; then + pass "ClickHouse HTTP — отвечает (требуется аутентификация, это нормально)" + local_ch_ok=1 +elif echo "$ch_code" | grep -q "1"; then + pass "ClickHouse HTTP — SELECT 1 OK" + local_ch_ok=1 +else + fail "ClickHouse HTTP — не отвечает: $ch_code" + echo " REMEDIATION: ssh igor@$PROXMOX_HOST 'cd /opt/activitywatch/clickhouse-1c && sudo docker compose ps; sudo docker compose logs --tail=20'" +fi + +# Проверка Docker контейнера через SSH +container_status=$(ssh_pve 'sudo docker ps --filter name=aw-rus-1c-clickhouse --format "{{.Status}}" 2>/dev/null' 2>/dev/null) +if [ -n "$container_status" ]; then + pass "ClickHouse Docker контейнер: $container_status" +else + fail "ClickHouse Docker контейнер не запущен" + echo " REMEDIATION: ssh igor@$PROXMOX_HOST 'cd /opt/activitywatch/clickhouse-1c && sudo docker compose up -d'" +fi + +# ClickHouse health timer +check_service_remote "ClickHouse health timer" "$PROXMOX_HOST" "aw-1c-clickhouse-health.timer" \ + "Проверьте: ssh igor@$PROXMOX_HOST 'sudo journalctl -u aw-1c-clickhouse-health.service -n 30 --no-pager'" + +# ClickHouse network health timer (с AW сервера) +check_service_remote "ClickHouse network health timer" "$AW_HOST" "aw-clickhouse-network-health.timer" \ + "Проверьте: ssh igor@$AW_HOST 'sudo journalctl -u aw-clickhouse-network-health.service -n 30 --no-pager'" + +# 1C-ingest timer +check_service_remote "1C ingest timer" "$PROXMOX_HOST" "aw-1c-ingest.timer" \ + "Проверьте: ssh igor@$PROXMOX_HOST 'sudo systemctl status aw-1c-ingest.timer; sudo journalctl -u aw-1c-ingest.service -n 20'" + +# ============================================================ +section "8. 1C Manager API (10.10.10.2:8710)" +# ============================================================ +check_http_json_key "1C /api/health" "http://$PROXMOX_HOST:8710/api/health" \ + '.status == "ok"' \ + "Проверьте Python процесс: ssh igor@$PROXMOX_HOST 'ps aux | grep 8710 | grep -v grep'" +check_http_code "1C /manager/brief" "http://$PROXMOX_HOST:8710/manager/brief" '^200$' + +# ============================================================ +section "9. Nginx Gateway (10.10.10.2)" +# ============================================================ +check_http_code "Gateway /healthz" "https://$PROXMOX_HOST/healthz" '^200$' +check_http_code "Gateway /go/proxmox-gui (401=protected, OK)" "https://$PROXMOX_HOST/go/proxmox-gui" '^30[1278]$|^401$' +check_http_code "Gateway /go/file1c-brief (401=protected, OK)" "https://$PROXMOX_HOST/go/file1c-brief" '^30[1278]$|^401$' + +check_service_remote "Nginx service" "$PROXMOX_HOST" "nginx.service" \ + "Проверьте: ssh igor@$PROXMOX_HOST 'sudo systemctl status nginx; sudo nginx -t'" + +# ============================================================ +section "10. DLP Pipeline (10.10.10.13)" +# ============================================================ +# DLP Policy Engine — должен быть active (running) +check_service_remote "DLP Policy Engine" "$AW_HOST" "aw-dlp-policy-engine.service" \ + "Проверьте: ssh igor@$AW_HOST 'sudo journalctl -u aw-dlp-policy-engine.service -n 30 --no-pager'" + +# DLP Case Management +check_service_remote "DLP Case Management" "$AW_HOST" "aw-dlp-case-management.service" \ + "Проверьте: ssh igor@$AW_HOST 'sudo journalctl -u aw-dlp-case-management.service -n 30 --no-pager'" + +# DLP Aggregator +aggr_status=$(ssh_aw 'systemctl is-active activitywatch-dlp-aggregator.timer 2>/dev/null || echo not_found' 2>/dev/null) +if [ "$aggr_status" = "active" ]; then + pass "DLP Aggregator timer (active)" +else + fail "DLP Aggregator timer ($aggr_status)" + echo " REMEDIATION: ssh igor@$AW_HOST 'sudo systemctl enable --now activitywatch-dlp-aggregator.timer; sudo journalctl -u activitywatch-dlp-aggregator.service -n 30'" +fi + +# DLP Influx Exporter +influx_exp_status=$(ssh_aw 'systemctl is-active aw-dlp-influx-exporter.timer 2>/dev/null || echo not_found' 2>/dev/null) +if [ "$influx_exp_status" = "active" ]; then + pass "DLP Influx Exporter timer (active)" +else + fail "DLP Influx Exporter timer ($influx_exp_status)" + echo " REMEDIATION: ssh igor@$AW_HOST 'sudo systemctl enable --now aw-dlp-influx-exporter.timer; sudo journalctl -u aw-dlp-influx-exporter.service -n 30'" +fi + +# DLP CEF Exporter +check_service_remote "DLP CEF Exporter timer" "$AW_HOST" "aw-dlp-cef-exporter.timer" \ + "ssh igor@$AW_HOST 'sudo systemctl enable --now aw-dlp-cef-exporter.timer; journalctl -u aw-dlp-cef-exporter.service -n 20'" + +# DLP IOC Refresh +check_service_remote "DLP IOC Refresh timer" "$AW_HOST" "aw-dlp-ioc-refresh.timer" \ + "ssh igor@$AW_HOST 'sudo systemctl enable --now aw-dlp-ioc-refresh.timer'" + +# DLP Syslog Forwarder +check_service_remote "DLP Syslog Forwarder timer" "$AW_HOST" "aw-dlp-syslog-forwarder.timer" \ + "ssh igor@$AW_HOST 'sudo systemctl enable --now aw-dlp-syslog-forwarder.timer'" + +# DLP Webhook Sender +check_service_remote "DLP Webhook Sender timer" "$AW_HOST" "aw-dlp-webhook-sender.timer" \ + "ssh igor@$AW_HOST 'sudo systemctl enable --now aw-dlp-webhook-sender.timer'" + +# DLP Report Scheduler +check_service_remote "DLP Report Scheduler timer" "$AW_HOST" "aw-dlp-report-scheduler.timer" \ + "ssh igor@$AW_HOST 'sudo systemctl enable --now aw-dlp-report-scheduler.timer'" + +# Worktime Influx Exporter +check_service_remote "Worktime Influx Exporter timer" "$AW_HOST" "aw-worktime-influx-exporter.timer" \ + "ssh igor@$AW_HOST 'sudo systemctl enable --now aw-worktime-influx-exporter.timer; journalctl -u aw-worktime-influx-exporter.service -n 20'" + +# ============================================================ +if [ "$SKIP_WINDOWS" = "1" ]; then + skip "Проверки RDP хоста пропущены (--skip-windows)" +else + section "11. RDP хост (192.168.100.18)" + if have ansible && [ -f "$INVENTORY" ]; then + check_ansible_module "WinRM ping" aw_windows win_ping + + check_ansible_win_shell "Сессии RDP" 'query user 2>&1' + + check_ansible_win_shell "Процессы watcher" \ + 'Get-Process aw-watcher-afk,aw-watcher-window -ErrorAction SilentlyContinue | Select-Object Name,Id,SessionId,StartTime | Format-Table -AutoSize' + + check_ansible_win_shell "Количество процессов powershell" \ + '(Get-Process powershell -ErrorAction SilentlyContinue | Measure-Object).Count' + + check_ansible_win_shell "Scheduled tasks (Recovery)" \ + 'schtasks /Query /TN "ActivityWatch Recovery" /FO LIST /V | Select-String "Status|Run|Next"' + + check_ansible_win_shell "Scheduled tasks (Launch Admin)" \ + 'schtasks /Query /TN "ActivityWatch Launch [SHARKON2025_Администратор]" /FO LIST /V | Select-String "Status|Run|Next"' + else + skip "Ansible или inventory не найдены" + fi +fi + +# ============================================================ +section "12. Systemd health (AW server)" +# ============================================================ +check_ansible_shell "AW server core units" aw_server \ + 'systemctl is-active activitywatch-server aw-worktime-api aw-dlp-policy-engine aw-dlp-case-management aw-worktime-influx-exporter.timer aw-dlp-influx-exporter.timer activitywatch-dlp-aggregator.timer aw-clickhouse-network-health.timer | paste -sd,' + +check_ansible_shell "AW server — нет failed units" aw_server \ + 'failed=$(systemctl --failed --no-legend | awk "{print \$1}" | grep -E "activitywatch|aw-|dlp" || true); test -z "$failed" && echo "no AW-related failed units" || { echo "$failed"; exit 1; }' + +# ============================================================ +section "13. Systemd health (Proxmox)" +# ============================================================ +check_ansible_shell "Proxmox core units" proxmox \ + 'systemctl is-active nginx docker aw-1c-clickhouse-health.timer aw-1c-ingest.timer 2>/dev/null | paste -sd,' + +# ============================================================ +section "14. Диск и память" +# ============================================================ +check_ansible_shell "Диски AW server" aw_server 'df -h / /var /opt 2>/dev/null | tail -5' +check_ansible_shell "Память AW server" aw_server 'free -h | tail -5' + +# ============================================================ +section "Итог диагностики" +# ============================================================ +printf " OK=%s WARN=%s FAIL=%s SKIP=%s\n" "$OK_COUNT" "$WARN_COUNT" "$FAIL_COUNT" "$SKIP_COUNT" + +if [ "$FAIL_COUNT" -gt 0 ]; then + echo "" + echo " Есть проблемы! Смотрите REMEDIATION выше для каждого FAIL." + echo " После исправления запустите повторно: $0" + exit 2 +elif [ "$WARN_COUNT" -gt 0 ]; then + echo "" + echo " Есть предупреждения (WARN) — стоит проверить, но не критично." + exit 1 +else + echo "" + echo " Все проверки пройдены. Контур в рабочем состоянии." + exit 0 +fi diff --git a/scripts/quality-gate.sh b/scripts/quality-gate.sh index 6a434a6..2d621bb 100755 --- a/scripts/quality-gate.sh +++ b/scripts/quality-gate.sh @@ -53,7 +53,11 @@ if command -v pwsh >/dev/null 2>&1; then [void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path "windows/ActivityWatch.Windows.Common.psm1"),[ref]$null,[ref]$null) [void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path "windows/ActivityWatch.Windows.Common.psd1"),[ref]$null,[ref]$null) ' - pwsh -NoLogo -NoProfile -File windows/aw-collector-guard.ps1 -SelfTest >/dev/null + if [[ -f windows/aw-collector-guard.ps1 ]]; then + pwsh -NoLogo -NoProfile -File windows/aw-collector-guard.ps1 -SelfTest >/dev/null + else + echo "windows/aw-collector-guard.ps1 absent; Rust collector guard is the primary runtime." + fi else echo "pwsh not found, skipping." fi diff --git a/scripts/verify_innosetup_installer.sh b/scripts/verify_innosetup_installer.sh index 4d1a049..e73d7ac 100644 --- a/scripts/verify_innosetup_installer.sh +++ b/scripts/verify_innosetup_installer.sh @@ -48,7 +48,6 @@ wineserver -w >/dev/null 2>&1 required_files=( windows/AWatchRusCollectorGuardService.cs - windows/aw-collector-guard.ps1 windows/install-collector-guard-service.ps1 windows/aw-windows-telemetry.exe windows/dlp-policy.native-cross-os.example.json @@ -70,9 +69,4 @@ for rel in "${required_files[@]}"; do fi done -if ! grep -q 'collector guard self-test OK' "${INSTALL_DIR_UNIX}/windows/aw-collector-guard.ps1"; then - echo "Guard self-test marker missing in extracted installer payload" >&2 - exit 1 -fi - echo "verify_innosetup_installer: OK" diff --git a/windows/ActivityWatch.Windows.Common.psm1 b/windows/ActivityWatch.Windows.Common.psm1 index f449f4b..b57cd40 100755 --- a/windows/ActivityWatch.Windows.Common.psm1 +++ b/windows/ActivityWatch.Windows.Common.psm1 @@ -312,7 +312,7 @@ function Get-ActivityWatchBuiltInAdministratorName { catch { } - if ([string]$env:COMPUTERNAME -ieq 'HOST-EXAMPLE') { + if ([string]$env:COMPUTERNAME -ieq 'SHARKON2025') { $script:ActivityWatchBuiltInAdministratorName = 'Администратор' return $script:ActivityWatchBuiltInAdministratorName } @@ -803,12 +803,18 @@ function Copy-ActivityWatchCollectorAssets { $examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json' $policyTarget = Join-Path $StateRoot 'dlp-policy.json' - Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force - Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force + if ($CollectorScriptSource -and (Test-Path -LiteralPath $CollectorScriptSource)) { + Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force + } + if ($EndpointCollectorScriptSource -and (Test-Path -LiteralPath $EndpointCollectorScriptSource)) { + Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force + } if ($PolicyClientScriptSource -and (Test-Path -LiteralPath $PolicyClientScriptSource)) { Copy-Item -LiteralPath $PolicyClientScriptSource -Destination $policyClientTarget -Force } - Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force + if ($FileCollectorScriptSource -and (Test-Path -LiteralPath $FileCollectorScriptSource)) { + Copy-Item -LiteralPath $FileCollectorScriptSource -Destination $fileCollectorTarget -Force + } Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force if ($EvtxExportScriptSource -and (Test-Path -LiteralPath $EvtxExportScriptSource)) { Copy-Item -LiteralPath $EvtxExportScriptSource -Destination $evtxExportTarget -Force @@ -841,11 +847,15 @@ function Copy-ActivityWatchCollectorAssets { Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force } + $effectiveCollectorTarget = if (Test-Path -LiteralPath $collectorTarget) { $collectorTarget } else { '' } + $effectiveEndpointCollectorTarget = if (Test-Path -LiteralPath $endpointCollectorTarget) { $endpointCollectorTarget } else { '' } + $effectiveFileCollectorTarget = if (Test-Path -LiteralPath $fileCollectorTarget) { $fileCollectorTarget } else { '' } + return [pscustomobject]@{ - CollectorScript = $collectorTarget - EndpointCollectorScript = $endpointCollectorTarget + CollectorScript = $effectiveCollectorTarget + EndpointCollectorScript = $effectiveEndpointCollectorTarget PolicyClientScript = $policyClientTarget - FileCollectorScript = $fileCollectorTarget + FileCollectorScript = $effectiveFileCollectorTarget SessionCollectorScript = $sessionCollectorTarget EvtxExportScript = $evtxExportTarget HayabusaUploadScript = $hayabusaUploadTarget @@ -960,6 +970,7 @@ function New-ActivityWatchDeploymentConfig { if ($File1CAutoUploadEnabled -and [string]::IsNullOrWhiteSpace($File1CTargetHost)) { throw 'File1CTargetHost is required when File1CAutoUploadEnabled is true.' } + $toolkitRoot = Join-Path (Split-Path -Parent $InstallRoot) 'windows' return [pscustomobject]@{ version = 1 @@ -983,7 +994,7 @@ function New-ActivityWatchDeploymentConfig { evtxExportScript = $EvtxExportScript hayabusaUploadScript = $HayabusaUploadScript file1cTelemetryScript = $File1CTelemetryScript - file1cTelemetryExecutable = if ([string]::IsNullOrWhiteSpace($File1CTelemetryScript)) { '' } else { Join-Path (Split-Path -Parent $File1CTelemetryScript) 'aw-windows-telemetry.exe' } + file1cTelemetryExecutable = Join-Path $toolkitRoot 'aw-windows-telemetry.exe' rulesPath = $RulesPath policyPath = $PolicyPath launchScript = $LaunchScriptPath @@ -998,6 +1009,9 @@ function New-ActivityWatchDeploymentConfig { windowEnabled = $WindowEnabled fileOpsEnabled = $FileOpsEnabled emailEnabled = $false + browserCollectorMode = 'rust_primary' + dlpEndpointMode = 'rust_primary' + fileOpsMode = 'rust_primary' worktimeSessionEnabled = $true worktimeSessionMode = 'powershell_primary' worktimeLegacyFallbackEnabled = $true @@ -1446,19 +1460,20 @@ function Start-RustCollectorIfNeeded { ) if ([string]::IsNullOrWhiteSpace(`$ExePath) -or [string]::IsNullOrWhiteSpace(`$Subcommand)) { - return + return `$false } if (-not (Test-Path -LiteralPath `$ExePath)) { - return + return `$false } if (Test-RustCollectorRunning -Subcommand `$Subcommand -SessionId `$SessionId) { - return + return `$true } `$argumentList = @(`$Subcommand, '--config-path', `$ConfigPath, '--mode', 'enforce') Start-Process -FilePath `$ExePath -ArgumentList `$argumentList -WindowStyle Hidden + return `$true } `$config = Get-DeploymentConfig -Path `$ConfigPath @@ -1466,6 +1481,9 @@ function Start-RustCollectorIfNeeded { `$installRoot = [string]`$config.paths.installRoot `$stateRoot = [string]`$config.paths.stateRoot `$deployRoot = if (`$config.paths.PSObject.Properties.Name -contains 'deployRoot' -and -not [string]::IsNullOrWhiteSpace([string]`$config.paths.deployRoot)) { [string]`$config.paths.deployRoot } elseif (`$config.paths.PSObject.Properties.Name -contains 'toolkitRoot' -and -not [string]::IsNullOrWhiteSpace([string]`$config.paths.toolkitRoot)) { [string]`$config.paths.toolkitRoot } else { `$installRoot } +if ((Split-Path -Path `$deployRoot -Leaf) -ieq 'bin') { + `$deployRoot = Split-Path -Path `$deployRoot -Parent +} `$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port `$script:Hostname = if (`$config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]`$config.awHostname)) { [string]`$config.awHostname } else { `$env:COMPUTERNAME } `$script:KnownBuckets = @{} @@ -1481,9 +1499,9 @@ function Start-RustCollectorIfNeeded { `$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true } `$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true } `$fileOpsEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]`$config.collectors.fileOpsEnabled } else { `$true } -`$browserCollectorMode = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'browserCollectorMode') { [string]`$config.collectors.browserCollectorMode } else { 'powershell_primary' } -`$dlpEndpointMode = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'dlpEndpointMode') { [string]`$config.collectors.dlpEndpointMode } else { 'powershell_primary' } -`$fileOpsMode = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsMode') { [string]`$config.collectors.fileOpsMode } else { 'powershell_primary' } +`$browserCollectorMode = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'browserCollectorMode') { [string]`$config.collectors.browserCollectorMode } else { 'rust_primary' } +`$dlpEndpointMode = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'dlpEndpointMode') { [string]`$config.collectors.dlpEndpointMode } else { 'rust_primary' } +`$fileOpsMode = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'fileOpsMode') { [string]`$config.collectors.fileOpsMode } else { 'rust_primary' } `$emailEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'emailEnabled') { [bool]`$config.collectors.emailEnabled } else { `$false } `$emailCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'emailCollectorScript') { [string]`$config.paths.emailCollectorScript } else { Join-Path `$stateRoot 'email-outbound-collector.ps1' } `$launchLockPath = New-LaunchLock -StateRoot `$stateRoot -SessionId `$sessionId @@ -1514,18 +1532,24 @@ try { catch { } if (`$browserCollectorMode -ieq 'rust_primary') { - Start-RustCollectorIfNeeded -ExePath `$telemetryExe -Subcommand 'browser-domains-collector' -ConfigPath `$ConfigPath -SessionId `$sessionId + if (-not (Start-RustCollectorIfNeeded -ExePath `$telemetryExe -Subcommand 'browser-domains-collector' -ConfigPath `$ConfigPath -SessionId `$sessionId)) { + Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId + } } else { Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId } if (`$dlpEndpointMode -ieq 'rust_primary') { - Start-RustCollectorIfNeeded -ExePath `$telemetryExe -Subcommand 'dlp-endpoint-collector' -ConfigPath `$ConfigPath -SessionId `$sessionId + if (-not (Start-RustCollectorIfNeeded -ExePath `$telemetryExe -Subcommand 'dlp-endpoint-collector' -ConfigPath `$ConfigPath -SessionId `$sessionId)) { + Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId + } } else { Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId } if (`$fileOpsEnabled) { if (`$fileOpsMode -ieq 'rust_primary') { - Start-RustCollectorIfNeeded -ExePath `$telemetryExe -Subcommand 'file-operations-collector' -ConfigPath `$ConfigPath -SessionId `$sessionId + if (-not (Start-RustCollectorIfNeeded -ExePath `$telemetryExe -Subcommand 'file-operations-collector' -ConfigPath `$ConfigPath -SessionId `$sessionId)) { + Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId + } } else { Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId } diff --git a/windows/aw-standalone-service.ps1 b/windows/aw-standalone-service.ps1 index 613af91..dc034e7 100644 --- a/windows/aw-standalone-service.ps1 +++ b/windows/aw-standalone-service.ps1 @@ -55,6 +55,41 @@ function Start-CollectorIfNeeded { Write-ServiceLog ("started collector: {0}" -f $ScriptPath) } +function Test-RustCollectorRunning { + param([string]$Subcommand) + if ([string]::IsNullOrWhiteSpace($Subcommand)) { return $false } + return [bool]( + Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | + Where-Object { + $_.Name -ieq 'aw-windows-telemetry.exe' -and + $_.CommandLine -and + $_.CommandLine -match [Regex]::Escape($Subcommand) + } | + Select-Object -First 1 + ) +} + +function Start-RustCollectorIfNeeded { + param( + [string]$ExePath, + [string]$Subcommand, + [string]$ConfigPath + ) + + if ([string]::IsNullOrWhiteSpace($ExePath) -or [string]::IsNullOrWhiteSpace($Subcommand)) { + return $false + } + if (-not (Test-Path -LiteralPath $ExePath)) { + return $false + } + if (Test-RustCollectorRunning -Subcommand $Subcommand) { + return $true + } + Start-Process -FilePath $ExePath -ArgumentList @($Subcommand, '--config-path', $ConfigPath, '--mode', 'enforce') -WindowStyle Hidden | Out-Null + Write-ServiceLog ("started rust collector: {0}" -f $Subcommand) + return $true +} + $cfg = Get-Config -Path $ConfigPath $stateRoot = if ($cfg.paths -and $cfg.paths.stateRoot) { [string]$cfg.paths.stateRoot } else { 'C:\ProgramData\AWatch-rus' } $logsRoot = Join-Path $stateRoot 'logs' @@ -70,6 +105,7 @@ while ($true) { $cfg = Get-Config -Path $ConfigPath $paths = $cfg.paths $collectors = $cfg.collectors + $telemetryExe = if ($paths.PSObject.Properties.Name -contains 'file1cTelemetryExecutable' -and -not [string]::IsNullOrWhiteSpace([string]$paths.file1cTelemetryExecutable)) { [string]$paths.file1cTelemetryExecutable } else { Join-Path $PSScriptRoot 'aw-windows-telemetry.exe' } $isSession0 = ([System.Diagnostics.Process]::GetCurrentProcess().SessionId -eq 0) # In Session 0 (SYSTEM) collectors that depend on interactive desktop/user profile @@ -78,10 +114,14 @@ while ($true) { $startFileOps = $true $startEmail = $true $startWorktime = $true + $dlpEndpointMode = 'rust_primary' + $fileOpsMode = 'rust_primary' if ($collectors) { if ($collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { $startFileOps = [bool]$collectors.fileOpsEnabled } if ($collectors.PSObject.Properties.Name -contains 'emailEnabled') { $startEmail = [bool]$collectors.emailEnabled } if ($collectors.PSObject.Properties.Name -contains 'worktimeSessionEnabled') { $startWorktime = [bool]$collectors.worktimeSessionEnabled } + $dlpEndpointMode = if ($collectors.PSObject.Properties.Name -contains 'dlpEndpointMode') { [string]$collectors.dlpEndpointMode } else { 'rust_primary' } + $fileOpsMode = if ($collectors.PSObject.Properties.Name -contains 'fileOpsMode') { [string]$collectors.fileOpsMode } else { 'rust_primary' } $worktimeSessionMode = if ($collectors.PSObject.Properties.Name -contains 'worktimeSessionMode') { [string]$collectors.worktimeSessionMode } else { 'powershell_primary' } $worktimeLegacyFallbackEnabled = if ($collectors.PSObject.Properties.Name -contains 'worktimeLegacyFallbackEnabled') { [bool]$collectors.worktimeLegacyFallbackEnabled } else { $true } if ($worktimeSessionMode -ieq 'rust_primary') { @@ -98,9 +138,23 @@ while ($true) { if ($startBrowser) { Start-CollectorIfNeeded -ScriptPath ([string]$paths.collectorScript) -ConfigPath $ConfigPath } - Start-CollectorIfNeeded -ScriptPath ([string]$paths.endpointCollectorScript) -ConfigPath $ConfigPath + if ($dlpEndpointMode -ieq 'rust_primary') { + if (-not (Start-RustCollectorIfNeeded -ExePath $telemetryExe -Subcommand 'dlp-endpoint-collector' -ConfigPath $ConfigPath)) { + Start-CollectorIfNeeded -ScriptPath ([string]$paths.endpointCollectorScript) -ConfigPath $ConfigPath + } + } + else { + Start-CollectorIfNeeded -ScriptPath ([string]$paths.endpointCollectorScript) -ConfigPath $ConfigPath + } if ($startFileOps) { - Start-CollectorIfNeeded -ScriptPath ([string]$paths.fileCollectorScript) -ConfigPath $ConfigPath + if ($fileOpsMode -ieq 'rust_primary') { + if (-not (Start-RustCollectorIfNeeded -ExePath $telemetryExe -Subcommand 'file-operations-collector' -ConfigPath $ConfigPath)) { + Start-CollectorIfNeeded -ScriptPath ([string]$paths.fileCollectorScript) -ConfigPath $ConfigPath + } + } + else { + Start-CollectorIfNeeded -ScriptPath ([string]$paths.fileCollectorScript) -ConfigPath $ConfigPath + } } if ($paths.PSObject.Properties.Name -contains 'emailCollectorScript') { if ($startEmail) { diff --git a/windows/hardening-recovery.ps1 b/windows/hardening-recovery.ps1 index b810d08..a0385e6 100755 --- a/windows/hardening-recovery.ps1 +++ b/windows/hardening-recovery.ps1 @@ -76,6 +76,7 @@ $effectiveFile1CTelemetryScript = if ($existingConfig -and $existingConfig.paths $effectiveRules = Join-Path $effectiveStateRoot 'web-category-rules.json' $effectivePolicy = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$existingConfig.paths.policyPath } else { Join-Path $effectiveStateRoot 'dlp-policy.json' } $effectivePolicyClientScript = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'policyClientScript') { [string]$existingConfig.paths.policyClientScript } else { Join-Path $effectiveStateRoot 'dlp-policy-client.ps1' } +$effectiveTelemetryExecutable = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'file1cTelemetryExecutable' -and -not [string]::IsNullOrWhiteSpace([string]$existingConfig.paths.file1cTelemetryExecutable)) { [string]$existingConfig.paths.file1cTelemetryExecutable } else { Join-Path $PSScriptRoot 'aw-windows-telemetry.exe' } $effectiveServerHost = if ($ServerHost) { $ServerHost } elseif ($existingConfig) { [string]$existingConfig.server.host } else { $null } $effectiveServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($existingConfig) { [int]$existingConfig.server.port } else { 5600 } @@ -116,6 +117,9 @@ $effectiveFile1CAutoUploadTaskName = if ($existingConfig -and $existingConfig.PS $effectiveFile1CAutoUploadRunAsUser = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'runAsUser') { [string]$existingConfig.analytics.file1cAutomation.runAsUser } else { '' } $effectiveFile1CTargetHost = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'targetHost') { [string]$existingConfig.analytics.file1cAutomation.targetHost } else { '' } $effectiveFile1CTargetUser = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'analytics' -and $existingConfig.analytics.PSObject.Properties.Name -contains 'file1cAutomation' -and $existingConfig.analytics.file1cAutomation.PSObject.Properties.Name -contains 'targetUser') { [string]$existingConfig.analytics.file1cAutomation.targetUser } else { 'igor' } +$effectiveBrowserCollectorMode = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'collectors' -and $existingConfig.collectors.PSObject.Properties.Name -contains 'browserCollectorMode') { [string]$existingConfig.collectors.browserCollectorMode } else { 'rust_primary' } +$effectiveDlpEndpointMode = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'collectors' -and $existingConfig.collectors.PSObject.Properties.Name -contains 'dlpEndpointMode') { [string]$existingConfig.collectors.dlpEndpointMode } else { 'rust_primary' } +$effectiveFileOpsMode = if ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'collectors' -and $existingConfig.collectors.PSObject.Properties.Name -contains 'fileOpsMode') { [string]$existingConfig.collectors.fileOpsMode } else { 'rust_primary' } if ([string]::IsNullOrWhiteSpace($effectiveFile1CTargetHost)) { $file1cLogPath = Join-Path $effectiveLogsRoot 'file1c-telemetry.log' @@ -232,6 +236,11 @@ $config = New-ActivityWatchDeploymentConfig ` -UserTasks $taskDefinitions ` -PackageVersion $effectiveVersion +$config.paths.file1cTelemetryExecutable = $effectiveTelemetryExecutable +$config.collectors.browserCollectorMode = $effectiveBrowserCollectorMode +$config.collectors.dlpEndpointMode = $effectiveDlpEndpointMode +$config.collectors.fileOpsMode = $effectiveFileOpsMode + Write-ActivityWatchDeploymentConfig -Config $config -Path $effectiveConfigPath Remove-LegacyActivityWatchEntries Set-ActivityWatchAcl -InstallRoot $effectiveInstallRoot -StateRoot $effectiveStateRoot -LogsRoot $effectiveLogsRoot diff --git a/windows/install-collector-guard-service.ps1 b/windows/install-collector-guard-service.ps1 index b6eb8e7..1c6dcb7 100644 --- a/windows/install-collector-guard-service.ps1 +++ b/windows/install-collector-guard-service.ps1 @@ -25,8 +25,8 @@ $guardScriptPath = Join-Path $PSScriptRoot 'aw-collector-guard.ps1' $rustTelemetryPath = Join-Path $PSScriptRoot 'aw-windows-telemetry.exe' $serviceSourcePath = Join-Path $PSScriptRoot 'AWatchRusCollectorGuardService.cs' $serviceExePath = Join-Path $PSScriptRoot 'AWatchRusCollectorGuardService.exe' -if (-not (Test-Path -LiteralPath $guardScriptPath)) { - throw "Collector guard script not found: $guardScriptPath" +if (-not (Test-Path -LiteralPath $rustTelemetryPath) -and -not (Test-Path -LiteralPath $guardScriptPath)) { + throw "Neither Rust collector guard nor PowerShell fallback was found: $rustTelemetryPath ; $guardScriptPath" } if (-not (Test-Path -LiteralPath $serviceSourcePath)) { throw "Collector guard service source not found: $serviceSourcePath" @@ -70,6 +70,9 @@ if (Test-Path -LiteralPath $rustTelemetryPath) { $binPath = "`"$serviceExePath`" --service-name `"$ServiceName`" --exec `"$rustTelemetryPath`" --args `"$rustArgs`" --log `"$serviceLogPath`"" } else { + if (-not (Test-Path -LiteralPath $guardScriptPath)) { + throw "Collector guard script not found: $guardScriptPath" + } $binPath = "`"$serviceExePath`" --service-name `"$ServiceName`" --script `"$guardScriptPath`" --config `"$ConfigPath`" --mode $Mode --loop $LoopSeconds --log `"$serviceLogPath`"" } diff --git a/windows/install-standalone-service.ps1 b/windows/install-standalone-service.ps1 index 3ec6c50..073c83d 100644 --- a/windows/install-standalone-service.ps1 +++ b/windows/install-standalone-service.ps1 @@ -29,6 +29,20 @@ function Ensure-Dir { } } +function Copy-IfExists { + param( + [Parameter(Mandatory = $true)] + [string]$Source, + [Parameter(Mandatory = $true)] + [string]$Destination + ) + if (Test-Path -LiteralPath $Source) { + Copy-Item -LiteralPath $Source -Destination $Destination -Force + return $Destination + } + return '' +} + Assert-Admin $logsRoot = Join-Path $StateRoot 'logs' @@ -40,14 +54,15 @@ $endpointCollectorScript = Join-Path $StateRoot 'dlp-endpoint-signals-collector. $fileCollectorScript = Join-Path $StateRoot 'file-operations-collector.ps1' $emailCollectorScript = Join-Path $StateRoot 'email-outbound-collector.ps1' $sessionCollectorScript = Join-Path $StateRoot 'worktime-session-collector.ps1' +$telemetryExecutable = Join-Path $PSScriptRoot 'aw-windows-telemetry.exe' $rulesPath = Join-Path $StateRoot 'web-category-rules.json' $policyPath = Join-Path $StateRoot 'dlp-policy.json' $configPath = Join-Path $StateRoot 'deployment-config.json' $serviceScriptPath = Join-Path $PSScriptRoot 'aw-standalone-service.ps1' -Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') -Destination $collectorScript -Force -Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1') -Destination $endpointCollectorScript -Force -Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'file-operations-collector.ps1') -Destination $fileCollectorScript -Force +$collectorScript = Copy-IfExists -Source (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') -Destination $collectorScript +$endpointCollectorScript = Copy-IfExists -Source (Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1') -Destination $endpointCollectorScript +$fileCollectorScript = Copy-IfExists -Source (Join-Path $PSScriptRoot 'file-operations-collector.ps1') -Destination $fileCollectorScript if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'email-outbound-collector.ps1')) { Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'email-outbound-collector.ps1') -Destination $emailCollectorScript -Force } @@ -82,6 +97,7 @@ $config = [pscustomobject]@{ fileCollectorScript = $fileCollectorScript emailCollectorScript = $emailCollectorScript sessionCollectorScript = $sessionCollectorScript + file1cTelemetryExecutable = $telemetryExecutable rulesPath = $rulesPath policyPath = $policyPath } @@ -94,6 +110,9 @@ $config = [pscustomobject]@{ windowEnabled = $false fileOpsEnabled = $true emailEnabled = $true + browserCollectorMode = 'rust_primary' + dlpEndpointMode = 'rust_primary' + fileOpsMode = 'rust_primary' worktimeSessionEnabled = $true worktimeSessionMode = 'powershell_primary' worktimeLegacyFallbackEnabled = $true diff --git a/windows/migrate-awatch-rus-paths.ps1 b/windows/migrate-awatch-rus-paths.ps1 index c588050..afc9225 100644 --- a/windows/migrate-awatch-rus-paths.ps1 +++ b/windows/migrate-awatch-rus-paths.ps1 @@ -97,6 +97,15 @@ function Update-AWatchConfigPaths { $Config.paths.logsRoot = $logsRoot $Config.paths.collectorScript = Join-Path $NewStateRoot 'browser-domains-native-collector.ps1' $Config.paths.endpointCollectorScript = Join-Path $NewStateRoot 'dlp-endpoint-signals-collector.ps1' + if ($Config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { + $Config.paths.fileCollectorScript = Join-Path $NewStateRoot 'file-operations-collector.ps1' + } + if ($Config.paths.PSObject.Properties.Name -contains 'file1cTelemetryExecutable') { + $Config.paths.file1cTelemetryExecutable = Join-Path $ToolkitRoot 'aw-windows-telemetry.exe' + } + else { + $Config.paths | Add-Member -NotePropertyName 'file1cTelemetryExecutable' -NotePropertyValue (Join-Path $ToolkitRoot 'aw-windows-telemetry.exe') + } if ($Config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { $Config.paths.sessionCollectorScript = Join-Path $NewStateRoot 'worktime-session-collector.ps1' } @@ -104,6 +113,20 @@ function Update-AWatchConfigPaths { if ($Config.paths.PSObject.Properties.Name -contains 'policyPath') { $Config.paths.policyPath = Join-Path $NewStateRoot 'dlp-policy.json' } + if ($Config.PSObject.Properties.Name -contains 'collectors') { + foreach ($entry in @( + @{ Name = 'browserCollectorMode'; Value = 'rust_primary' }, + @{ Name = 'dlpEndpointMode'; Value = 'rust_primary' }, + @{ Name = 'fileOpsMode'; Value = 'rust_primary' } + )) { + if ($Config.collectors.PSObject.Properties.Name -contains $entry.Name) { + $Config.collectors.PSObject.Properties[$entry.Name].Value = $entry.Value + } + else { + $Config.collectors | Add-Member -NotePropertyName $entry.Name -NotePropertyValue $entry.Value + } + } + } $Config.paths.launchScript = Join-Path $NewStateRoot 'launch-watchers.ps1' $Config.paths.recoveryScript = Join-Path $NewStateRoot 'recovery-loop.ps1' @@ -194,7 +217,9 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Миграция ActivityWatch W foreach ($file in @( 'browser-domains-native-collector.ps1', 'dlp-endpoint-signals-collector.ps1', + 'file-operations-collector.ps1', 'worktime-session-collector.ps1', + 'aw-windows-telemetry.exe', 'web-category-rules.example.json', 'dlp-policy.example.json' )) { diff --git a/windows/validate-deployment.ps1 b/windows/validate-deployment.ps1 index bdfcb61..9ee27de 100644 --- a/windows/validate-deployment.ps1 +++ b/windows/validate-deployment.ps1 @@ -16,6 +16,7 @@ $collectorScript = [string]$config.paths.collectorScript $endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' } $fileCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]$config.paths.fileCollectorScript } else { Join-Path $stateRoot 'file-operations-collector.ps1' } $sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' } +$telemetryExecutable = if ($config.paths.PSObject.Properties.Name -contains 'file1cTelemetryExecutable' -and -not [string]::IsNullOrWhiteSpace([string]$config.paths.file1cTelemetryExecutable)) { [string]$config.paths.file1cTelemetryExecutable } else { Join-Path (Join-Path ([System.Environment]::GetFolderPath('ProgramFiles')) 'AWatch-rus\windows') 'aw-windows-telemetry.exe' } $evtxExportScript = if ($config.paths.PSObject.Properties.Name -contains 'evtxExportScript') { [string]$config.paths.evtxExportScript } else { Join-Path $stateRoot 'export-evtx-for-hayabusa.ps1' } $rulesPath = [string]$config.paths.rulesPath $policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' } @@ -36,6 +37,9 @@ $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 } +$browserCollectorMode = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'browserCollectorMode') { [string]$config.collectors.browserCollectorMode } else { 'rust_primary' } +$dlpEndpointMode = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'dlpEndpointMode') { [string]$config.collectors.dlpEndpointMode } else { 'rust_primary' } +$fileOpsMode = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'fileOpsMode') { [string]$config.collectors.fileOpsMode } else { 'rust_primary' } $sessionEventsConfig = if ($config.PSObject.Properties.Name -contains 'sessionEvents') { $config.sessionEvents } else { $null } $sessionLogonEnabled = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$sessionEventsConfig.logonEnabled } else { $false } $sessionProcessEventsEnabled = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'processEventsEnabled') { [bool]$sessionEventsConfig.processEventsEnabled } else { $false } @@ -438,8 +442,6 @@ function Get-TaskSnapshot { } $requiredFiles = @( - $collectorScript, - $endpointCollectorScript, $sessionCollectorScript, $evtxExportScript, $rulesPath, @@ -449,7 +451,16 @@ $requiredFiles = @( $recoveryScript, $ConfigPath ) -if ($fileOpsExpected) { +if ($browserCollectorMode -ieq 'rust_primary' -or $dlpEndpointMode -ieq 'rust_primary' -or ($fileOpsExpected -and $fileOpsMode -ieq 'rust_primary')) { + $requiredFiles += $telemetryExecutable +} +if ($browserCollectorMode -ine 'rust_primary') { + $requiredFiles += $collectorScript +} +if ($dlpEndpointMode -ine 'rust_primary') { + $requiredFiles += $endpointCollectorScript +} +if ($fileOpsExpected -and $fileOpsMode -ine 'rust_primary') { $requiredFiles += $fileCollectorScript } if ($afkExpected) { @@ -477,6 +488,15 @@ $sessionCollectorProcesses = @(Get-CollectorProcesses -ScriptPath $sessionCollec $endpointCollectorProcesses = @(Get-CollectorProcesses -ScriptPath $endpointCollectorScript) $fileCollectorProcesses = if ($fileOpsExpected) { @(Get-CollectorProcesses -ScriptPath $fileCollectorScript) } else { @() } $browserCollectorProcesses = @(Get-CollectorProcesses -ScriptPath $collectorScript) +$rustCollectorProcesses = @( + Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | + Where-Object { + $_.Name -ieq 'aw-windows-telemetry.exe' -and + $_.CommandLine -and + ($_.CommandLine -match 'browser-domains-collector' -or $_.CommandLine -match 'dlp-endpoint-collector' -or $_.CommandLine -match 'file-operations-collector') + } | + Select-Object @{ Name = 'Name'; Expression = { $_.Name } }, @{ Name = 'Id'; Expression = { [int]$_.ProcessId } }, @{ Name = 'SessionId'; Expression = { [int]$_.SessionId } }, @{ Name = 'CommandLine'; Expression = { [string]$_.CommandLine } } +) $liveLoggedOnUsers = Get-LoggedOnUsers $interactiveUsers = Get-LoggedOnUsers -IncludeDisconnected $true @@ -611,6 +631,12 @@ $result = [ordered]@{ endpointCollectorDuplicates = @($endpointCollectorDuplicates) fileCollectors = @($fileCollectorProcesses) fileCollectorDuplicates = @($fileCollectorDuplicates) + rustCollectors = @($rustCollectorProcesses) + collectorModes = [ordered]@{ + browser = $browserCollectorMode + endpoint = $dlpEndpointMode + fileOps = $fileOpsMode + } ok = [bool]( $watcherCountsOk -and $sessionCollectorOk -and