fix(ops): sync verified production baseline for AW-Rus DLP
This commit is contained in:
+12
-1
@@ -171,13 +171,24 @@ Playbook:
|
||||
- `telegram_allowed_chat_ids`
|
||||
- `tsj_bot_source_local_path`
|
||||
3. Убедитесь, что в inventory есть группа `[proxmox]`.
|
||||
4. Запустите:
|
||||
Для текущего контура AW-Rus bot ожидает Proxmox host `10.10.10.2`.
|
||||
Рабочая модель для этого контура: `igor` + `sudo`, а не обязательный `root` login.
|
||||
4. При необходимости задайте recovery-команды для AW-Rus:
|
||||
- `tsj_bot_aw_rus_worktime_heal_cmd`
|
||||
- `tsj_bot_aw_rus_dlp_heal_cmd`
|
||||
5. Запустите:
|
||||
|
||||
```bash
|
||||
cd ansible
|
||||
ansible-playbook -i inventory.ini deploy_tsj_guardian_bot_proxmox.yml
|
||||
```
|
||||
|
||||
После актуального production hardening:
|
||||
|
||||
- bot различает `worktime idle` и реальную деградацию;
|
||||
- bot поддерживает отдельный `AW_RUS_DLP_HEAL_CMD`;
|
||||
- redeploy не должен терять runtime env-ключи, связанные с proxy, FS checks и AI escalation.
|
||||
|
||||
## Результат
|
||||
|
||||
- Установлен ActivityWatch Server.
|
||||
|
||||
@@ -378,6 +378,15 @@
|
||||
mode: "0644"
|
||||
when: aw_dlp_content_analysis_enabled | default(true) | bool
|
||||
|
||||
- name: Установить wrapper запуска DLP content analysis через virtualenv
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/dlp-content-analysis/aw-dlp-content-analyzer.sh"
|
||||
dest: /usr/local/bin/aw-dlp-content-analyzer
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
when: aw_dlp_content_analysis_enabled | default(true) | bool
|
||||
|
||||
- name: Создать virtualenv DLP content analysis
|
||||
ansible.builtin.command:
|
||||
cmd: python3 -m venv /opt/activitywatch/dlp-content-analysis/.venv
|
||||
|
||||
@@ -259,16 +259,58 @@
|
||||
try {
|
||||
Enable-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}" -ErrorAction SilentlyContinue | Out-Null
|
||||
} catch {}
|
||||
Get-ScheduledTask |
|
||||
Where-Object TaskName -like "{{ aw_windows_launch_task_pattern }}" |
|
||||
ForEach-Object {
|
||||
try { Enable-ScheduledTask -TaskName $_.TaskName -ErrorAction SilentlyContinue | Out-Null } catch {}
|
||||
|
||||
$config = Get-Content -Raw -LiteralPath "{{ aw_windows_state_root }}\deployment-config.json" | ConvertFrom-Json
|
||||
$loggedOnUsers = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
try {
|
||||
foreach ($line in @(& quser.exe 2>$null)) {
|
||||
$normalized = [string]$line
|
||||
if ([string]::IsNullOrWhiteSpace($normalized)) { continue }
|
||||
$normalized = $normalized.TrimStart(' ', '>')
|
||||
if ([string]::IsNullOrWhiteSpace($normalized)) { continue }
|
||||
if ($normalized -match '^(USERNAME|ПОЛЬЗОВАТЕЛЬ)\s+') { continue }
|
||||
$parts = $normalized -split '\s+'
|
||||
if ($parts.Count -lt 1) { continue }
|
||||
$user = [string]$parts[0]
|
||||
if ([string]::IsNullOrWhiteSpace($user)) { continue }
|
||||
[void]$loggedOnUsers.Add($user)
|
||||
[void]$loggedOnUsers.Add(('{0}\{1}' -f $env:COMPUTERNAME, $user))
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) {
|
||||
[void]$loggedOnUsers.Add(('{0}\{1}' -f $env:USERDOMAIN, $user))
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
function Test-TaskUserHasSession {
|
||||
param([string]$UserId)
|
||||
if ([string]::IsNullOrWhiteSpace($UserId)) { return $false }
|
||||
$candidates = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
[void]$candidates.Add($UserId)
|
||||
$leafUser = $UserId
|
||||
if ($leafUser -match '^[^\\]+\\(.+)$') {
|
||||
$leafUser = $Matches[1]
|
||||
[void]$candidates.Add($leafUser)
|
||||
}
|
||||
[void]$candidates.Add(('{0}\{1}' -f $env:COMPUTERNAME, $leafUser))
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) {
|
||||
[void]$candidates.Add(('{0}\{1}' -f $env:USERDOMAIN, $leafUser))
|
||||
}
|
||||
foreach ($candidate in @($candidates)) {
|
||||
if ($loggedOnUsers.Contains($candidate)) { return $true }
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
foreach ($taskDef in @($config.userTasks)) {
|
||||
try { Enable-ScheduledTask -TaskName ([string]$taskDef.launchTaskName) -ErrorAction SilentlyContinue | Out-Null } catch {}
|
||||
}
|
||||
|
||||
Start-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}"
|
||||
Get-ScheduledTask |
|
||||
Where-Object TaskName -like "{{ aw_windows_launch_task_pattern }}" |
|
||||
ForEach-Object { Start-ScheduledTask -TaskName $_.TaskName }
|
||||
foreach ($taskDef in @($config.userTasks)) {
|
||||
if (Test-TaskUserHasSession -UserId ([string]$taskDef.userId)) {
|
||||
Start-ScheduledTask -TaskName ([string]$taskDef.launchTaskName) -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
- name: Получить Windows hostname для AW smoke-check bucket
|
||||
when:
|
||||
|
||||
@@ -80,8 +80,20 @@
|
||||
TELEGRAM_BOT_TOKEN={{ telegram_bot_token }}
|
||||
TELEGRAM_ALLOWED_CHAT_IDS={{ telegram_allowed_chat_ids }}
|
||||
TELEGRAM_DEFAULT_CHAT_ID={{ tsj_bot_default_chat_id }}
|
||||
HTTPS_PROXY={{ tsj_bot_https_proxy_url | default(tsj_bot_telegram_proxy_url | default('http://127.0.0.1:11090')) }}
|
||||
HTTP_PROXY={{ tsj_bot_http_proxy_url | default(tsj_bot_telegram_proxy_url | default('http://127.0.0.1:11090')) }}
|
||||
NO_PROXY={{ tsj_bot_no_proxy | default('localhost,127.0.0.1,10.10.10.0/24') }}
|
||||
NODE_13_HOST={{ tsj_bot_node_13_host | default('10.10.10.13') }}
|
||||
NODE_16_HOST={{ tsj_bot_node_16_host | default('10.10.10.16') }}
|
||||
NODE_13_URL={{ tsj_bot_node_13_url | default('http://10.10.10.13:5600/') }}
|
||||
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') }}
|
||||
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') }}
|
||||
@@ -90,6 +102,8 @@
|
||||
RETRY_AUTORECOVERY_EVERY_SEC={{ tsj_bot_retry_autorecovery_every_sec | default(300) }}
|
||||
EXIT_ON_AUTORECOVERY_SUCCESS={{ tsj_bot_exit_on_autorecovery_success | default('true') }}
|
||||
ENABLE_AI_ESCALATION={{ tsj_bot_enable_ai_escalation | default('true') }}
|
||||
FS_IMMEDIATE_AI_ON_CRITICAL={{ tsj_bot_fs_immediate_ai_on_critical | default('true') }}
|
||||
AI_ESCALATION_MODE={{ tsj_bot_ai_escalation_mode | default('codex_exec') }}
|
||||
ENABLE_SERVER_FALLBACK={{ tsj_bot_enable_server_fallback | default('true') }}
|
||||
TELEGRAM_PROXY_URL={{ tsj_bot_telegram_proxy_url | default('http://127.0.0.1:11090') }}
|
||||
AI_CHAT_ENABLED={{ tsj_bot_ai_chat_enabled | default('true') }}
|
||||
@@ -103,9 +117,22 @@
|
||||
TMUX_SESSION={{ tsj_bot_tmux_session | default('ai') }}
|
||||
TMUX_CREATE_IF_MISSING={{ tsj_bot_tmux_create_if_missing | default('false') }}
|
||||
TMUX_START_COMMAND={{ tsj_bot_tmux_start_command | default('codex') }}
|
||||
PFSENSE_CHANGE_CONTROL_ENABLED={{ tsj_bot_pfsense_change_control_enabled | default('true') }}
|
||||
PFSENSE_CHANGE_CONFIRM_TTL_SEC={{ tsj_bot_pfsense_change_confirm_ttl_sec | default(900) }}
|
||||
OPENVPN_CONFIG_ENABLED={{ tsj_bot_openvpn_config_enabled | default('true') }}
|
||||
OPENVPN_CONFIG_CONFIRM_TTL_SEC={{ tsj_bot_openvpn_config_confirm_ttl_sec | default(900) }}
|
||||
OPENVPN_EXPIRY_WARN_ENABLED={{ tsj_bot_openvpn_expiry_warn_enabled | default('false') }}
|
||||
OPENVPN_EXPIRY_WARN_DAYS={{ tsj_bot_openvpn_expiry_warn_days | default(30) }}
|
||||
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') }}
|
||||
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') }}
|
||||
AW_RUS_WORKTIME_HEAL_CMD={{ tsj_bot_aw_rus_worktime_heal_cmd | default("sshpass -p '04091968' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13 'sudo -S /usr/local/bin/aw-worktime-autoheal.sh && sudo -S systemctl start aw-worktime-ui-bridge.service'") }}
|
||||
AW_RUS_DLP_HEAL_CMD={{ tsj_bot_aw_rus_dlp_heal_cmd | default("sshpass -p '04091968' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13 'sudo -S systemctl restart activitywatch-server.service && sudo -S systemctl start activitywatch-dlp-aggregator.service || true && sudo -S /usr/local/bin/aw-health-check && sudo -S /usr/local/bin/dlp-health-check'") }}
|
||||
AW_RUS_HOST={{ tsj_bot_aw_rus_host | default('SHARKON2025') }}
|
||||
AW_RUS_PRIMARY_USER={{ tsj_bot_aw_rus_primary_user | default('USER1') }}
|
||||
AW_RUS_STALE_SEC={{ tsj_bot_aw_rus_stale_sec | default(900) }}
|
||||
|
||||
@@ -10,11 +10,38 @@ tsj_bot_check_interval_sec: 60
|
||||
tsj_bot_operator_timeout_sec: 900
|
||||
tsj_bot_retry_autorecovery_every_sec: 300
|
||||
tsj_bot_telegram_proxy_url: "http://127.0.0.1:11090"
|
||||
tsj_bot_https_proxy_url: "http://127.0.0.1:11090"
|
||||
tsj_bot_http_proxy_url: "http://127.0.0.1:11090"
|
||||
tsj_bot_no_proxy: "localhost,127.0.0.1,10.10.10.0/24"
|
||||
tsj_bot_node_13_host: "10.10.10.13"
|
||||
tsj_bot_node_16_host: "10.10.10.16"
|
||||
tsj_bot_node_13_url: "http://10.10.10.13:5600/"
|
||||
tsj_bot_node_16_url: "http://10.10.10.16/"
|
||||
tsj_bot_node_16_enabled: "false"
|
||||
tsj_bot_fs_warn_pct: 85
|
||||
tsj_bot_fs_crit_pct: 92
|
||||
tsj_bot_fs_targets: "host,200,201,202,203"
|
||||
tsj_bot_fs_exclude_types: "tmpfs,devtmpfs,proc,sysfs,cgroup,cgroup2,overlay,squashfs,nsfs,tracefs,debugfs,securityfs,configfs,fusectl,mqueue,hugetlbfs,ramfs"
|
||||
tsj_bot_fs_immediate_ai_on_critical: "true"
|
||||
tsj_bot_ai_escalation_mode: "codex_exec"
|
||||
tsj_bot_pfsense_change_control_enabled: "true"
|
||||
tsj_bot_pfsense_change_confirm_ttl_sec: 900
|
||||
tsj_bot_openvpn_config_enabled: "true"
|
||||
tsj_bot_openvpn_config_confirm_ttl_sec: 900
|
||||
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_update_targets: "auto"
|
||||
tsj_bot_pfsense_mcp_bearer: "CHANGE_ME"
|
||||
|
||||
# AW-Rus + DLP check defaults
|
||||
tsj_bot_aw_rus_api_base: "http://10.10.10.13:5600/api/0"
|
||||
tsj_bot_aw_rus_worktime_base: "http://10.10.10.13:5610"
|
||||
tsj_bot_aw_rus_worktime_heal_cmd: "sshpass -p 'CHANGE_ME' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13 'sudo -S /usr/local/bin/aw-worktime-autoheal.sh && sudo -S systemctl start aw-worktime-ui-bridge.service'"
|
||||
tsj_bot_aw_rus_dlp_heal_cmd: "sshpass -p 'CHANGE_ME' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13 'sudo -S systemctl restart activitywatch-server.service && sudo -S systemctl start activitywatch-dlp-aggregator.service || true && sudo -S /usr/local/bin/aw-health-check && sudo -S /usr/local/bin/dlp-health-check'"
|
||||
tsj_bot_aw_rus_host: "SHARKON2025"
|
||||
tsj_bot_aw_rus_primary_user: "USER1"
|
||||
tsj_bot_aw_rus_stale_sec: 900
|
||||
|
||||
@@ -25,6 +25,14 @@
|
||||
group: "{{ aw_server_group | default('activitywatch') }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Install content analysis wrapper
|
||||
ansible.builtin.copy:
|
||||
src: "{{ playbook_dir }}/../aw-server/dlp-content-analysis/aw-dlp-content-analyzer.sh"
|
||||
dest: /usr/local/bin/aw-dlp-content-analyzer
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Create venv for content analysis
|
||||
ansible.builtin.command:
|
||||
cmd: python3 -m venv /opt/activitywatch/dlp-content-analysis/.venv
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Description=AW Worktime Report API
|
||||
After=network.target activitywatch-server.service
|
||||
Wants=activitywatch-server.service
|
||||
StartLimitBurst=3
|
||||
StartLimitIntervalSec=60
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
@@ -9,8 +11,6 @@ EnvironmentFile=/etc/activitywatch/aw-server.env
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/aw-worktime-api.py
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StartLimitBurst=3
|
||||
StartLimitIntervalSec=60
|
||||
User=activitywatch
|
||||
Group=activitywatch
|
||||
StandardOutput=journal
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Description=AW Worktime UI bridge (sessions -> afk/window)
|
||||
After=network-online.target activitywatch-server.service
|
||||
Wants=network-online.target
|
||||
StartLimitBurst=3
|
||||
StartLimitIntervalSec=120
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
@@ -10,8 +12,6 @@ Environment=AW_WORKTIME_HOST=SHARKON2025
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/aw-worktime-ui-bridge.py
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
StartLimitBurst=3
|
||||
StartLimitIntervalSec=120
|
||||
User=activitywatch
|
||||
Group=activitywatch
|
||||
StandardOutput=journal
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_DIR="/opt/activitywatch/dlp-content-analysis"
|
||||
VENV_PY="$BASE_DIR/.venv/bin/python"
|
||||
ANALYZER="$BASE_DIR/content_analyzer.py"
|
||||
|
||||
if [ ! -x "$VENV_PY" ]; then
|
||||
echo "ERROR: content-analysis virtualenv is missing: $VENV_PY" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec "$VENV_PY" "$ANALYZER" "$@"
|
||||
@@ -2,6 +2,8 @@
|
||||
Description=AW DLP Policy Engine
|
||||
After=network.target activitywatch-server.service
|
||||
Wants=activitywatch-server.service
|
||||
StartLimitBurst=3
|
||||
StartLimitIntervalSec=60
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
@@ -10,8 +12,6 @@ WorkingDirectory=/opt/activitywatch/dlp-policy-engine
|
||||
ExecStart=/opt/activitywatch/dlp-policy-engine/.venv/bin/uvicorn policy_service:app --host ${AW_DLP_POLICY_ENGINE_BIND_HOST} --port ${AW_DLP_POLICY_ENGINE_PORT}
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StartLimitBurst=3
|
||||
StartLimitIntervalSec=60
|
||||
User=activitywatch
|
||||
Group=activitywatch
|
||||
StandardOutput=journal
|
||||
|
||||
+12
-103
@@ -40,115 +40,24 @@ check_api_endpoint() {
|
||||
}
|
||||
|
||||
check_dlp_transport_freshness() {
|
||||
local api_base="${1:-http://127.0.0.1:5600/api/0}"
|
||||
local max_age_seconds="${2:-900}"
|
||||
local strict_fileops="${3:-0}"
|
||||
local dlp_health="${DLP_HEALTH_BIN:-/usr/local/bin/dlp-health-check}"
|
||||
local result
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "⚠ python3 is not available, skipping DLP transport freshness checks"
|
||||
WARNINGS+=("dlp-transport-check-skipped")
|
||||
if [[ ! -x "$dlp_health" ]]; then
|
||||
echo "⚠ dlp-health-check is not available, skipping DLP transport freshness checks"
|
||||
WARNINGS+=("dlp-health-check-missing")
|
||||
return
|
||||
fi
|
||||
|
||||
result="$(python3 - "$api_base" "$max_age_seconds" "$strict_fileops" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from urllib.request import urlopen
|
||||
|
||||
api_base = sys.argv[1].rstrip("/")
|
||||
max_age = int(sys.argv[2])
|
||||
strict_fileops = str(sys.argv[3]).strip().lower() in ("1", "true", "yes", "on")
|
||||
now = time.time()
|
||||
|
||||
def parse_ts(ts):
|
||||
if not ts:
|
||||
return None
|
||||
ts = ts.replace("Z", "+00:00")
|
||||
try:
|
||||
from datetime import datetime
|
||||
return datetime.fromisoformat(ts).timestamp()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_json(url):
|
||||
with urlopen(url, timeout=8) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
out = {
|
||||
"ok": True,
|
||||
"warnings": [],
|
||||
"errors": []
|
||||
}
|
||||
|
||||
try:
|
||||
buckets = get_json(f"{api_base}/buckets/")
|
||||
except Exception as ex:
|
||||
out["ok"] = False
|
||||
out["errors"].append(f"dlp-buckets-read-failed:{ex}")
|
||||
print(json.dumps(out))
|
||||
sys.exit(0)
|
||||
|
||||
endpoint = [k for k in buckets.keys() if k.startswith("aw-dlp-endpoint-signals_")]
|
||||
fileops = [k for k in buckets.keys() if k.startswith("aw-file-operations_")]
|
||||
|
||||
if not endpoint:
|
||||
out["ok"] = False
|
||||
out["errors"].append("no-endpoint-signal-buckets")
|
||||
if not fileops:
|
||||
out["warnings"].append("no-file-operations-buckets")
|
||||
|
||||
def check_bucket_freshness(bucket_id, label):
|
||||
b = buckets.get(bucket_id, {})
|
||||
meta = b.get("metadata") or {}
|
||||
end = parse_ts(meta.get("end"))
|
||||
if end is None:
|
||||
# Some aw-server deployments may not populate metadata.end; fallback to latest event.
|
||||
try:
|
||||
events = get_json(f"{api_base}/buckets/{bucket_id}/events?limit=1")
|
||||
if events:
|
||||
end = parse_ts(events[0].get("timestamp"))
|
||||
except Exception:
|
||||
end = None
|
||||
if end is None:
|
||||
out["warnings"].append(f"{label}:no-end-ts-or-events:{bucket_id}")
|
||||
result="$("$dlp_health" --json 2>/dev/null || true)"
|
||||
if [[ -z "$result" ]]; then
|
||||
echo "⚠ dlp-health-check did not return JSON, skipping DLP transport freshness checks"
|
||||
WARNINGS+=("dlp-health-check-empty")
|
||||
return
|
||||
age = int(now - end)
|
||||
if age > max_age:
|
||||
if label == "fileops" and not strict_fileops:
|
||||
out["warnings"].append(f"{label}:stale:{bucket_id}:age={age}s")
|
||||
else:
|
||||
out["ok"] = False
|
||||
out["errors"].append(f"{label}:stale:{bucket_id}:age={age}s")
|
||||
|
||||
for bid in endpoint:
|
||||
check_bucket_freshness(bid, "endpoint")
|
||||
for bid in fileops:
|
||||
check_bucket_freshness(bid, "fileops")
|
||||
|
||||
# Validate that endpoint self_test contains transport metrics at least once recently.
|
||||
for bid in endpoint:
|
||||
try:
|
||||
events = get_json(f"{api_base}/buckets/{bid}/events?limit=20")
|
||||
found = False
|
||||
for e in events:
|
||||
d = e.get("data") or {}
|
||||
if d.get("signalType") == "self_test":
|
||||
if all(k in d for k in ("queueDepth", "eventsEnqueued", "eventsFlushed", "sendFailures")):
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
out["warnings"].append(f"endpoint:self_test-metrics-missing:{bid}")
|
||||
except Exception as ex:
|
||||
out["warnings"].append(f"endpoint:self_test-read-failed:{bid}:{ex}")
|
||||
|
||||
print(json.dumps(out))
|
||||
PY
|
||||
)" || true
|
||||
fi
|
||||
|
||||
local ok
|
||||
ok="$(printf '%s' "$result" | python3 -c 'import json,sys; d=json.load(sys.stdin); print("1" if d.get("ok") else "0")' 2>/dev/null || echo "0")"
|
||||
ok="$(printf '%s' "$result" | python3 -c 'import json,sys; data=json.load(sys.stdin); names={r["name"]:r for r in data.get("results", [])}; checks=["buckets:endpoint-signals","buckets:file-operations","endpoint-self-test-metrics"]; bad=[n for n in checks if names.get(n,{}).get("status")=="fail"]; print("1" if not bad else "0")' 2>/dev/null || echo "0")"
|
||||
if [[ "$ok" == "1" ]]; then
|
||||
echo "✓ DLP transport freshness check passed"
|
||||
else
|
||||
@@ -157,8 +66,8 @@ PY
|
||||
fi
|
||||
|
||||
local errors warnings
|
||||
errors="$(printf '%s' "$result" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(", ".join(d.get("errors", [])))' 2>/dev/null || true)"
|
||||
warnings="$(printf '%s' "$result" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(", ".join(d.get("warnings", [])))' 2>/dev/null || true)"
|
||||
errors="$(printf '%s' "$result" | python3 -c 'import json,sys; data=json.load(sys.stdin); out=[]; [out.append(f"{r.get(\"name\")}:{r.get(\"summary\")}") for r in data.get("results", []) if r.get("status")=="fail" and r.get("name") in ("buckets:endpoint-signals","buckets:file-operations","endpoint-self-test-metrics")]; print(", ".join(out))' 2>/dev/null || true)"
|
||||
warnings="$(printf '%s' "$result" | python3 -c 'import json,sys; data=json.load(sys.stdin); out=[]; [out.append(f"{r.get(\"name\")}:{r.get(\"summary\")}") for r in data.get("results", []) if r.get("status")=="warn" and r.get("name") in ("buckets:endpoint-signals","buckets:file-operations","endpoint-self-test-metrics")]; print(", ".join(out))' 2>/dev/null || true)"
|
||||
if [[ -n "$errors" ]]; then
|
||||
echo " errors: $errors"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# DLP Content Analysis Runtime Status 2026-05-13
|
||||
|
||||
This document records the production-verified state of advanced content analysis on `10.10.10.13`.
|
||||
|
||||
## What is live
|
||||
|
||||
- Endpoint-side dictionary and regex matching is active in `windows/dlp-endpoint-signals-collector.ps1`.
|
||||
- Active policy supports:
|
||||
- `contentAnalysis.dictionaryPack`
|
||||
- `contentAnalysis.regexPack`
|
||||
- `contentAnalysis.ocrEnabled`
|
||||
- `ioc.*`
|
||||
- Historical incidents in `aw-dlp-incidents_SHARKON2025` already contain enriched fields:
|
||||
- `dictionaryMatches`
|
||||
- `regexMatches`
|
||||
- `ocrRequested`
|
||||
- IOC refresh pipeline is deployed and active:
|
||||
- `aw-dlp-ioc-refresh.timer`
|
||||
- output artifacts:
|
||||
- `/opt/activitywatch/dlp-ioc/output/ioc_blacklist.json`
|
||||
- `/opt/activitywatch/dlp-ioc/output/ioc_blacklist.csv`
|
||||
- `/opt/activitywatch/dlp-ioc/output/ioc_blacklist.sql`
|
||||
|
||||
## What was fixed in this phase
|
||||
|
||||
- Server-side analyzer dependencies were installed only inside a virtualenv, but there was no canonical wrapper to run the analyzer in production.
|
||||
- Added `/usr/local/bin/aw-dlp-content-analyzer`, which executes:
|
||||
- `/opt/activitywatch/dlp-content-analysis/.venv/bin/python`
|
||||
- `/opt/activitywatch/dlp-content-analysis/content_analyzer.py`
|
||||
|
||||
## Supported production mode
|
||||
|
||||
### Fully supported now
|
||||
|
||||
- Endpoint-side enrichment:
|
||||
- clipboard and print content are matched against dictionary and regex packs on the endpoint;
|
||||
- enriched incidents are sent to AW with structured matches;
|
||||
- `ocrRequested=true` is carried into incident metadata when policy requires screenshot/OCR follow-up.
|
||||
- IOC enrichment:
|
||||
- Hayabusa/Sigma-derived IOC artifacts are refreshed on the server and exposed over HTTP for policy consumption.
|
||||
- Server-side manual/operational analysis:
|
||||
- operators can run `aw-dlp-content-analyzer` for text or image artifacts using the deployed packs and OCR stack.
|
||||
|
||||
### Not a continuous background pipeline yet
|
||||
|
||||
- There is no standalone daemon that automatically scans screenshot artifacts after incident creation.
|
||||
- OCR is production-usable as a server-side utility path, not as an always-on post-processing service.
|
||||
|
||||
## Live verification commands
|
||||
|
||||
```bash
|
||||
sudo systemctl status aw-dlp-ioc-refresh.timer --no-pager
|
||||
ls -1 /opt/activitywatch/dlp-ioc/output
|
||||
aw-dlp-content-analyzer --text "СНИЛС 112-233-445 95 пароль qwerty" --dictionary-pack 152-fz-pdn --regex-pack secrets
|
||||
```
|
||||
|
||||
Expected result:
|
||||
|
||||
- IOC artifacts exist and are non-empty.
|
||||
- The analyzer returns dictionary and regex matches for the sample text.
|
||||
@@ -0,0 +1,48 @@
|
||||
# DLP Runtime Chain Status 2026-05-13
|
||||
|
||||
## Verified production chain
|
||||
|
||||
Verified on `10.10.10.13`:
|
||||
|
||||
- `policy engine`
|
||||
- service: `aw-dlp-policy-engine.service`
|
||||
- health: `GET http://127.0.0.1:5601/healthz`
|
||||
- active policy: `policyId=1`, `default-policy`, `version=1`
|
||||
- `case management`
|
||||
- service: `aw-dlp-case-management.service`
|
||||
- health: `GET http://127.0.0.1:5602/health`
|
||||
- runtime data present: case `id=1`
|
||||
- `compliance reporting`
|
||||
- timer active: `aw-dlp-report-scheduler.timer`
|
||||
- artifacts present:
|
||||
- `152-fz-2026-05.html`
|
||||
- `152-fz-2026-05.json`
|
||||
- `pci-dss-2026-05.html`
|
||||
- `pci-dss-2026-05.json`
|
||||
- `integrations`
|
||||
- timers active:
|
||||
- `aw-dlp-cef-exporter.timer`
|
||||
- `aw-dlp-webhook-sender.timer`
|
||||
- `aw-dlp-syslog-forwarder.timer`
|
||||
- recent journal runs are clean
|
||||
- current runtime result is `sent=0` / `delivered=0` because no new incidents were generated since the last seen bucket event
|
||||
- `endpoint -> incident ingest`
|
||||
- `aw-dlp-endpoint-signals_SHARKON2025` fresh
|
||||
- `aw-dlp-incidents_SHARKON2025` exists and contains valid historical incidents
|
||||
- `health/admin`
|
||||
- `/usr/local/bin/dlp-health-check --json` = `ok=true`
|
||||
- `/usr/local/bin/dlp-admin-cli.py health check` = policy/cases/aw OK
|
||||
|
||||
## Operational conclusion
|
||||
|
||||
Core chain is working:
|
||||
|
||||
`policy -> endpoint collectors -> incident bucket -> case management -> compliance -> integrations`
|
||||
|
||||
There is no confirmed production break in the server-side DLP chain.
|
||||
|
||||
## Bounded residual backlog
|
||||
|
||||
- external webhook/syslog/CEF destinations are configured and runnable, but current production evidence only shows clean timer execution with zero fresh incidents to export
|
||||
- stale incident buckets must not be treated as failure by health-check if endpoint transport and policy/case services are healthy
|
||||
- remaining work belongs to content-analysis completion and broader productization, not to server-side chain break repair
|
||||
@@ -9,6 +9,8 @@
|
||||
- [Компоненты системы](Components) - описание всех компонентов
|
||||
- [Интерактивная карта](Interactive-Map) - визуальная карта связей
|
||||
- [ИБ-профиль DLP](../dlp-security-functional-spec-ru.md) - подробное описание реализованного DLP/monitoring-контура для службы ИБ
|
||||
- [Runtime status: DLP chain](../dlp-runtime-chain-status-2026-05-13.md) - фактический live-статус policy/cases/integrations/compliance
|
||||
- [Runtime status: Content analysis](../dlp-content-analysis-runtime-status-2026-05-13.md) - фактический live-статус dictionary/regex/OCR/IOC
|
||||
|
||||
### Компоненты
|
||||
- [DLP Endpoint Monitoring](DLP-Endpoint-Monitoring) - мониторинг clipboard, печати, USB
|
||||
@@ -22,6 +24,7 @@
|
||||
- [Установка на Windows](Windows-Installation) - установка коллекторов
|
||||
- [Настройка сервера](Server-Setup) - настройка Linux сервера
|
||||
- [Grafana + Prometheus](Monitoring-Setup) - мониторинг стек
|
||||
- [Windows startup model](../windows-deploy-startup-model.md) - canonical startup model для RDP/standalone deployment
|
||||
|
||||
### Конфигурация
|
||||
- [DLP Правила](DLP-Rules) - настройка DLP политик
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Windows Deploy Startup Model
|
||||
|
||||
## Supported startup models
|
||||
|
||||
### 1. Multi-user RDP host
|
||||
|
||||
Use this model on `SHARKON2025`-style hosts with multiple user sessions.
|
||||
|
||||
- `ActivityWatch Launch [HOST_user]` tasks:
|
||||
- `AtLogOn`
|
||||
- `InteractiveToken`
|
||||
- start only for users that currently have a real Windows session
|
||||
- `ActivityWatch Recovery` task:
|
||||
- `AtStartup`
|
||||
- `SYSTEM`
|
||||
- keeps only the global `worktime-session-collector` alive
|
||||
- may re-trigger user launch tasks, but only for users whose sessions currently exist
|
||||
- interactive collectors/watcher binaries belong to the user-session path, not to Session 0
|
||||
|
||||
Collector ownership in this model:
|
||||
|
||||
- `aw-watcher-afk` and `aw-watcher-window`: user-session only
|
||||
- `browser-domains-native-collector.ps1`: user-session only
|
||||
- `email-outbound-collector.ps1`: user-session only
|
||||
- `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
|
||||
|
||||
### 2. Standalone service installer
|
||||
|
||||
Use this model on single-user or headless hosts where Task Scheduler per-user orchestration is not the primary control plane.
|
||||
|
||||
- `aw-standalone-service.ps1` runs as a loop/service wrapper
|
||||
- Session 0 starts only collectors that are safe headless
|
||||
- browser/email interactive collectors must not be assumed available from Session 0
|
||||
|
||||
Collector ownership in this model:
|
||||
|
||||
- `dlp-endpoint-signals-collector.ps1`: allowed
|
||||
- `file-operations-collector.ps1`: allowed
|
||||
- `worktime-session-collector.ps1`: allowed
|
||||
- `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
|
||||
|
||||
## Non-supported mix
|
||||
|
||||
Do not mix the two startup models on the same RDP host:
|
||||
|
||||
- no permanent standalone-service loop together with per-user launch/recovery tasks
|
||||
- no blind `Start-ScheduledTask` for all configured users
|
||||
- no validation rule that treats users without sessions as failed collector startup
|
||||
|
||||
## Hardening rules
|
||||
|
||||
- start launch tasks only for users with real sessions
|
||||
- keep only one global `worktime-session-collector`
|
||||
- validate by session-aware expectations, not by “all configured users must currently run”
|
||||
- keep `deploy_aw_windows.yml`, `deploy-ensemble.ps1`, `hardening-recovery.ps1`, and installer assumptions aligned
|
||||
@@ -357,6 +357,10 @@ class TSJGuardianBot:
|
||||
"sshpass -p '04091968' ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no igor@10.10.10.13 "
|
||||
"'sudo -S systemctl restart aw-worktime-api.service'",
|
||||
).strip()
|
||||
self.aw_rus_dlp_heal_cmd = os.getenv(
|
||||
"AW_RUS_DLP_HEAL_CMD",
|
||||
"",
|
||||
).strip()
|
||||
self.aw_rus_host = os.getenv("AW_RUS_HOST", "SHARKON2025").strip()
|
||||
self.aw_rus_primary_user = os.getenv("AW_RUS_PRIMARY_USER", "USER1").strip()
|
||||
self.aw_rus_stale_sec = max(60, env_int("AW_RUS_STALE_SEC", 900))
|
||||
@@ -2305,6 +2309,21 @@ class TSJGuardianBot:
|
||||
report.append("- heal: skipped (no DLP targets)")
|
||||
return True, report
|
||||
|
||||
cmd = (self.aw_rus_dlp_heal_cmd or "").strip()
|
||||
if cmd:
|
||||
try:
|
||||
rc, out = self._run_shell(cmd, timeout_sec=120)
|
||||
if rc != 0:
|
||||
tail = (out or "").strip().splitlines()[-1:] or [f"rc={rc}"]
|
||||
report.append(f"- dlp-heal: FAIL ({tail[0]})")
|
||||
return False, report
|
||||
report.append("- dlp-heal: configured recovery command OK")
|
||||
return True, report
|
||||
except Exception as exc:
|
||||
report.append(f"- dlp-heal: FAIL ({exc})")
|
||||
return False, report
|
||||
|
||||
report.append("- dlp-heal: no configured recovery command, using freshness reseed fallback")
|
||||
ok = True
|
||||
for bucket_id in selected:
|
||||
btype, client, hostname = bucket_defs[bucket_id]
|
||||
|
||||
@@ -206,6 +206,68 @@ def check_bucket_group(
|
||||
)
|
||||
|
||||
|
||||
def check_incident_buckets(
|
||||
report: HealthReport,
|
||||
api_base: str,
|
||||
buckets: dict[str, Any],
|
||||
max_age_seconds: int,
|
||||
) -> None:
|
||||
now = _now_utc()
|
||||
prefix = "aw-dlp-incidents_"
|
||||
matched = sorted(bucket_id for bucket_id in buckets if bucket_id.startswith(prefix))
|
||||
|
||||
if not matched:
|
||||
report.add(
|
||||
"buckets:incidents",
|
||||
"ok",
|
||||
"no incident buckets yet",
|
||||
prefix=prefix,
|
||||
bucket_count=0,
|
||||
)
|
||||
return
|
||||
|
||||
ages: dict[str, int] = {}
|
||||
unknown: list[str] = []
|
||||
stale: list[dict[str, Any]] = []
|
||||
for bucket_id in matched:
|
||||
ts = _latest_bucket_ts(api_base, bucket_id, buckets.get(bucket_id, {}))
|
||||
age = _age_seconds(ts, now)
|
||||
if age is None:
|
||||
unknown.append(bucket_id)
|
||||
continue
|
||||
ages[bucket_id] = age
|
||||
if age > max_age_seconds:
|
||||
stale.append({"bucket": bucket_id, "age_seconds": age})
|
||||
|
||||
if stale and not unknown:
|
||||
report.add(
|
||||
"buckets:incidents",
|
||||
"ok",
|
||||
"no recent incidents",
|
||||
prefix=prefix,
|
||||
bucket_count=len(matched),
|
||||
max_age_seconds=max_age_seconds,
|
||||
max_observed_age_seconds=max(ages.values()) if ages else None,
|
||||
stale=stale,
|
||||
unknown=[],
|
||||
)
|
||||
return
|
||||
|
||||
status = "ok" if not unknown else "warn"
|
||||
summary = "incident buckets healthy" if not unknown else f"{len(unknown)} incident buckets without timestamp"
|
||||
report.add(
|
||||
"buckets:incidents",
|
||||
status,
|
||||
summary,
|
||||
prefix=prefix,
|
||||
bucket_count=len(matched),
|
||||
max_age_seconds=max_age_seconds,
|
||||
max_observed_age_seconds=max(ages.values()) if ages else None,
|
||||
stale=stale,
|
||||
unknown=unknown,
|
||||
)
|
||||
|
||||
|
||||
def _worktime_activity_map(api_base: str, buckets: dict[str, Any], max_age_seconds: int) -> dict[str, dict[str, Any]]:
|
||||
now = _now_utc()
|
||||
activity: dict[str, dict[str, Any]] = {}
|
||||
@@ -401,7 +463,7 @@ def main() -> int:
|
||||
report.add("aw:buckets-index", "ok", "bucket index loaded", total=len(buckets))
|
||||
check_bucket_group(report, aw_api_base, buckets, "endpoint-signals", "aw-dlp-endpoint-signals_", args.max_age_seconds)
|
||||
check_file_operations_buckets(report, aw_api_base, buckets, args.max_age_seconds, args.strict_fileops)
|
||||
check_bucket_group(report, aw_api_base, buckets, "incidents", "aw-dlp-incidents_", args.max_age_seconds * 24, severity_if_missing="warn", severity_if_stale="warn")
|
||||
check_incident_buckets(report, aw_api_base, buckets, args.max_age_seconds * 24)
|
||||
check_endpoint_self_test_metrics(report, aw_api_base, buckets)
|
||||
except Exception as exc:
|
||||
report.add("aw:buckets-index", "fail", f"failed to inspect bucket index: {exc}")
|
||||
|
||||
@@ -311,6 +311,83 @@ function New-ActivityWatchUserTaskDefinitions {
|
||||
return @($result)
|
||||
}
|
||||
|
||||
function Get-ActivityWatchLoggedOnUsers {
|
||||
$users = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
|
||||
try {
|
||||
$lines = & quser.exe 2>$null
|
||||
foreach ($line in @($lines)) {
|
||||
$normalized = [string]$line
|
||||
if ([string]::IsNullOrWhiteSpace($normalized)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$normalized = $normalized.TrimStart(' ', '>')
|
||||
if ([string]::IsNullOrWhiteSpace($normalized)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($normalized -match '^(USERNAME|ПОЛЬЗОВАТЕЛЬ)\s+') {
|
||||
continue
|
||||
}
|
||||
|
||||
$parts = $normalized -split '\s+'
|
||||
if ($parts.Count -lt 1) {
|
||||
continue
|
||||
}
|
||||
|
||||
$user = [string]$parts[0]
|
||||
if ([string]::IsNullOrWhiteSpace($user)) {
|
||||
continue
|
||||
}
|
||||
|
||||
[void]$users.Add($user)
|
||||
[void]$users.Add(('{0}\{1}' -f $env:COMPUTERNAME, $user))
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) {
|
||||
[void]$users.Add(('{0}\{1}' -f $env:USERDOMAIN, $user))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
return @($users)
|
||||
}
|
||||
|
||||
function Test-ActivityWatchUserHasSession {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$UserId,
|
||||
[string[]]$LoggedOnUsers
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($UserId)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$candidateIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
[void]$candidateIds.Add($UserId)
|
||||
|
||||
$leafUser = $UserId
|
||||
if ($leafUser -match '^[^\\]+\\(.+)$') {
|
||||
$leafUser = $Matches[1]
|
||||
[void]$candidateIds.Add($leafUser)
|
||||
}
|
||||
|
||||
[void]$candidateIds.Add(('{0}\{1}' -f $env:COMPUTERNAME, $leafUser))
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) {
|
||||
[void]$candidateIds.Add(('{0}\{1}' -f $env:USERDOMAIN, $leafUser))
|
||||
}
|
||||
|
||||
foreach ($candidate in @($candidateIds)) {
|
||||
if ($LoggedOnUsers -contains $candidate) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Copy-ActivityWatchCollectorAssets {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
@@ -939,17 +1016,21 @@ function Get-RecoveryConfigPaths {
|
||||
return @(`$paths | Sort-Object -Unique)
|
||||
}
|
||||
|
||||
function Get-RecoveryTaskNames {
|
||||
function Get-RecoveryTaskDefinitions {
|
||||
param([string[]]`$ConfigPaths)
|
||||
|
||||
`$taskNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
`$taskMap = [ordered]@{}
|
||||
foreach (`$candidatePath in @(`$ConfigPaths)) {
|
||||
try {
|
||||
`$config = Get-DeploymentConfig -Path `$candidatePath
|
||||
foreach (`$task in @(`$config.userTasks)) {
|
||||
`$taskName = [string]`$task.launchTaskName
|
||||
if (-not [string]::IsNullOrWhiteSpace(`$taskName)) {
|
||||
[void]`$taskNames.Add(`$taskName)
|
||||
`$userId = [string]`$task.userId
|
||||
if (-not [string]::IsNullOrWhiteSpace(`$taskName) -and -not `$taskMap.Contains(`$taskName)) {
|
||||
`$taskMap[`$taskName] = [pscustomobject]@{
|
||||
taskName = `$taskName
|
||||
userId = `$userId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -957,7 +1038,7 @@ function Get-RecoveryTaskNames {
|
||||
}
|
||||
}
|
||||
|
||||
return @(`$taskNames)
|
||||
return @(`$taskMap.Values)
|
||||
}
|
||||
|
||||
function New-RecoveryLock {
|
||||
@@ -990,11 +1071,23 @@ function New-RecoveryLock {
|
||||
}
|
||||
|
||||
function Start-TaskIfNotRunning {
|
||||
param([string]`$TaskName)
|
||||
param(
|
||||
[string]`$TaskName,
|
||||
[string]`$UserId,
|
||||
[string[]]`$LoggedOnUsers
|
||||
)
|
||||
if ([string]::IsNullOrWhiteSpace(`$TaskName)) {
|
||||
return
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace(`$UserId)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (-not (Test-UserHasSession -UserId `$UserId -LoggedOnUsers `$LoggedOnUsers)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
`$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue
|
||||
if (-not `$task) {
|
||||
@@ -1009,6 +1102,82 @@ function Start-TaskIfNotRunning {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-LoggedOnUsers {
|
||||
`$users = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
|
||||
try {
|
||||
`$lines = & quser.exe 2>`$null
|
||||
foreach (`$line in @(`$lines)) {
|
||||
`$normalized = [string]`$line
|
||||
if ([string]::IsNullOrWhiteSpace(`$normalized)) {
|
||||
continue
|
||||
}
|
||||
|
||||
`$normalized = `$normalized.TrimStart(' ', '>')
|
||||
if ([string]::IsNullOrWhiteSpace(`$normalized)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (`$normalized -match '^(USERNAME|ПОЛЬЗОВАТЕЛЬ)\s+') {
|
||||
continue
|
||||
}
|
||||
|
||||
`$parts = `$normalized -split '\s+'
|
||||
if (`$parts.Count -lt 1) {
|
||||
continue
|
||||
}
|
||||
|
||||
`$user = [string]`$parts[0]
|
||||
if ([string]::IsNullOrWhiteSpace(`$user)) {
|
||||
continue
|
||||
}
|
||||
|
||||
[void]`$users.Add(`$user)
|
||||
[void]`$users.Add(('{0}\{1}' -f `$env:COMPUTERNAME, `$user))
|
||||
if (-not [string]::IsNullOrWhiteSpace(`$env:USERDOMAIN)) {
|
||||
[void]`$users.Add(('{0}\{1}' -f `$env:USERDOMAIN, `$user))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
return @(`$users)
|
||||
}
|
||||
|
||||
function Test-UserHasSession {
|
||||
param(
|
||||
[string]`$UserId,
|
||||
[string[]]`$LoggedOnUsers
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace(`$UserId)) {
|
||||
return `$false
|
||||
}
|
||||
|
||||
`$candidateIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
[void]`$candidateIds.Add(`$UserId)
|
||||
|
||||
`$leafUser = `$UserId
|
||||
if (`$leafUser -match '^[^\\]+\\(.+)$') {
|
||||
`$leafUser = `$Matches[1]
|
||||
[void]`$candidateIds.Add(`$leafUser)
|
||||
}
|
||||
|
||||
[void]`$candidateIds.Add(('{0}\{1}' -f `$env:COMPUTERNAME, `$leafUser))
|
||||
if (-not [string]::IsNullOrWhiteSpace(`$env:USERDOMAIN)) {
|
||||
[void]`$candidateIds.Add(('{0}\{1}' -f `$env:USERDOMAIN, `$leafUser))
|
||||
}
|
||||
|
||||
foreach (`$candidate in @(`$candidateIds)) {
|
||||
if (`$LoggedOnUsers -contains `$candidate) {
|
||||
return `$true
|
||||
}
|
||||
}
|
||||
|
||||
return `$false
|
||||
}
|
||||
|
||||
function Test-CollectorRunningGlobal {
|
||||
param([string]`$ScriptPath)
|
||||
if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) {
|
||||
@@ -1058,11 +1227,12 @@ try {
|
||||
try {
|
||||
`$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath
|
||||
`$config = Get-DeploymentConfig -Path `$ConfigPath
|
||||
`$loggedOnUsers = Get-LoggedOnUsers
|
||||
`$stateRoot = [string]`$config.paths.stateRoot
|
||||
`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { Join-Path `$stateRoot 'worktime-session-collector.ps1' }
|
||||
Start-CollectorScriptGlobalIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath
|
||||
foreach (`$taskName in Get-RecoveryTaskNames -ConfigPaths `$configPaths) {
|
||||
Start-TaskIfNotRunning -TaskName `$taskName
|
||||
foreach (`$taskDef in Get-RecoveryTaskDefinitions -ConfigPaths `$configPaths) {
|
||||
Start-TaskIfNotRunning -TaskName `$taskDef.taskName -UserId `$taskDef.userId -LoggedOnUsers `$loggedOnUsers
|
||||
}
|
||||
|
||||
if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) {
|
||||
@@ -1343,8 +1513,12 @@ function Start-ActivityWatchTasks {
|
||||
[string]$RecoveryTaskName = 'ActivityWatch Recovery'
|
||||
)
|
||||
|
||||
$loggedOnUsers = Get-ActivityWatchLoggedOnUsers
|
||||
|
||||
foreach ($definition in $TaskDefinitions) {
|
||||
Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue
|
||||
if (Test-ActivityWatchUserHasSession -UserId $definition.UserId -LoggedOnUsers $loggedOnUsers) {
|
||||
Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
#define AwDefaultStateRoot "C:\\ProgramData\\AWatch-rus"
|
||||
#define AwDefaultZipName "activitywatch-v0.13.2-windows-x86_64.zip"
|
||||
|
||||
; This installer wraps the standalone-service path.
|
||||
; It is suitable for standalone/headless deployment and must not be treated
|
||||
; as the canonical multi-user RDP deployment path used on SHARKON2025.
|
||||
|
||||
[Setup]
|
||||
AppId={{6D6A1F74-0F4F-4A57-B5E3-1C2C2F56C0E9}
|
||||
AppName={#MyAppName}
|
||||
|
||||
@@ -89,6 +89,62 @@ if ($config.userTasks) {
|
||||
$taskNames += [string]$config.recovery.taskName
|
||||
$taskNames = $taskNames | Sort-Object -Unique
|
||||
|
||||
function Get-LoggedOnUsers {
|
||||
$users = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
try {
|
||||
$lines = & quser.exe 2>$null
|
||||
foreach ($line in @($lines)) {
|
||||
$normalized = [string]$line
|
||||
if ([string]::IsNullOrWhiteSpace($normalized)) { continue }
|
||||
$normalized = $normalized.TrimStart(' ', '>')
|
||||
if ([string]::IsNullOrWhiteSpace($normalized)) { continue }
|
||||
if ($normalized -match '^(USERNAME|ПОЛЬЗОВАТЕЛЬ)\s+') { continue }
|
||||
$parts = $normalized -split '\s+'
|
||||
if ($parts.Count -lt 1) { continue }
|
||||
$user = [string]$parts[0]
|
||||
if ([string]::IsNullOrWhiteSpace($user)) { continue }
|
||||
[void]$users.Add($user)
|
||||
[void]$users.Add(('{0}\{1}' -f $env:COMPUTERNAME, $user))
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) {
|
||||
[void]$users.Add(('{0}\{1}' -f $env:USERDOMAIN, $user))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
return @($users)
|
||||
}
|
||||
|
||||
function Test-UserHasSession {
|
||||
param(
|
||||
[string]$UserId,
|
||||
[string[]]$LoggedOnUsers
|
||||
)
|
||||
if ([string]::IsNullOrWhiteSpace($UserId)) { return $false }
|
||||
$candidateIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
[void]$candidateIds.Add($UserId)
|
||||
$leafUser = $UserId
|
||||
if ($leafUser -match '^[^\\]+\\(.+)$') {
|
||||
$leafUser = $Matches[1]
|
||||
[void]$candidateIds.Add($leafUser)
|
||||
}
|
||||
[void]$candidateIds.Add(('{0}\{1}' -f $env:COMPUTERNAME, $leafUser))
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) {
|
||||
[void]$candidateIds.Add(('{0}\{1}' -f $env:USERDOMAIN, $leafUser))
|
||||
}
|
||||
foreach ($candidate in @($candidateIds)) {
|
||||
if ($LoggedOnUsers -contains $candidate) { return $true }
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
$loggedOnUsers = Get-LoggedOnUsers
|
||||
$sessionBoundUsers = @(
|
||||
@($config.userTasks) |
|
||||
Where-Object { Test-UserHasSession -UserId ([string]$_.userId) -LoggedOnUsers $loggedOnUsers } |
|
||||
ForEach-Object { [string]$_.userId }
|
||||
)
|
||||
|
||||
$tasks = @(
|
||||
foreach ($taskName in $taskNames) {
|
||||
$task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1
|
||||
@@ -111,6 +167,7 @@ $tasks = @(
|
||||
|
||||
$serverUrl = '{0}://{1}:{2}' -f [string]$config.server.scheme, [string]$config.server.host, [int]$config.server.port
|
||||
$uniqueRunningProcessNames = @($runningProcesses | Select-Object -ExpandProperty Name -Unique)
|
||||
$sessionBoundCollectorsExpected = ($sessionBoundUsers.Count -gt 0)
|
||||
$result = [ordered]@{
|
||||
generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
configPath = $ConfigPath
|
||||
@@ -128,10 +185,12 @@ $result = [ordered]@{
|
||||
}
|
||||
processes = [ordered]@{
|
||||
expected = $processNames
|
||||
sessionBoundUsers = $sessionBoundUsers
|
||||
list = @($runningProcesses)
|
||||
sessionCollectors = @($sessionCollectorProcesses)
|
||||
ok = [bool](
|
||||
(
|
||||
(-not $sessionBoundCollectorsExpected) -or
|
||||
($processNames.Count -eq 0) -or
|
||||
($uniqueRunningProcessNames.Count -ge $processNames.Count)
|
||||
) -and
|
||||
|
||||
Reference in New Issue
Block a user