fix(worktime): isolate global session collector and reset RDP bridge flow
This commit is contained in:
@@ -31,8 +31,8 @@
|
||||
aw_windows_policy_engine_host: "{{ aw_windows_server_host }}"
|
||||
aw_windows_policy_engine_port: 5601
|
||||
aw_windows_policy_engine_scheme: "http"
|
||||
aw_windows_afk_enabled: true
|
||||
aw_windows_window_enabled: true
|
||||
aw_windows_afk_enabled_default: true
|
||||
aw_windows_window_enabled_default: true
|
||||
aw_windows_file_ops_enabled: true
|
||||
aw_windows_local_agent_logs_enabled: false
|
||||
aw_windows_incident_capture_enabled: true
|
||||
@@ -50,7 +50,7 @@
|
||||
aw_windows_api_smoke_check_enabled: true
|
||||
aw_windows_api_smoke_check_bucket: ""
|
||||
aw_windows_api_smoke_check_limit: 10
|
||||
aw_windows_api_smoke_check_window_enabled: true
|
||||
aw_windows_api_smoke_check_window_enabled_default: true
|
||||
aw_windows_api_smoke_check_window_bucket: ""
|
||||
aw_windows_api_smoke_check_min_events: 1
|
||||
aw_windows_fail_on_validation_error: true
|
||||
@@ -72,6 +72,12 @@
|
||||
- aw_windows_state_root is defined
|
||||
fail_msg: "Не заданы обязательные переменные Windows-развёртывания."
|
||||
|
||||
- name: Нормализовать effective флаги collector'ов и smoke-check
|
||||
ansible.builtin.set_fact:
|
||||
aw_windows_afk_enabled_effective: "{{ (aw_windows_afk_enabled | default(aw_windows_afk_enabled_default)) | bool }}"
|
||||
aw_windows_window_enabled_effective: "{{ (aw_windows_window_enabled | default(aw_windows_window_enabled_default)) | bool }}"
|
||||
aw_windows_api_smoke_check_window_enabled_effective: "{{ (aw_windows_api_smoke_check_window_enabled | default(aw_windows_api_smoke_check_window_enabled_default)) | bool }}"
|
||||
|
||||
- name: Создать каталоги развёртывания
|
||||
ansible.windows.win_file:
|
||||
path: "{{ item }}"
|
||||
@@ -97,6 +103,7 @@
|
||||
- deploy-domain-users.ps1
|
||||
- deploy-ensemble.ps1
|
||||
- hardening-recovery.ps1
|
||||
- rebuild-worktime-tasks.ps1
|
||||
- validate-deployment.ps1
|
||||
- web-category-rules.example.json
|
||||
- dlp-policy.example.json
|
||||
@@ -155,8 +162,8 @@
|
||||
UserListPath = "{{ aw_windows_deploy_root }}\windows\users.txt"
|
||||
InstallRoot = "{{ aw_windows_install_root }}"
|
||||
StateRoot = "{{ aw_windows_state_root }}"
|
||||
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
|
||||
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
|
||||
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled_effective | bool) else '$false' }}
|
||||
WindowEnabled = {{ '$true' if (aw_windows_window_enabled_effective | bool) else '$false' }}
|
||||
FileOpsEnabled = {{ '$true' if (aw_windows_file_ops_enabled | bool) else '$false' }}
|
||||
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
|
||||
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
|
||||
@@ -225,18 +232,20 @@
|
||||
ForEach-Object {
|
||||
$key = Get-CollectorKey -CommandLine $_.CommandLine
|
||||
if ($key) {
|
||||
$groupKey = if ($key -eq 'worktime') { 'worktime::global' } else { '{0}::{1}' -f $key, ([int]$_.SessionId) }
|
||||
[pscustomobject]@{
|
||||
ProcessId = [int]$_.ProcessId
|
||||
SessionId = [int]$_.SessionId
|
||||
CreationDate = $_.CreationDate
|
||||
CollectorKey = $key
|
||||
GroupKey = $groupKey
|
||||
}
|
||||
}
|
||||
} |
|
||||
Where-Object { $_ -ne $null }
|
||||
|
||||
# Keep only one process per (collector, session): newest survives, older duplicates are stopped.
|
||||
foreach ($group in ($collectorProcs | Group-Object CollectorKey, SessionId)) {
|
||||
# Keep only one process per collector scope: worktime collector is global, others stay per-session.
|
||||
foreach ($group in ($collectorProcs | Group-Object GroupKey)) {
|
||||
$ordered = @($group.Group | Sort-Object CreationDate -Descending)
|
||||
if ($ordered.Count -le 1) { continue }
|
||||
foreach ($dup in $ordered | Select-Object -Skip 1) {
|
||||
@@ -271,7 +280,7 @@
|
||||
- name: Вычислить AW AFK smoke-check bucket
|
||||
when:
|
||||
- aw_windows_api_smoke_check_enabled | bool
|
||||
- aw_windows_afk_enabled | bool
|
||||
- aw_windows_afk_enabled_effective | bool
|
||||
- aw_windows_hostname_result.stdout is defined
|
||||
ansible.builtin.set_fact:
|
||||
aw_windows_api_smoke_check_bucket_effective: >-
|
||||
@@ -284,8 +293,8 @@
|
||||
- name: Вычислить AW Window smoke-check bucket
|
||||
when:
|
||||
- aw_windows_api_smoke_check_enabled | bool
|
||||
- aw_windows_api_smoke_check_window_enabled | bool
|
||||
- aw_windows_window_enabled | bool
|
||||
- aw_windows_api_smoke_check_window_enabled_effective | bool
|
||||
- aw_windows_window_enabled_effective | bool
|
||||
- aw_windows_hostname_result.stdout is defined
|
||||
ansible.builtin.set_fact:
|
||||
aw_windows_api_smoke_check_window_bucket_effective: >-
|
||||
@@ -298,7 +307,7 @@
|
||||
- name: Выполнить AW API smoke-check (AFK bucket должен получать события)
|
||||
when:
|
||||
- aw_windows_api_smoke_check_enabled | bool
|
||||
- aw_windows_afk_enabled | bool
|
||||
- aw_windows_afk_enabled_effective | bool
|
||||
ansible.builtin.uri:
|
||||
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
|
||||
method: GET
|
||||
@@ -318,8 +327,8 @@
|
||||
- name: Выполнить AW API smoke-check (Window bucket должен получать события)
|
||||
when:
|
||||
- aw_windows_api_smoke_check_enabled | bool
|
||||
- aw_windows_api_smoke_check_window_enabled | bool
|
||||
- aw_windows_window_enabled | bool
|
||||
- aw_windows_api_smoke_check_window_enabled_effective | bool
|
||||
- aw_windows_window_enabled_effective | bool
|
||||
ansible.builtin.uri:
|
||||
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_window_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
|
||||
method: GET
|
||||
|
||||
@@ -32,8 +32,8 @@ aw_windows_policy_engine_host: "{{ aw_windows_server_host }}"
|
||||
aw_windows_policy_engine_port: 5601
|
||||
aw_windows_policy_engine_scheme: "http"
|
||||
|
||||
aw_windows_afk_enabled: true
|
||||
aw_windows_window_enabled: true
|
||||
aw_windows_afk_enabled: false
|
||||
aw_windows_window_enabled: false
|
||||
aw_windows_file_ops_enabled: true
|
||||
aw_windows_email_collector_enabled: true
|
||||
aw_windows_email_collector_mode: "both"
|
||||
|
||||
@@ -42,7 +42,8 @@ def _is_active_sample(data: dict) -> bool:
|
||||
except Exception:
|
||||
sid = -1
|
||||
user = str(data.get("username") or "").strip()
|
||||
if sid > 0 and user and (not _is_machine_user(user)):
|
||||
session_name = str(data.get("sessionName") or "").strip().lower()
|
||||
if sid > 0 and user and (not _is_machine_user(user)) and (session_name.startswith("rdp-") or session_name == "console"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -112,7 +112,8 @@ def is_active(d):
|
||||
try: sid=int(d.get("sessionId"))
|
||||
except: sid=-1
|
||||
u=str(d.get("username","")).strip().lower()
|
||||
if sid>0 and u and (not u.endswith("$")): return True
|
||||
sn=str(d.get("sessionName","")).strip().lower()
|
||||
if sid>0 and u and (not u.endswith("$")) and (sn.startswith("rdp-") or sn=="console"): return True
|
||||
return False
|
||||
|
||||
rows=req("GET",f"/api/0/buckets/{sb}/events?limit=12000") or []
|
||||
|
||||
@@ -96,7 +96,8 @@ def _is_session_active(row_data):
|
||||
except Exception:
|
||||
sid = -1
|
||||
user = str(row_data.get("username", "")).strip().lower()
|
||||
if sid > 0 and user and (not user.endswith("$")):
|
||||
session_name = str(row_data.get("sessionName", "")).strip().lower()
|
||||
if sid > 0 and user and (not user.endswith("$")) and (session_name.startswith("rdp-") or session_name == "console"):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -111,7 +112,11 @@ def transform(events):
|
||||
ts = e.get("timestamp")
|
||||
if not ts:
|
||||
continue
|
||||
grouped.setdefault(ts, []).append(e)
|
||||
try:
|
||||
normalized_ts = to_iso_utc(parse_iso_utc(ts).replace(microsecond=0).isoformat())
|
||||
except Exception:
|
||||
normalized_ts = ts
|
||||
grouped.setdefault(normalized_ts, []).append(e)
|
||||
|
||||
ordered_ts = sorted(grouped.keys())
|
||||
parsed_ts = {}
|
||||
|
||||
@@ -873,7 +873,6 @@ try {
|
||||
if (`$fileOpsEnabled) {
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$fileCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
}
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
if (`$emailEnabled -and (Test-Path -LiteralPath `$emailCollectorScript)) {
|
||||
Start-CollectorScriptIfNeeded -ScriptPath `$emailCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
||||
}
|
||||
@@ -1010,6 +1009,44 @@ function Start-TaskIfNotRunning {
|
||||
}
|
||||
}
|
||||
|
||||
function Test-CollectorRunningGlobal {
|
||||
param([string]`$ScriptPath)
|
||||
if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) {
|
||||
return `$false
|
||||
}
|
||||
|
||||
return [bool]@(
|
||||
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
(`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and
|
||||
`$_.CommandLine -match [Regex]::Escape(`$ScriptPath)
|
||||
}
|
||||
).Count
|
||||
}
|
||||
|
||||
function Start-CollectorScriptGlobalIfNeeded {
|
||||
param(
|
||||
[string]`$ScriptPath,
|
||||
[string]`$ConfigPath
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath `$ScriptPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Test-CollectorRunningGlobal -ScriptPath `$ScriptPath) {
|
||||
return
|
||||
}
|
||||
|
||||
`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
||||
`$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass', '-File', `$ScriptPath, '-ConfigPath', `$ConfigPath)
|
||||
Start-Process -FilePath `$powershellExe -ArgumentList `$argumentList -WindowStyle Hidden
|
||||
}
|
||||
|
||||
`$recoveryLockPath = New-RecoveryLock -PrimaryConfigPath `$ConfigPath
|
||||
if (-not `$recoveryLockPath) {
|
||||
return
|
||||
@@ -1020,11 +1057,14 @@ try {
|
||||
`$sleepSeconds = 180
|
||||
try {
|
||||
`$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath
|
||||
`$config = Get-DeploymentConfig -Path `$ConfigPath
|
||||
`$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
|
||||
}
|
||||
|
||||
`$config = Get-DeploymentConfig -Path `$ConfigPath
|
||||
if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) {
|
||||
`$sleepSeconds = [Math]::Max([int]`$config.recovery.intervalSeconds, 30)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$ModulePath = 'C:\Program Files\AWatch-rus\windows\ActivityWatch.Windows.Common.psm1'
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
Import-Module $ModulePath -Force
|
||||
|
||||
$config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
|
||||
$launchScript = [string]$config.paths.launchScript
|
||||
$recoveryScript = [string]$config.paths.recoveryScript
|
||||
$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') {
|
||||
[string]$config.paths.sessionCollectorScript
|
||||
} else {
|
||||
Join-Path ([string]$config.paths.stateRoot) 'worktime-session-collector.ps1'
|
||||
}
|
||||
$taskDefinitions = @($config.userTasks)
|
||||
$recoveryTaskName = [string]$config.recovery.taskName
|
||||
|
||||
$collectorProcs = @(
|
||||
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and
|
||||
$_.CommandLine -and
|
||||
$_.CommandLine -match [Regex]::Escape($sessionCollectorScript)
|
||||
}
|
||||
)
|
||||
|
||||
foreach ($proc in $collectorProcs) {
|
||||
try {
|
||||
Stop-Process -Id ([int]$proc.ProcessId) -Force -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
Write-ActivityWatchLaunchScript -Path $launchScript -ConfigPath $ConfigPath
|
||||
Write-ActivityWatchRecoveryScript -Path $recoveryScript -ConfigPath $ConfigPath
|
||||
Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $launchScript -ConfigPath $ConfigPath
|
||||
Register-ActivityWatchRecoveryTask -TaskName $recoveryTaskName -RecoveryScriptPath $recoveryScript -ConfigPath $ConfigPath
|
||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $recoveryTaskName
|
||||
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
$running = @(
|
||||
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and
|
||||
$_.CommandLine -and
|
||||
$_.CommandLine -match [Regex]::Escape($sessionCollectorScript)
|
||||
} |
|
||||
Select-Object Name, ProcessId, SessionId, CommandLine
|
||||
)
|
||||
|
||||
[pscustomobject]@{
|
||||
rebuiltAtUtc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
launchScript = $launchScript
|
||||
recoveryScript = $recoveryScript
|
||||
sessionCollectorScript = $sessionCollectorScript
|
||||
runningSessionCollectors = @($running)
|
||||
}
|
||||
@@ -158,20 +158,32 @@ function Parse-SessionLines {
|
||||
if ($Lines.Count -gt 0 -and $Lines[0] -match '\b(USERNAME|UserName|USER)\b') { $startIndex = 1 }
|
||||
|
||||
for ($i = $startIndex; $i -lt $Lines.Count; $i++) {
|
||||
$line = $Lines[$i].Trim()
|
||||
$line = ($Lines[$i] -replace '^\s*>', '').Trim()
|
||||
if (-not $line) { continue }
|
||||
|
||||
$m = [regex]::Match($line, '^\s*(?<user>\S+)\s+(?<sess>\S+)?\s+(?<id>\d+)\s+(?<state>\S+)', [System.Text.RegularExpressions.RegexOptions]::None)
|
||||
if ($m.Success) {
|
||||
$user = $m.Groups['user'].Value; $sess = $m.Groups['sess'].Value; $id = [int]$m.Groups['id'].Value; $state = $m.Groups['state'].Value
|
||||
$parts = $line -split '\s+'
|
||||
if ($parts.Count -lt 3) { continue }
|
||||
$user = $parts[0]
|
||||
$sess = ''
|
||||
$id = -1
|
||||
$state = ''
|
||||
|
||||
if ($parts.Count -ge 4 -and $parts[1] -match '^\d+$') {
|
||||
$sess = ''
|
||||
$id = [int]$parts[1]
|
||||
$state = [string]$parts[2]
|
||||
}
|
||||
elseif ($parts.Count -ge 4 -and $parts[2] -match '^\d+$') {
|
||||
$sess = [string]$parts[1]
|
||||
$id = [int]$parts[2]
|
||||
$state = [string]$parts[3]
|
||||
}
|
||||
else {
|
||||
$parts = $line -split '\s+'
|
||||
if ($parts.Count -lt 4) { continue }
|
||||
$user = $parts[0]
|
||||
if ($parts[1] -match '^\d+$') { $sess = ''; $id = [int]$parts[1]; $state = $parts[2] } else { $sess = $parts[1]; $id = [int]$parts[2]; $state = $parts[3] }
|
||||
continue
|
||||
}
|
||||
|
||||
if ($id -lt 0) { continue }
|
||||
|
||||
$records += [pscustomobject]@{ username=$user; sessionName=$sess; sessionId=$id; state=$state }
|
||||
}
|
||||
return $records
|
||||
|
||||
Reference in New Issue
Block a user