2668 lines
100 KiB
PowerShell
Executable File
2668 lines
100 KiB
PowerShell
Executable File
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
$script:ActivityWatchBuiltInAdministratorName = $null
|
|
|
|
function Assert-Administrator {
|
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
|
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
throw 'Запустите этот скрипт из PowerShell с правами администратора.'
|
|
}
|
|
}
|
|
|
|
function New-ActivityWatchDirectory {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
New-Item -Path $Path -ItemType Directory -Force | Out-Null
|
|
}
|
|
}
|
|
|
|
function Enable-ActivityWatchPrintTelemetry {
|
|
$policyPath = 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers'
|
|
if (-not (Test-Path -LiteralPath $policyPath)) {
|
|
New-Item -Path $policyPath -Force | Out-Null
|
|
}
|
|
New-ItemProperty -Path $policyPath -Name 'ShowJobTitleInEventLogs' -Value 1 -PropertyType DWord -Force | Out-Null
|
|
|
|
& wevtutil.exe sl 'Microsoft-Windows-PrintService/Operational' /e:true | Out-Null
|
|
}
|
|
|
|
|
|
function Get-ActivityWatchPackageUrl {
|
|
param(
|
|
[string]$Version = 'v0.13.2'
|
|
)
|
|
|
|
return "https://github.com/ActivityWatch/activitywatch/releases/download/$Version/activitywatch-$Version-windows-x86_64.zip"
|
|
}
|
|
|
|
function Get-ActivityWatchArchive {
|
|
param(
|
|
[string]$PackageZipPath,
|
|
[string]$PackageUrl,
|
|
[string]$Version = 'v0.13.2',
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$WorkingRoot
|
|
)
|
|
|
|
New-ActivityWatchDirectory -Path $WorkingRoot
|
|
|
|
if ($PackageZipPath) {
|
|
$resolved = Resolve-Path -LiteralPath $PackageZipPath -ErrorAction Stop
|
|
return $resolved.Path
|
|
}
|
|
|
|
if (-not $PackageUrl) {
|
|
$PackageUrl = Get-ActivityWatchPackageUrl -Version $Version
|
|
}
|
|
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
Get-ChildItem -LiteralPath $WorkingRoot -File -Filter 'activitywatch-*.zip' -ErrorAction SilentlyContinue |
|
|
Sort-Object LastWriteTime -Descending |
|
|
Select-Object -Skip 2 |
|
|
ForEach-Object {
|
|
try { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue } catch {}
|
|
}
|
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
$suffix = ([guid]::NewGuid().Guid.Substring(0, 8))
|
|
$archivePath = Join-Path $WorkingRoot ("activitywatch-{0}-{1}-{2}.zip" -f $Version.TrimStart('v'), $stamp, $suffix)
|
|
Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath
|
|
return $archivePath
|
|
}
|
|
|
|
function Remove-ActivityWatchOldInstallBackups {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$BackupRoot,
|
|
[int]$Keep = 2
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $BackupRoot)) {
|
|
return
|
|
}
|
|
|
|
Get-ChildItem -LiteralPath $BackupRoot -Directory -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Name -like 'install-*' } |
|
|
Sort-Object LastWriteTime -Descending |
|
|
Select-Object -Skip $Keep |
|
|
ForEach-Object {
|
|
try { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } catch {}
|
|
}
|
|
}
|
|
|
|
function Get-ActivityWatchPackageRoot {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ExpandedRoot
|
|
)
|
|
|
|
$afkBinary = Get-ChildItem -Path $ExpandedRoot -Filter 'aw-watcher-afk.exe' -File -Recurse |
|
|
Select-Object -First 1
|
|
|
|
if (-not $afkBinary) {
|
|
throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot."
|
|
}
|
|
|
|
return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent)
|
|
}
|
|
|
|
function Expand-ActivityWatchArchiveSafe {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ArchivePath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$DestinationPath,
|
|
[int]$Attempts = 3
|
|
)
|
|
|
|
for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
|
|
try {
|
|
if (Test-Path -LiteralPath $DestinationPath) {
|
|
Remove-Item -LiteralPath $DestinationPath -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
New-ActivityWatchDirectory -Path $DestinationPath
|
|
Expand-Archive -Path $ArchivePath -DestinationPath $DestinationPath -Force -ErrorAction Stop
|
|
return
|
|
}
|
|
catch {
|
|
if ($attempt -lt $Attempts) {
|
|
Start-Sleep -Milliseconds (500 * $attempt)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
# Fallback for intermittent Expand-Archive issues in Windows PowerShell.
|
|
if (Test-Path -LiteralPath $DestinationPath) {
|
|
Remove-Item -LiteralPath $DestinationPath -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
New-ActivityWatchDirectory -Path $DestinationPath
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
[System.IO.Compression.ZipFile]::ExtractToDirectory($ArchivePath, $DestinationPath)
|
|
}
|
|
|
|
function Install-ActivityWatchPackage {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ArchivePath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$InstallRoot,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$WorkingRoot,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$BackupRoot
|
|
)
|
|
|
|
New-ActivityWatchDirectory -Path $WorkingRoot
|
|
New-ActivityWatchDirectory -Path $BackupRoot
|
|
Remove-ActivityWatchOldInstallBackups -BackupRoot $BackupRoot
|
|
|
|
# Cleanup stale extraction directories from previous failed deployments.
|
|
Get-ChildItem -LiteralPath $WorkingRoot -Directory -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Name -like 'extract-*' } |
|
|
ForEach-Object {
|
|
try { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } catch {}
|
|
}
|
|
|
|
# Ensure nothing is holding locks inside InstallRoot during upgrade.
|
|
foreach ($procName in @('aw-watcher-afk', 'aw-watcher-window', 'aw-server', 'aw-qt')) {
|
|
try {
|
|
Get-Process -Name $procName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
|
|
$extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid)
|
|
if (Test-Path -LiteralPath $extractRoot) {
|
|
Remove-Item -LiteralPath $extractRoot -Recurse -Force
|
|
}
|
|
New-ActivityWatchDirectory -Path $extractRoot
|
|
|
|
$archiveSize = (Get-Item -LiteralPath $ArchivePath -ErrorAction Stop).Length
|
|
$workDriveName = [System.IO.Path]::GetPathRoot($WorkingRoot).TrimEnd('\').TrimEnd(':')
|
|
$workDrive = Get-PSDrive -Name $workDriveName -ErrorAction SilentlyContinue
|
|
$freeBytes = $null
|
|
if ($workDrive -and $null -ne $workDrive.Free) {
|
|
$freeBytes = [int64]$workDrive.Free
|
|
}
|
|
elseif ($workDriveName) {
|
|
try {
|
|
$disk = Get-CimInstance Win32_LogicalDisk -Filter ("DeviceID='{0}:'" -f $workDriveName) -ErrorAction Stop
|
|
if ($disk -and $null -ne $disk.FreeSpace) {
|
|
$freeBytes = [int64]$disk.FreeSpace
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
if ($null -ne $freeBytes) {
|
|
# Require at least ~2.5x archive size to handle extraction + copy safely.
|
|
$required = [int64]([Math]::Ceiling($archiveSize * 2.5))
|
|
if ($freeBytes -lt $required) {
|
|
throw ("Недостаточно свободного места на {0}: free={1} bytes, required>={2} bytes" -f $workDriveName, $freeBytes, $required)
|
|
}
|
|
}
|
|
|
|
try {
|
|
Expand-ActivityWatchArchiveSafe -ArchivePath $ArchivePath -DestinationPath $extractRoot
|
|
$packageRoot = Get-ActivityWatchPackageRoot -ExpandedRoot $extractRoot
|
|
|
|
if (Test-Path -LiteralPath $InstallRoot) {
|
|
$existingItems = Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue
|
|
if ($existingItems) {
|
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
$backupPath = Join-Path $BackupRoot ("install-$stamp")
|
|
New-ActivityWatchDirectory -Path $backupPath
|
|
Copy-Item -Path (Join-Path $InstallRoot '*') -Destination $backupPath -Recurse -Force
|
|
Get-ChildItem -LiteralPath $InstallRoot -Force | Remove-Item -Recurse -Force
|
|
}
|
|
}
|
|
else {
|
|
New-ActivityWatchDirectory -Path $InstallRoot
|
|
}
|
|
|
|
Copy-Item -Path (Join-Path $packageRoot '*') -Destination $InstallRoot -Recurse -Force
|
|
|
|
return [pscustomobject]@{
|
|
PackageRoot = $packageRoot
|
|
ExtractRoot = $extractRoot
|
|
BackupRoot = $BackupRoot
|
|
}
|
|
}
|
|
finally {
|
|
if (Test-Path -LiteralPath $extractRoot) {
|
|
try { Remove-Item -LiteralPath $extractRoot -Recurse -Force -ErrorAction SilentlyContinue } catch {}
|
|
}
|
|
if ((Test-Path -LiteralPath $ArchivePath) -and ($ArchivePath -like (Join-Path $WorkingRoot 'activitywatch-*.zip'))) {
|
|
try { Remove-Item -LiteralPath $ArchivePath -Force -ErrorAction SilentlyContinue } catch {}
|
|
}
|
|
Remove-ActivityWatchOldInstallBackups -BackupRoot $BackupRoot
|
|
}
|
|
}
|
|
|
|
function Get-ActivityWatchExecutableMap {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$InstallRoot
|
|
)
|
|
|
|
$map = [ordered]@{
|
|
Afk = Join-Path $InstallRoot 'aw-watcher-afk\aw-watcher-afk.exe'
|
|
Window = Join-Path $InstallRoot 'aw-watcher-window\aw-watcher-window.exe'
|
|
}
|
|
|
|
foreach ($entry in $map.GetEnumerator()) {
|
|
if (-not (Test-Path -LiteralPath $entry.Value)) {
|
|
throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)"
|
|
}
|
|
}
|
|
|
|
return [pscustomobject]$map
|
|
}
|
|
|
|
function Repair-ActivityWatchPotentialMojibake {
|
|
param([string]$Value)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Value)) {
|
|
return $Value
|
|
}
|
|
|
|
if ($Value -notmatch '[\u0400-\u04FF]') {
|
|
return $Value
|
|
}
|
|
|
|
try {
|
|
$bytes = [Text.Encoding]::GetEncoding(1251).GetBytes($Value)
|
|
$repaired = [Text.Encoding]::UTF8.GetString($bytes)
|
|
if (-not [string]::IsNullOrWhiteSpace($repaired) -and $repaired -match '[\u0400-\u04FF]') {
|
|
return $repaired
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
|
|
return $Value
|
|
}
|
|
|
|
function Get-ActivityWatchBuiltInAdministratorName {
|
|
if ($script:ActivityWatchBuiltInAdministratorName) {
|
|
return $script:ActivityWatchBuiltInAdministratorName
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($env:AWATCH_RUS_BUILTIN_ADMINISTRATOR_NAME)) {
|
|
$script:ActivityWatchBuiltInAdministratorName = [string]$env:AWATCH_RUS_BUILTIN_ADMINISTRATOR_NAME
|
|
return $script:ActivityWatchBuiltInAdministratorName
|
|
}
|
|
|
|
try {
|
|
$account = Get-CimInstance Win32_UserAccount -Filter "LocalAccount=True" -ErrorAction Stop |
|
|
Where-Object { [string]$_.SID -match '-500$' } |
|
|
Select-Object -First 1
|
|
if ($account -and -not [string]::IsNullOrWhiteSpace([string]$account.Name)) {
|
|
$script:ActivityWatchBuiltInAdministratorName = [string]$account.Name
|
|
return $script:ActivityWatchBuiltInAdministratorName
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
|
|
if ([string]$env:COMPUTERNAME -ieq 'SHARKON2025') {
|
|
$script:ActivityWatchBuiltInAdministratorName = 'Администратор'
|
|
return $script:ActivityWatchBuiltInAdministratorName
|
|
}
|
|
|
|
$script:ActivityWatchBuiltInAdministratorName = 'Administrator'
|
|
return $script:ActivityWatchBuiltInAdministratorName
|
|
}
|
|
|
|
function Normalize-ActivityWatchUserId {
|
|
param(
|
|
[string]$UserId,
|
|
[string]$Domain
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($UserId)) {
|
|
return $null
|
|
}
|
|
|
|
$normalized = Repair-ActivityWatchPotentialMojibake -Value $UserId.Trim()
|
|
$resolvedDomain = $null
|
|
$leafUser = $normalized
|
|
|
|
if ($normalized -match '^([^\\]+)\\(.+)$') {
|
|
$resolvedDomain = Repair-ActivityWatchPotentialMojibake -Value $Matches[1]
|
|
$leafUser = Repair-ActivityWatchPotentialMojibake -Value $Matches[2]
|
|
}
|
|
|
|
if ($leafUser -match '^(?i:administrator|администратор)$') {
|
|
$leafUser = Get-ActivityWatchBuiltInAdministratorName
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($resolvedDomain) -and -not [string]::IsNullOrWhiteSpace($Domain)) {
|
|
$resolvedDomain = Repair-ActivityWatchPotentialMojibake -Value $Domain.Trim()
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($resolvedDomain)) {
|
|
return ('{0}\{1}' -f $resolvedDomain, $leafUser)
|
|
}
|
|
|
|
return $leafUser
|
|
}
|
|
|
|
function Normalize-ActivityWatchUsers {
|
|
param(
|
|
[string[]]$Users,
|
|
[string]$UserListPath,
|
|
[string]$Domain
|
|
)
|
|
|
|
$collected = New-Object System.Collections.Generic.List[string]
|
|
|
|
if ($Users) {
|
|
foreach ($user in $Users) {
|
|
if (-not [string]::IsNullOrWhiteSpace($user)) {
|
|
$collected.Add($user.Trim())
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($UserListPath) {
|
|
$resolved = Resolve-Path -LiteralPath $UserListPath -ErrorAction Stop
|
|
$extension = [IO.Path]::GetExtension($resolved.Path)
|
|
if ($extension -ieq '.csv') {
|
|
$rows = Import-Csv -LiteralPath $resolved.Path -Encoding UTF8
|
|
foreach ($row in $rows) {
|
|
foreach ($column in 'User', 'Username', 'SamAccountName', 'Login') {
|
|
if ($row.PSObject.Properties.Name -contains $column) {
|
|
$value = [string]$row.$column
|
|
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
|
$collected.Add($value.Trim())
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
Get-Content -LiteralPath $resolved.Path -Encoding UTF8 | ForEach-Object {
|
|
$line = $_.Trim()
|
|
if ($line -and -not $line.StartsWith('#')) {
|
|
$collected.Add($line)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$normalized = @($collected |
|
|
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
|
ForEach-Object { Normalize-ActivityWatchUserId -UserId $_ -Domain $Domain } |
|
|
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
|
Sort-Object -Unique)
|
|
|
|
if (-not $normalized -or $normalized.Count -eq 0) {
|
|
throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.'
|
|
}
|
|
|
|
return @($normalized)
|
|
}
|
|
|
|
function Get-ActivityWatchTaskNameToken {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$UserId
|
|
)
|
|
|
|
$buffer = [Text.StringBuilder]::new()
|
|
foreach ($character in $UserId.ToCharArray()) {
|
|
if ([char]::IsLetterOrDigit($character)) {
|
|
[void]$buffer.Append($character)
|
|
}
|
|
else {
|
|
[void]$buffer.Append('_')
|
|
}
|
|
}
|
|
|
|
return $buffer.ToString().Trim('_')
|
|
}
|
|
|
|
function Test-ActivityWatchScheduledTaskExistsExact {
|
|
param([string]$TaskName)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($TaskName)) {
|
|
return $false
|
|
}
|
|
|
|
try {
|
|
& schtasks.exe /Query /TN $TaskName *> $null
|
|
return ($LASTEXITCODE -eq 0)
|
|
}
|
|
catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function New-ActivityWatchUserTaskDefinitions {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]]$Users
|
|
)
|
|
|
|
$result = foreach ($user in $Users) {
|
|
$normalizedUser = Normalize-ActivityWatchUserId -UserId $user
|
|
$token = Get-ActivityWatchTaskNameToken -UserId $normalizedUser
|
|
[pscustomobject]@{
|
|
UserId = $normalizedUser
|
|
LaunchTaskName = "ActivityWatch Launch [$token]"
|
|
}
|
|
}
|
|
|
|
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 Get-ActivityWatchSessionRecords {
|
|
$sessions = New-Object System.Collections.Generic.List[object]
|
|
|
|
try {
|
|
$lines = & qwinsta.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 '^(SESSIONNAME|ИМЯ СЕАНСА)\s+') {
|
|
continue
|
|
}
|
|
|
|
$columns = @(
|
|
(($normalized -replace '\s{2,}', '|') -split '\|') |
|
|
ForEach-Object { $_.Trim() } |
|
|
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
|
)
|
|
if ($columns.Count -lt 3) {
|
|
continue
|
|
}
|
|
|
|
$sessionName = [string]$columns[0]
|
|
$userName = $null
|
|
$sessionIdIndex = 1
|
|
|
|
if ($columns[1] -notmatch '^\d+$') {
|
|
$userName = [string]$columns[1]
|
|
$sessionIdIndex = 2
|
|
}
|
|
|
|
if ($columns.Count -le $sessionIdIndex -or $columns[$sessionIdIndex] -notmatch '^\d+$') {
|
|
continue
|
|
}
|
|
|
|
$sessionId = [int]$columns[$sessionIdIndex]
|
|
$state = if ($columns.Count -gt ($sessionIdIndex + 1)) { [string]$columns[$sessionIdIndex + 1] } else { '' }
|
|
$isLive = $state -match '^(Active|Conn|Активно|Подкл\w*)$'
|
|
|
|
$sessions.Add([pscustomobject]@{
|
|
SessionName = $sessionName
|
|
UserName = $userName
|
|
SessionId = $sessionId
|
|
State = $state
|
|
IsLive = $isLive
|
|
}) | Out-Null
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
|
|
$explorerUsers = Get-ActivityWatchExplorerUsersBySession
|
|
foreach ($session in @($sessions.ToArray())) {
|
|
$sessionId = [int]$session.SessionId
|
|
if ($explorerUsers.ContainsKey($sessionId)) {
|
|
$session.UserName = [string]$explorerUsers[$sessionId]
|
|
}
|
|
}
|
|
|
|
return @($sessions.ToArray())
|
|
}
|
|
|
|
function Resolve-ActivityWatchUserCandidates {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$UserId
|
|
)
|
|
|
|
$normalizedUserId = Normalize-ActivityWatchUserId -UserId $UserId
|
|
$candidateIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
|
[void]$candidateIds.Add($normalizedUserId)
|
|
|
|
$leafUser = $normalizedUserId
|
|
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))
|
|
}
|
|
|
|
return @($candidateIds)
|
|
}
|
|
|
|
function Test-ActivityWatchUserHasSession {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$UserId,
|
|
[string[]]$LoggedOnUsers
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($UserId)) {
|
|
return $false
|
|
}
|
|
|
|
foreach ($candidate in @(Resolve-ActivityWatchUserCandidates -UserId $UserId)) {
|
|
if ($LoggedOnUsers -contains $candidate) {
|
|
return $true
|
|
}
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
function Test-ActivityWatchUserHasLiveSession {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$UserId,
|
|
[object[]]$SessionRecords
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($UserId)) {
|
|
return $false
|
|
}
|
|
|
|
foreach ($candidate in @(Resolve-ActivityWatchUserCandidates -UserId $UserId)) {
|
|
if (@($SessionRecords | Where-Object {
|
|
$_.IsLive -and
|
|
-not [string]::IsNullOrWhiteSpace([string]$_.UserName) -and
|
|
(
|
|
[string]$_.UserName -ieq $candidate -or
|
|
('{0}\{1}' -f $env:COMPUTERNAME, [string]$_.UserName) -ieq $candidate -or
|
|
((-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) -and ('{0}\{1}' -f $env:USERDOMAIN, [string]$_.UserName) -ieq $candidate)
|
|
)
|
|
}).Count -gt 0) {
|
|
return $true
|
|
}
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
function Test-ActivityWatchSessionMatchesUserId {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[object]$SessionRecord,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$UserId
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($UserId) -or $null -eq $SessionRecord) {
|
|
return $false
|
|
}
|
|
|
|
$sessionUser = [string]$SessionRecord.UserName
|
|
if ([string]::IsNullOrWhiteSpace($sessionUser)) {
|
|
return $false
|
|
}
|
|
|
|
foreach ($candidate in @(Resolve-ActivityWatchUserCandidates -UserId $UserId)) {
|
|
if ($sessionUser -ieq $candidate -or
|
|
('{0}\{1}' -f $env:COMPUTERNAME, $sessionUser) -ieq $candidate -or
|
|
((-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) -and ('{0}\{1}' -f $env:USERDOMAIN, $sessionUser) -ieq $candidate)) {
|
|
return $true
|
|
}
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
function Test-ActivityWatchUserHasManagedSession {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$UserId,
|
|
[object[]]$SessionRecords,
|
|
[switch]$IncludeLive,
|
|
[switch]$IncludeDisconnected
|
|
)
|
|
|
|
foreach ($session in @($SessionRecords)) {
|
|
if ([int]$session.SessionId -le 0) {
|
|
continue
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace([string]$session.UserName)) {
|
|
continue
|
|
}
|
|
if ([bool]$session.IsLive -and -not $IncludeLive.IsPresent) {
|
|
continue
|
|
}
|
|
if (-not [bool]$session.IsLive -and -not $IncludeDisconnected.IsPresent) {
|
|
continue
|
|
}
|
|
if (Test-ActivityWatchSessionMatchesUserId -SessionRecord $session -UserId $UserId) {
|
|
return $true
|
|
}
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
function Get-ActivityWatchManagedInteractiveSessions {
|
|
param(
|
|
[pscustomobject[]]$TaskDefinitions,
|
|
[object[]]$SessionRecords,
|
|
[switch]$IncludeLive,
|
|
[switch]$IncludeDisconnected
|
|
)
|
|
|
|
$result = New-Object System.Collections.Generic.List[object]
|
|
$seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
|
|
|
foreach ($taskDef in @($TaskDefinitions)) {
|
|
$userId = [string]$taskDef.userId
|
|
$taskName = [string]$taskDef.taskName
|
|
if ([string]::IsNullOrWhiteSpace($userId) -or [string]::IsNullOrWhiteSpace($taskName)) {
|
|
continue
|
|
}
|
|
|
|
foreach ($session in @($SessionRecords)) {
|
|
if ([int]$session.SessionId -le 0) {
|
|
continue
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace([string]$session.UserName)) {
|
|
continue
|
|
}
|
|
if ([bool]$session.IsLive -and -not $IncludeLive.IsPresent) {
|
|
continue
|
|
}
|
|
if (-not [bool]$session.IsLive -and -not $IncludeDisconnected.IsPresent) {
|
|
continue
|
|
}
|
|
if (-not (Test-ActivityWatchSessionMatchesUserId -SessionRecord $session -UserId $userId)) {
|
|
continue
|
|
}
|
|
|
|
$key = '{0}|{1}|{2}' -f $taskName, [int]$session.SessionId, $userId
|
|
if (-not $seen.Add($key)) {
|
|
continue
|
|
}
|
|
|
|
$result.Add([pscustomobject]@{
|
|
TaskName = $taskName
|
|
UserId = $userId
|
|
SessionName = [string]$session.SessionName
|
|
SessionId = [int]$session.SessionId
|
|
State = [string]$session.State
|
|
UserName = [string]$session.UserName
|
|
IsLive = [bool]$session.IsLive
|
|
}) | Out-Null
|
|
}
|
|
}
|
|
|
|
return @($result.ToArray())
|
|
}
|
|
|
|
function Copy-ActivityWatchCollectorAssets {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$CollectorScriptSource,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$EndpointCollectorScriptSource,
|
|
[string]$PolicyClientScriptSource,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$FileCollectorScriptSource,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$SessionCollectorScriptSource,
|
|
[string]$EvtxExportScriptSource,
|
|
[string]$HayabusaUploadScriptSource,
|
|
[string]$File1CTelemetryScriptSource,
|
|
[string]$EmailCollectorScriptSource,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ExampleRulesSource,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ExamplePolicySource,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$StateRoot,
|
|
[string]$CustomRulesSource,
|
|
[string]$CustomPolicySource
|
|
)
|
|
|
|
New-ActivityWatchDirectory -Path $StateRoot
|
|
|
|
$collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1'
|
|
$endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1'
|
|
$policyClientTarget = Join-Path $StateRoot 'dlp-policy-client.ps1'
|
|
$fileCollectorTarget = Join-Path $StateRoot 'file-operations-collector.ps1'
|
|
$sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1'
|
|
$evtxExportTarget = Join-Path $StateRoot 'export-evtx-for-hayabusa.ps1'
|
|
$hayabusaUploadTarget = Join-Path $StateRoot 'export-upload-hayabusa-to-aw-server.ps1'
|
|
$file1cTelemetryTarget = Join-Path $StateRoot 'export-upload-file-1c-telemetry.ps1'
|
|
$emailCollectorTarget = Join-Path $StateRoot 'email-outbound-collector.ps1'
|
|
$exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json'
|
|
$rulesTarget = Join-Path $StateRoot 'web-category-rules.json'
|
|
$examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json'
|
|
$policyTarget = Join-Path $StateRoot 'dlp-policy.json'
|
|
|
|
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
|
|
}
|
|
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
|
|
}
|
|
if ($HayabusaUploadScriptSource -and (Test-Path -LiteralPath $HayabusaUploadScriptSource)) {
|
|
Copy-Item -LiteralPath $HayabusaUploadScriptSource -Destination $hayabusaUploadTarget -Force
|
|
}
|
|
if ($File1CTelemetryScriptSource -and (Test-Path -LiteralPath $File1CTelemetryScriptSource)) {
|
|
Copy-Item -LiteralPath $File1CTelemetryScriptSource -Destination $file1cTelemetryTarget -Force
|
|
}
|
|
if ($EmailCollectorScriptSource -and (Test-Path -LiteralPath $EmailCollectorScriptSource)) {
|
|
Copy-Item -LiteralPath $EmailCollectorScriptSource -Destination $emailCollectorTarget -Force
|
|
}
|
|
Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force
|
|
Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force
|
|
|
|
if ($CustomRulesSource) {
|
|
$resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop
|
|
Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force
|
|
}
|
|
else {
|
|
Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force
|
|
}
|
|
|
|
if ($CustomPolicySource) {
|
|
$resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop
|
|
Copy-Item -LiteralPath $resolvedPolicy.Path -Destination $policyTarget -Force
|
|
}
|
|
else {
|
|
Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force
|
|
}
|
|
|
|
$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 = $effectiveCollectorTarget
|
|
EndpointCollectorScript = $effectiveEndpointCollectorTarget
|
|
PolicyClientScript = $policyClientTarget
|
|
FileCollectorScript = $effectiveFileCollectorTarget
|
|
SessionCollectorScript = $sessionCollectorTarget
|
|
EvtxExportScript = $evtxExportTarget
|
|
HayabusaUploadScript = $hayabusaUploadTarget
|
|
File1CTelemetryScript = $file1cTelemetryTarget
|
|
EmailCollectorScript = $emailCollectorTarget
|
|
ExampleRules = $exampleRulesTarget
|
|
ActiveRules = $rulesTarget
|
|
ExamplePolicy = $examplePolicyTarget
|
|
ActivePolicy = $policyTarget
|
|
}
|
|
}
|
|
|
|
function New-ActivityWatchDeploymentConfig {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ServerHost,
|
|
[Parameter(Mandatory = $true)]
|
|
[int]$ServerPort,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ServerScheme,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$InstallRoot,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$StateRoot,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$LogsRoot,
|
|
[Parameter(Mandatory = $true)]
|
|
[AllowEmptyString()]
|
|
[string]$CollectorScript,
|
|
[Parameter(Mandatory = $true)]
|
|
[AllowEmptyString()]
|
|
[string]$EndpointCollectorScript,
|
|
[string]$PolicyClientScript,
|
|
[Parameter(Mandatory = $true)]
|
|
[AllowEmptyString()]
|
|
[string]$FileCollectorScript,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$SessionCollectorScript,
|
|
[string]$EvtxExportScript,
|
|
[string]$HayabusaUploadScript,
|
|
[string]$File1CTelemetryScript,
|
|
[string]$EmailCollectorScript,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RulesPath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$PolicyPath,
|
|
[Parameter(Mandatory = $true)]
|
|
[int]$PollSeconds,
|
|
[Parameter(Mandatory = $true)]
|
|
[int]$PulseSeconds,
|
|
[Parameter(Mandatory = $true)]
|
|
[int]$RecoveryIntervalSeconds,
|
|
[bool]$AfkEnabled = $true,
|
|
[bool]$WindowEnabled = $true,
|
|
[bool]$FileOpsEnabled = $true,
|
|
[bool]$LocalAgentLogsEnabled = $true,
|
|
[bool]$IncidentCaptureEnabled = $true,
|
|
[bool]$IncidentScreenshotEnabled = $true,
|
|
[string]$IncidentArtifactsRoot,
|
|
[string]$EvtxExportRoot,
|
|
[int]$EvtxRetentionDays = 14,
|
|
[string[]]$EvtxChannels = @(),
|
|
[bool]$LogonMarkerEnabled = $true,
|
|
[bool]$ProcessEventsEnabled = $false,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$LaunchScriptPath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RecoveryScriptPath,
|
|
[string]$AwHostname,
|
|
[ValidateSet('local', 'server')]
|
|
[string]$PolicyMode = 'local',
|
|
[bool]$PolicyEngineEnabled = $false,
|
|
[string]$PolicyEngineHost,
|
|
[int]$PolicyEnginePort = 5601,
|
|
[ValidateSet('http', 'https')]
|
|
[string]$PolicyEngineScheme = 'http',
|
|
[int]$PolicyRefreshSeconds = 300,
|
|
[string]$PolicyCachePath,
|
|
[Parameter(Mandatory = $true)]
|
|
[pscustomobject[]]$UserTasks,
|
|
[string]$PackageVersion = 'v0.13.2',
|
|
[bool]$HayabusaAutoUploadEnabled = $true,
|
|
[int]$HayabusaAutoUploadIntervalHours = 6,
|
|
[int]$HayabusaAutoUploadHoursBack = 6,
|
|
[string]$HayabusaAutoUploadMode = 'incident',
|
|
[string]$HayabusaAutoUploadTaskName = 'ActivityWatch Hayabusa Upload',
|
|
[bool]$File1CAutoUploadEnabled = $true,
|
|
[int]$File1CAutoUploadIntervalHours = 6,
|
|
[int]$File1CAutoUploadIntervalMinutes = 15,
|
|
[string]$File1CAutoUploadTaskName = 'ActivityWatch File1C Upload',
|
|
[string]$File1CAutoUploadRunAsUser,
|
|
[string]$File1CTargetHost,
|
|
[string]$File1CTargetUser = 'igor',
|
|
[string]$File1CRemoteRoot = '/opt/activitywatch/clickhouse-1c/landing',
|
|
[string]$File1CRegistryWorkbookPath = 'E:\USER1\СПИСОК ПРЕДПРИЯТИЙ И ИХ РАСПРЕДЕЛЕНИЕ.xlsx',
|
|
[switch]$IntegrationTestEnabled
|
|
)
|
|
|
|
$effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' }
|
|
$effectiveEvtxExportRoot = if ($EvtxExportRoot) { $EvtxExportRoot } else { Join-Path $StateRoot 'forensics\evtx-exports' }
|
|
$effectiveEvtxChannels = if ($EvtxChannels -and $EvtxChannels.Count -gt 0) {
|
|
@($EvtxChannels)
|
|
} else {
|
|
@(
|
|
'Security',
|
|
'System',
|
|
'Application',
|
|
'Microsoft-Windows-PowerShell/Operational',
|
|
'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational',
|
|
'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational'
|
|
)
|
|
}
|
|
$effectivePolicyEngineHost = if ([string]::IsNullOrWhiteSpace($PolicyEngineHost)) { $ServerHost } else { $PolicyEngineHost }
|
|
$effectivePolicyCachePath = if ([string]::IsNullOrWhiteSpace($PolicyCachePath)) { Join-Path $StateRoot 'dlp-policy-cache.json' } else { $PolicyCachePath }
|
|
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
|
|
generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
|
|
awHostname = if ([string]::IsNullOrWhiteSpace($AwHostname)) { [string]$env:COMPUTERNAME } else { [string]$AwHostname }
|
|
server = [pscustomobject]@{
|
|
host = $ServerHost
|
|
port = $ServerPort
|
|
scheme = $ServerScheme
|
|
}
|
|
paths = [pscustomobject]@{
|
|
installRoot = $InstallRoot
|
|
stateRoot = $StateRoot
|
|
logsRoot = $LogsRoot
|
|
collectorScript = $CollectorScript
|
|
endpointCollectorScript = $EndpointCollectorScript
|
|
policyClientScript = $PolicyClientScript
|
|
emailCollectorScript = $EmailCollectorScript
|
|
fileCollectorScript = $FileCollectorScript
|
|
sessionCollectorScript = $SessionCollectorScript
|
|
evtxExportScript = $EvtxExportScript
|
|
hayabusaUploadScript = $HayabusaUploadScript
|
|
file1cTelemetryScript = $File1CTelemetryScript
|
|
file1cTelemetryExecutable = Join-Path $toolkitRoot 'aw-windows-telemetry.exe'
|
|
rulesPath = $RulesPath
|
|
policyPath = $PolicyPath
|
|
launchScript = $LaunchScriptPath
|
|
recoveryScript = $RecoveryScriptPath
|
|
}
|
|
collector = [pscustomobject]@{
|
|
pollSeconds = $PollSeconds
|
|
pulseSeconds = $PulseSeconds
|
|
}
|
|
collectors = [pscustomobject]@{
|
|
afkEnabled = $AfkEnabled
|
|
windowEnabled = $WindowEnabled
|
|
fileOpsEnabled = $FileOpsEnabled
|
|
emailEnabled = $false
|
|
browserCollectorMode = 'rust_primary'
|
|
dlpEndpointMode = 'rust_primary'
|
|
fileOpsMode = 'rust_primary'
|
|
worktimeSessionEnabled = $true
|
|
worktimeSessionMode = 'powershell_primary'
|
|
worktimeLegacyFallbackEnabled = $true
|
|
}
|
|
logging = [pscustomobject]@{
|
|
localAgentLogsEnabled = $LocalAgentLogsEnabled
|
|
}
|
|
incidentCapture = [pscustomobject]@{
|
|
enabled = $IncidentCaptureEnabled
|
|
screenshotEnabled = $IncidentScreenshotEnabled
|
|
artifactsRoot = $effectiveIncidentArtifactsRoot
|
|
}
|
|
forensics = [pscustomobject]@{
|
|
evtxExportRoot = $effectiveEvtxExportRoot
|
|
retentionDays = $EvtxRetentionDays
|
|
evtxChannels = @($effectiveEvtxChannels)
|
|
hayabusaAutomation = [pscustomobject]@{
|
|
enabled = [bool]$HayabusaAutoUploadEnabled
|
|
intervalHours = $HayabusaAutoUploadIntervalHours
|
|
hoursBack = $HayabusaAutoUploadHoursBack
|
|
mode = $HayabusaAutoUploadMode
|
|
taskName = $HayabusaAutoUploadTaskName
|
|
}
|
|
}
|
|
analytics = [pscustomobject]@{
|
|
file1cAutomation = [pscustomobject]@{
|
|
enabled = [bool]$File1CAutoUploadEnabled
|
|
intervalMinutes = $File1CAutoUploadIntervalMinutes
|
|
intervalHours = $File1CAutoUploadIntervalHours
|
|
taskName = $File1CAutoUploadTaskName
|
|
runAsUser = $File1CAutoUploadRunAsUser
|
|
targetHost = $File1CTargetHost
|
|
targetUser = $File1CTargetUser
|
|
remoteRoot = $File1CRemoteRoot
|
|
registryWorkbookPath = $File1CRegistryWorkbookPath
|
|
}
|
|
}
|
|
sessionEvents = [pscustomobject]@{
|
|
logonEnabled = $LogonMarkerEnabled
|
|
processEventsEnabled = $ProcessEventsEnabled
|
|
bucketPrefix = 'aw-session-events'
|
|
}
|
|
recovery = [pscustomobject]@{
|
|
intervalSeconds = $RecoveryIntervalSeconds
|
|
taskName = 'ActivityWatch Recovery'
|
|
}
|
|
dlp = [pscustomobject]@{
|
|
incidentBucketPrefix = 'aw-dlp-incidents'
|
|
enabled = $true
|
|
}
|
|
policyEngine = [pscustomobject]@{
|
|
enabled = $PolicyEngineEnabled
|
|
mode = $PolicyMode
|
|
host = $effectivePolicyEngineHost
|
|
port = $PolicyEnginePort
|
|
scheme = $PolicyEngineScheme
|
|
refreshSeconds = $PolicyRefreshSeconds
|
|
cachePath = $effectivePolicyCachePath
|
|
}
|
|
package = [pscustomobject]@{
|
|
version = $PackageVersion
|
|
}
|
|
userTasks = @($UserTasks)
|
|
integrationTestEnabled = [bool]$IntegrationTestEnabled
|
|
}
|
|
}
|
|
|
|
function Write-ActivityWatchDeploymentConfig {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[pscustomobject]$Config,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path
|
|
)
|
|
|
|
$directory = Split-Path -Path $Path -Parent
|
|
if ($directory) {
|
|
New-ActivityWatchDirectory -Path $directory
|
|
}
|
|
|
|
$json = $Config | ConvertTo-Json -Depth 8
|
|
Set-Content -LiteralPath $Path -Value $json -Encoding UTF8
|
|
}
|
|
|
|
function Read-ActivityWatchDeploymentConfig {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
throw "Конфигурация развёртывания не найдена: $Path"
|
|
}
|
|
|
|
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
|
}
|
|
|
|
function Write-ActivityWatchLaunchScript {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ConfigPath
|
|
)
|
|
|
|
$content = @"
|
|
param(
|
|
[string]`$ConfigPath = '$ConfigPath'
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
`$ErrorActionPreference = 'Stop'
|
|
|
|
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
|
|
Add-Type -AssemblyName System.Net.Http
|
|
`$script:MaxCollectorPowerShellProcesses = 48
|
|
`$script:CollectorProcessSnapshotLoaded = `$false
|
|
`$script:CollectorProcessSnapshot = @()
|
|
|
|
function Get-DeploymentConfig {
|
|
param([string]`$Path)
|
|
return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json
|
|
}
|
|
|
|
function Test-ProcessInSession {
|
|
param(
|
|
[string]`$Name,
|
|
[int]`$SessionId
|
|
)
|
|
|
|
return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1)
|
|
}
|
|
|
|
function Get-CollectorProcessSnapshot {
|
|
if (`$script:CollectorProcessSnapshotLoaded) {
|
|
return @(`$script:CollectorProcessSnapshot)
|
|
}
|
|
|
|
`$script:CollectorProcessSnapshotLoaded = `$true
|
|
`$script:CollectorProcessSnapshot = @()
|
|
`$job = `$null
|
|
try {
|
|
`$job = Start-Job -ScriptBlock {
|
|
Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe' OR Name = 'pwsh.exe'" -ErrorAction SilentlyContinue |
|
|
Where-Object {
|
|
`$_.CommandLine -match 'AWatch-rus' -and
|
|
`$_.CommandLine -match '\.ps1'
|
|
} |
|
|
Select-Object ProcessId, SessionId, CommandLine
|
|
}
|
|
|
|
if (Wait-Job -Job `$job -Timeout 4) {
|
|
`$script:CollectorProcessSnapshot = @(Receive-Job -Job `$job -ErrorAction SilentlyContinue)
|
|
}
|
|
}
|
|
catch {
|
|
`$script:CollectorProcessSnapshot = @()
|
|
}
|
|
finally {
|
|
if (`$job) {
|
|
Stop-Job -Job `$job -ErrorAction SilentlyContinue | Out-Null
|
|
Remove-Job -Job `$job -Force -ErrorAction SilentlyContinue | Out-Null
|
|
}
|
|
}
|
|
|
|
return @(`$script:CollectorProcessSnapshot)
|
|
}
|
|
|
|
function Test-CollectorRunning {
|
|
param(
|
|
[string]`$ScriptPath,
|
|
[int]`$SessionId
|
|
)
|
|
|
|
`$escapedCollector = [Regex]::Escape(`$ScriptPath)
|
|
`$processes = Get-CollectorProcessSnapshot |
|
|
Where-Object {
|
|
`$_.SessionId -eq `$SessionId -and
|
|
`$_.CommandLine -match `$escapedCollector
|
|
}
|
|
|
|
return [bool](`$processes | Select-Object -First 1)
|
|
}
|
|
|
|
function Get-CollectorPowerShellProcessCount {
|
|
return @(Get-CollectorProcessSnapshot).Count
|
|
}
|
|
|
|
function New-LaunchLock {
|
|
param([string]`$StateRoot, [int]`$SessionId)
|
|
|
|
`$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId)
|
|
if (Test-Path -LiteralPath `$lockPath) {
|
|
try {
|
|
`$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json
|
|
`$existingPid = [int]`$lockData.pid
|
|
if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) {
|
|
return `$null
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
|
|
`$payload = @{
|
|
pid = `$PID
|
|
sessionId = `$SessionId
|
|
createdAt = (Get-Date).ToUniversalTime().ToString('o')
|
|
} | ConvertTo-Json -Compress
|
|
Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8
|
|
return `$lockPath
|
|
}
|
|
|
|
function Get-SessionMarkerToken {
|
|
param([int]`$SessionId)
|
|
|
|
try {
|
|
`$explorer = Get-Process -Name 'explorer' -ErrorAction SilentlyContinue |
|
|
Where-Object { `$_.SessionId -eq `$SessionId } |
|
|
Sort-Object StartTime |
|
|
Select-Object -First 1
|
|
if (`$explorer -and `$explorer.StartTime) {
|
|
return `$explorer.StartTime.ToUniversalTime().ToString('yyyyMMddTHHmmssZ')
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
|
|
try {
|
|
`$currentProcess = Get-Process -Id `$PID -ErrorAction Stop
|
|
if (`$currentProcess.StartTime) {
|
|
return `$currentProcess.StartTime.ToUniversalTime().ToString('yyyyMMddTHHmmssZ')
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
|
|
return [string]`$SessionId
|
|
}
|
|
|
|
function Invoke-AwJsonPost {
|
|
param(
|
|
[Parameter(Mandatory = `$true)][string]`$Uri,
|
|
[Parameter(Mandatory = `$true)][string]`$Json
|
|
)
|
|
|
|
`$httpClient = New-Object System.Net.Http.HttpClient
|
|
try {
|
|
`$content = New-Object System.Net.Http.StringContent(`$Json, [System.Text.Encoding]::UTF8, 'application/json')
|
|
`$response = `$httpClient.PostAsync(`$Uri, `$content).Result
|
|
if (-not `$response.IsSuccessStatusCode) {
|
|
return `$false
|
|
}
|
|
return `$true
|
|
}
|
|
catch {
|
|
return `$false
|
|
}
|
|
finally {
|
|
`$httpClient.Dispose()
|
|
}
|
|
}
|
|
|
|
function Ensure-Bucket {
|
|
param(
|
|
[string]`$BucketId,
|
|
[string]`$ClientName,
|
|
[string]`$BucketType
|
|
)
|
|
|
|
if (`$script:KnownBuckets.ContainsKey(`$BucketId)) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null
|
|
`$script:KnownBuckets[`$BucketId] = `$true
|
|
return
|
|
}
|
|
catch {
|
|
}
|
|
|
|
`$body = @{
|
|
client = `$ClientName
|
|
type = `$BucketType
|
|
hostname = `$script:Hostname
|
|
} | ConvertTo-Json -Compress
|
|
|
|
try {
|
|
if (-not (Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body)) {
|
|
return
|
|
}
|
|
}
|
|
catch {
|
|
try {
|
|
Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null
|
|
}
|
|
catch {
|
|
return
|
|
}
|
|
}
|
|
|
|
`$script:KnownBuckets[`$BucketId] = `$true
|
|
}
|
|
|
|
function Send-LogonMarkerIfNeeded {
|
|
param(
|
|
[pscustomobject]`$Config,
|
|
[int]`$SessionId
|
|
)
|
|
|
|
`$sessionEvents = if (`$Config.PSObject.Properties.Name -contains 'sessionEvents') { `$Config.sessionEvents } else { `$null }
|
|
`$logging = if (`$Config.PSObject.Properties.Name -contains 'logging') { `$Config.logging } else { `$null }
|
|
`$logonEnabled = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]`$sessionEvents.logonEnabled } else { `$false }
|
|
if (-not `$logonEnabled) {
|
|
return
|
|
}
|
|
|
|
`$bucketPrefix = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]`$sessionEvents.bucketPrefix)) {
|
|
[string]`$sessionEvents.bucketPrefix
|
|
}
|
|
else {
|
|
'aw-session-events'
|
|
}
|
|
|
|
`$stateRoot = [string]`$Config.paths.stateRoot
|
|
`$markerRoots = New-Object System.Collections.Generic.List[string]
|
|
if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) {
|
|
`$markerRoots.Add((Join-Path `$stateRoot 'markers'))
|
|
}
|
|
if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) {
|
|
`$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers'))
|
|
}
|
|
|
|
`$markerDir = `$null
|
|
foreach (`$candidate in `$markerRoots) {
|
|
try {
|
|
if (-not (Test-Path -LiteralPath `$candidate)) {
|
|
New-Item -Path `$candidate -ItemType Directory -Force | Out-Null
|
|
}
|
|
|
|
`$probePath = Join-Path `$candidate 'write-test.tmp'
|
|
Set-Content -LiteralPath `$probePath -Value 'ok' -Encoding ASCII
|
|
Remove-Item -LiteralPath `$probePath -Force -ErrorAction SilentlyContinue
|
|
`$markerDir = `$candidate
|
|
break
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
|
|
if (-not `$markerDir) {
|
|
return
|
|
}
|
|
|
|
`$sessionMarkerToken = Get-SessionMarkerToken -SessionId `$SessionId
|
|
`$markerFile = Join-Path `$markerDir ("logon-{0}-{1}-{2}.marker" -f `$env:USERNAME, `$SessionId, `$sessionMarkerToken)
|
|
if (Test-Path -LiteralPath `$markerFile) {
|
|
return
|
|
}
|
|
|
|
Set-Content -LiteralPath `$markerFile -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8
|
|
|
|
`$bucketId = ('{0}_{1}' -f `$bucketPrefix, `$script:Hostname)
|
|
Ensure-Bucket -BucketId `$bucketId -ClientName 'aw-session-events' -BucketType 'aw.session.event'
|
|
|
|
`$payload = @{
|
|
timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
|
duration = 0
|
|
data = @{
|
|
eventType = 'logon'
|
|
username = `$env:USERNAME
|
|
userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)"
|
|
sessionId = `$SessionId
|
|
hostname = `$script:Hostname
|
|
source = 'launch-watchers-awatch-rus'
|
|
}
|
|
} | ConvertTo-Json -Depth 5 -Compress
|
|
|
|
try {
|
|
Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$bucketId/heartbeat?pulsetime=1" -Json `$payload
|
|
}
|
|
catch {
|
|
Remove-Item -LiteralPath `$markerFile -Force -ErrorAction SilentlyContinue
|
|
throw
|
|
}
|
|
}
|
|
|
|
function Start-CollectorScriptIfNeeded {
|
|
param(
|
|
[string]`$ScriptPath,
|
|
[string]`$ConfigPath,
|
|
[string]`$PowerShellExe,
|
|
[int]`$SessionId
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) {
|
|
return
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath `$ScriptPath)) {
|
|
return
|
|
}
|
|
|
|
if (Test-CollectorRunning -ScriptPath `$ScriptPath -SessionId `$SessionId) {
|
|
return
|
|
}
|
|
|
|
if ((Get-CollectorPowerShellProcessCount) -ge `$script:MaxCollectorPowerShellProcesses) {
|
|
return
|
|
}
|
|
|
|
`$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null }
|
|
`$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass')
|
|
if (`$staParam) { `$argumentList += `$staParam }
|
|
`$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath)
|
|
Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden
|
|
}
|
|
|
|
function Test-RustCollectorRunning {
|
|
param(
|
|
[string]`$Subcommand,
|
|
[int]`$SessionId
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace(`$Subcommand)) {
|
|
return `$false
|
|
}
|
|
|
|
return [bool]@(
|
|
Get-CimInstance Win32_Process -Filter "Name = 'aw-windows-telemetry.exe'" -ErrorAction SilentlyContinue |
|
|
Where-Object {
|
|
`$_.SessionId -eq `$SessionId -and
|
|
`$_.CommandLine -match [Regex]::Escape(`$Subcommand)
|
|
} |
|
|
Select-Object -First 1
|
|
).Count
|
|
}
|
|
|
|
function Start-RustCollectorIfNeeded {
|
|
param(
|
|
[string]`$ExePath,
|
|
[string]`$Subcommand,
|
|
[string]`$ConfigPath,
|
|
[int]`$SessionId
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace(`$ExePath) -or [string]::IsNullOrWhiteSpace(`$Subcommand)) {
|
|
return `$false
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath `$ExePath)) {
|
|
return `$false
|
|
}
|
|
|
|
if (Test-RustCollectorRunning -Subcommand `$Subcommand -SessionId `$SessionId) {
|
|
return `$true
|
|
}
|
|
|
|
`$argumentList = @(`$Subcommand, '--config-path', `$ConfigPath, '--mode', 'enforce')
|
|
Start-Process -FilePath `$ExePath -ArgumentList `$argumentList -WindowStyle Hidden
|
|
return `$true
|
|
}
|
|
|
|
`$config = Get-DeploymentConfig -Path `$ConfigPath
|
|
`$sessionId = (Get-Process -Id `$PID).SessionId
|
|
`$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 = @{}
|
|
`$telemetryExe = if (`$config.paths.PSObject.Properties.Name -contains 'file1cTelemetryExecutable' -and -not [string]::IsNullOrWhiteSpace([string]`$config.paths.file1cTelemetryExecutable)) { [string]`$config.paths.file1cTelemetryExecutable } else { Join-Path `$deployRoot 'windows\aw-windows-telemetry.exe' }
|
|
`$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' }
|
|
`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe'
|
|
`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe'
|
|
`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port)
|
|
`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true }
|
|
`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true }
|
|
`$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 { '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
|
|
if (-not `$launchLockPath) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) {
|
|
throw "Не найден aw-watcher-afk.exe: `$afkExe"
|
|
}
|
|
|
|
if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) {
|
|
throw "Не найден aw-watcher-window.exe: `$windowExe"
|
|
}
|
|
|
|
if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) {
|
|
Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden
|
|
}
|
|
|
|
if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) {
|
|
Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden
|
|
}
|
|
|
|
try {
|
|
Send-LogonMarkerIfNeeded -Config `$config -SessionId `$sessionId
|
|
}
|
|
catch {
|
|
}
|
|
if (`$browserCollectorMode -ieq 'rust_primary') {
|
|
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') {
|
|
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') {
|
|
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
|
|
}
|
|
}
|
|
if (`$emailEnabled -and (Test-Path -LiteralPath `$emailCollectorScript)) {
|
|
Start-CollectorScriptIfNeeded -ScriptPath `$emailCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
|
|
}
|
|
}
|
|
finally {
|
|
if (`$launchLockPath -and (Test-Path -LiteralPath `$launchLockPath)) {
|
|
Remove-Item -LiteralPath `$launchLockPath -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
"@
|
|
|
|
Set-Content -LiteralPath $Path -Value $content -Encoding UTF8
|
|
}
|
|
|
|
function Write-ActivityWatchRecoveryScript {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ConfigPath
|
|
)
|
|
|
|
$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1'
|
|
$content = @"
|
|
param(
|
|
[string]`$ConfigPath = '$ConfigPath'
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
`$ErrorActionPreference = 'Continue'
|
|
Import-Module '$modulePath' -Force
|
|
Invoke-ActivityWatchRecoveryLoop -ConfigPath `$ConfigPath
|
|
"@
|
|
|
|
Set-Content -LiteralPath $Path -Value $content -Encoding UTF8
|
|
}
|
|
|
|
function Get-ActivityWatchRecoveryConfigPaths {
|
|
param([string]$PrimaryConfigPath)
|
|
|
|
$paths = New-Object System.Collections.Generic.List[string]
|
|
if ($PrimaryConfigPath -and (Test-Path -LiteralPath $PrimaryConfigPath)) {
|
|
$paths.Add((Resolve-Path -LiteralPath $PrimaryConfigPath).Path)
|
|
}
|
|
|
|
$searchRoot = $env:ProgramData
|
|
if ($PrimaryConfigPath) {
|
|
$stateRoot = Split-Path -Path $PrimaryConfigPath -Parent
|
|
$candidateRoot = Split-Path -Path $stateRoot -Parent
|
|
if ($candidateRoot -and (Test-Path -LiteralPath $candidateRoot)) {
|
|
$searchRoot = $candidateRoot
|
|
}
|
|
}
|
|
|
|
if (Test-Path -LiteralPath $searchRoot) {
|
|
Get-ChildItem -LiteralPath $searchRoot -Directory -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Name -like 'ActivityWatch*' } |
|
|
ForEach-Object {
|
|
$candidate = Join-Path $_.FullName 'deployment-config.json'
|
|
if (Test-Path -LiteralPath $candidate) {
|
|
$paths.Add($candidate)
|
|
}
|
|
}
|
|
}
|
|
|
|
return @($paths | Sort-Object -Unique)
|
|
}
|
|
|
|
function Get-ActivityWatchRecoveryTaskDefinitions {
|
|
param([string[]]$ConfigPaths)
|
|
|
|
$taskMap = [ordered]@{}
|
|
foreach ($candidatePath in @($ConfigPaths)) {
|
|
try {
|
|
$config = Read-ActivityWatchDeploymentConfig -Path $candidatePath
|
|
foreach ($task in @($config.userTasks)) {
|
|
$taskName = [string]$task.launchTaskName
|
|
$userId = Normalize-ActivityWatchUserId -UserId ([string]$task.userId)
|
|
$canonicalTaskName = "ActivityWatch Launch [$((Get-ActivityWatchTaskNameToken -UserId $userId))]"
|
|
if ($canonicalTaskName -ne $taskName -and (Test-ActivityWatchScheduledTaskExistsExact -TaskName $canonicalTaskName)) {
|
|
$taskName = $canonicalTaskName
|
|
}
|
|
if (-not [string]::IsNullOrWhiteSpace($taskName) -and -not $taskMap.Contains($taskName)) {
|
|
$taskMap[$taskName] = [pscustomobject]@{
|
|
taskName = $taskName
|
|
userId = $userId
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
|
|
return @($taskMap.Values)
|
|
}
|
|
|
|
function New-ActivityWatchRecoveryLock {
|
|
param([string]$PrimaryConfigPath)
|
|
|
|
$stateRoot = if ($PrimaryConfigPath) { Split-Path -Path $PrimaryConfigPath -Parent } else { Join-Path $env:ProgramData 'AWatch-rus' }
|
|
if (-not (Test-Path -LiteralPath $stateRoot)) {
|
|
New-Item -Path $stateRoot -ItemType Directory -Force | Out-Null
|
|
}
|
|
|
|
$lockPath = Join-Path $stateRoot 'recovery-loop.lock'
|
|
if (Test-Path -LiteralPath $lockPath) {
|
|
try {
|
|
$lockData = Get-Content -LiteralPath $lockPath -Raw | ConvertFrom-Json
|
|
$existingPid = [int]$lockData.pid
|
|
if ($existingPid -gt 0 -and (Get-Process -Id $existingPid -ErrorAction SilentlyContinue)) {
|
|
return $null
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
|
|
$payload = @{
|
|
pid = $PID
|
|
createdAt = (Get-Date).ToUniversalTime().ToString('o')
|
|
} | ConvertTo-Json -Compress
|
|
Set-Content -LiteralPath $lockPath -Value $payload -Encoding UTF8
|
|
return $lockPath
|
|
}
|
|
|
|
function Test-ActivityWatchCollectorRunningGlobal {
|
|
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-ActivityWatchCollectorScriptGlobalIfNeeded {
|
|
param(
|
|
[string]$ScriptPath,
|
|
[string]$ConfigPath
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ScriptPath)) {
|
|
return
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $ScriptPath)) {
|
|
return
|
|
}
|
|
|
|
if (Test-ActivityWatchCollectorRunningGlobal -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
|
|
}
|
|
|
|
function Start-ActivityWatchTaskIfNotRunning {
|
|
param(
|
|
[string]$TaskName,
|
|
[string]$UserId,
|
|
[object[]]$SessionRecords
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($TaskName) -or [string]::IsNullOrWhiteSpace($UserId)) {
|
|
return $false
|
|
}
|
|
|
|
if (-not (Test-ActivityWatchUserHasLiveSession -UserId $UserId -SessionRecords $SessionRecords)) {
|
|
return $false
|
|
}
|
|
|
|
try {
|
|
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
if (-not $task) {
|
|
return $false
|
|
}
|
|
if ([string]$task.State -eq 'Running') {
|
|
return $true
|
|
}
|
|
Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
return $true
|
|
}
|
|
catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Get-ActivityWatchLiveInteractiveSessions {
|
|
param([object[]]$SessionRecords)
|
|
|
|
return @(
|
|
$SessionRecords |
|
|
Where-Object {
|
|
$_.IsLive -and
|
|
$_.SessionId -gt 0 -and
|
|
-not [string]::IsNullOrWhiteSpace([string]$_.UserName)
|
|
} |
|
|
Sort-Object @{ Expression = { if ([string]$_.SessionName -ieq 'console') { 0 } else { 1 } } }, @{ Expression = { [int]$_.SessionId } }
|
|
)
|
|
}
|
|
|
|
function Resolve-ActivityWatchLiveSessionUserId {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[pscustomobject]$SessionRecord,
|
|
[pscustomobject[]]$TaskDefinitions
|
|
)
|
|
|
|
$rawUser = [string]$SessionRecord.UserName
|
|
if ([string]::IsNullOrWhiteSpace($rawUser)) {
|
|
return $null
|
|
}
|
|
|
|
foreach ($taskDef in @($TaskDefinitions)) {
|
|
foreach ($candidate in @(Resolve-ActivityWatchUserCandidates -UserId [string]$taskDef.userId)) {
|
|
if ($candidate -ieq $rawUser -or
|
|
$candidate -ieq ('{0}\{1}' -f $env:COMPUTERNAME, $rawUser) -or
|
|
((-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) -and $candidate -ieq ('{0}\{1}' -f $env:USERDOMAIN, $rawUser))) {
|
|
return [string]$taskDef.userId
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($rawUser -match '^[^\\]+\\') {
|
|
return $rawUser
|
|
}
|
|
|
|
return ('{0}\{1}' -f $env:COMPUTERNAME, $rawUser)
|
|
}
|
|
|
|
function Get-ActivityWatchExplorerUsersBySession {
|
|
$map = @{}
|
|
|
|
try {
|
|
Get-Process explorer -IncludeUserName -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.SessionId -gt 0 -and -not [string]::IsNullOrWhiteSpace([string]$_.UserName) } |
|
|
Sort-Object SessionId, StartTime |
|
|
ForEach-Object {
|
|
if (-not $map.ContainsKey([int]$_.SessionId)) {
|
|
$map[[int]$_.SessionId] = [string]$_.UserName
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
|
|
return $map
|
|
}
|
|
|
|
function Get-ActivityWatchDisconnectedInteractiveSessions {
|
|
param([object[]]$SessionRecords)
|
|
|
|
$explorerUsers = Get-ActivityWatchExplorerUsersBySession
|
|
$result = New-Object System.Collections.Generic.List[object]
|
|
|
|
foreach ($session in @($SessionRecords | Where-Object { -not $_.IsLive -and $_.SessionId -gt 0 })) {
|
|
$resolvedUser = [string]$session.UserName
|
|
if ([string]::IsNullOrWhiteSpace($resolvedUser) -and $explorerUsers.ContainsKey([int]$session.SessionId)) {
|
|
$resolvedUser = [string]$explorerUsers[[int]$session.SessionId]
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($resolvedUser)) {
|
|
continue
|
|
}
|
|
|
|
$result.Add([pscustomobject]@{
|
|
SessionName = [string]$session.SessionName
|
|
SessionId = [int]$session.SessionId
|
|
State = [string]$session.State
|
|
UserName = $resolvedUser
|
|
}) | Out-Null
|
|
}
|
|
|
|
return @($result | Sort-Object SessionId -Unique)
|
|
}
|
|
|
|
function Get-ActivityWatchMarkerDirectories {
|
|
param([string]$StateRoot)
|
|
|
|
$roots = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($StateRoot)) {
|
|
[void]$roots.Add((Join-Path $StateRoot 'markers'))
|
|
}
|
|
|
|
$usersRoot = Join-Path $env:SystemDrive 'Users'
|
|
if (Test-Path -LiteralPath $usersRoot) {
|
|
foreach ($dir in @(Get-ChildItem -LiteralPath $usersRoot -Directory -ErrorAction SilentlyContinue)) {
|
|
[void]$roots.Add((Join-Path $dir.FullName 'AppData\Local\AWatch-rus\markers'))
|
|
}
|
|
}
|
|
|
|
return @($roots)
|
|
}
|
|
|
|
function Remove-ActivityWatchLogonMarkersForSession {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$StateRoot,
|
|
[int]$SessionId,
|
|
[string]$UserName
|
|
)
|
|
|
|
$userCandidates = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
|
if (-not [string]::IsNullOrWhiteSpace($UserName)) {
|
|
[void]$userCandidates.Add($UserName)
|
|
if ($UserName -match '^[^\\]+\\(.+)$') {
|
|
[void]$userCandidates.Add($Matches[1])
|
|
}
|
|
}
|
|
|
|
foreach ($markerDir in @(Get-ActivityWatchMarkerDirectories -StateRoot $StateRoot)) {
|
|
if (-not (Test-Path -LiteralPath $markerDir)) {
|
|
continue
|
|
}
|
|
|
|
foreach ($marker in @(Get-ChildItem -LiteralPath $markerDir -Filter '*.marker' -File -ErrorAction SilentlyContinue)) {
|
|
$name = [string]$marker.BaseName
|
|
if ($name -notmatch '^logon-(.+)-(\d+)-') {
|
|
continue
|
|
}
|
|
|
|
$markerUser = [string]$Matches[1]
|
|
$markerSessionId = [int]$Matches[2]
|
|
if ($markerSessionId -ne $SessionId) {
|
|
continue
|
|
}
|
|
|
|
if ($userCandidates.Count -gt 0 -and -not $userCandidates.Contains($markerUser)) {
|
|
continue
|
|
}
|
|
|
|
Remove-Item -LiteralPath $marker.FullName -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
|
|
function Stop-ActivityWatchProcessesInNonLiveSessions {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[object[]]$SessionRecords,
|
|
[Parameter(Mandatory = $true)]
|
|
[pscustomobject]$Config,
|
|
[pscustomobject[]]$TaskDefinitions = @(),
|
|
[switch]$PreserveManagedSessions
|
|
)
|
|
|
|
$stateRoot = if ($Config.paths.PSObject.Properties.Name -contains 'stateRoot') { [string]$Config.paths.stateRoot } else { Join-Path $env:ProgramData 'AWatch-rus' }
|
|
$preservedSessionIds = @()
|
|
if ($PreserveManagedSessions.IsPresent) {
|
|
$preservedSessionIds = @(
|
|
Get-ActivityWatchManagedInteractiveSessions -TaskDefinitions $TaskDefinitions -SessionRecords $SessionRecords -IncludeDisconnected |
|
|
ForEach-Object { [int]$_.SessionId } |
|
|
Sort-Object -Unique
|
|
)
|
|
}
|
|
|
|
$sessionIds = @(
|
|
$SessionRecords |
|
|
Where-Object { -not $_.IsLive -and $_.SessionId -gt 0 -and ($preservedSessionIds -notcontains [int]$_.SessionId) } |
|
|
ForEach-Object { [int]$_.SessionId } |
|
|
Sort-Object -Unique
|
|
)
|
|
|
|
if (-not $sessionIds -or $sessionIds.Count -eq 0) {
|
|
return
|
|
}
|
|
|
|
$sessionScopedScripts = New-Object System.Collections.Generic.List[string]
|
|
foreach ($propertyName in @('collectorScript', 'endpointCollectorScript', 'fileCollectorScript', 'emailCollectorScript', 'launchScript')) {
|
|
if ($Config.paths.PSObject.Properties.Name -contains $propertyName) {
|
|
$candidatePath = [string]$Config.paths.$propertyName
|
|
if (-not [string]::IsNullOrWhiteSpace($candidatePath)) {
|
|
$sessionScopedScripts.Add($candidatePath) | Out-Null
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($session in @($SessionRecords | Where-Object { -not $_.IsLive -and $_.SessionId -gt 0 -and ($preservedSessionIds -notcontains [int]$_.SessionId) })) {
|
|
Remove-ActivityWatchLogonMarkersForSession -StateRoot $stateRoot -SessionId ([int]$session.SessionId) -UserName ([string]$session.UserName)
|
|
}
|
|
|
|
Get-Process -Name 'aw-watcher-afk','aw-watcher-window' -ErrorAction SilentlyContinue |
|
|
Where-Object { $sessionIds -contains [int]$_.SessionId } |
|
|
ForEach-Object {
|
|
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
|
Where-Object {
|
|
($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and
|
|
($sessionIds -contains [int]$_.SessionId)
|
|
} |
|
|
ForEach-Object {
|
|
$commandLine = [string]$_.CommandLine
|
|
foreach ($scriptPath in $sessionScopedScripts) {
|
|
if (-not [string]::IsNullOrWhiteSpace($scriptPath) -and $commandLine -match [Regex]::Escape($scriptPath)) {
|
|
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Promote-ActivityWatchDisconnectedSessionToConsole {
|
|
param(
|
|
[pscustomobject[]]$TaskDefinitions,
|
|
[object[]]$SessionRecords
|
|
)
|
|
|
|
$candidates = Get-ActivityWatchDisconnectedInteractiveSessions -SessionRecords $SessionRecords
|
|
if (-not $candidates -or $candidates.Count -eq 0) {
|
|
return $false
|
|
}
|
|
|
|
$selected = $null
|
|
foreach ($taskDef in @($TaskDefinitions)) {
|
|
foreach ($candidate in @($candidates)) {
|
|
foreach ($knownUser in @(Resolve-ActivityWatchUserCandidates -UserId [string]$taskDef.userId)) {
|
|
if ($knownUser -ieq [string]$candidate.UserName -or
|
|
$knownUser -ieq ('{0}\{1}' -f $env:COMPUTERNAME, [string]$candidate.UserName) -or
|
|
((-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) -and $knownUser -ieq ('{0}\{1}' -f $env:USERDOMAIN, [string]$candidate.UserName))) {
|
|
$selected = $candidate
|
|
break
|
|
}
|
|
}
|
|
if ($selected) { break }
|
|
}
|
|
if ($selected) { break }
|
|
}
|
|
|
|
if (-not $selected) {
|
|
$selected = $candidates | Select-Object -First 1
|
|
}
|
|
|
|
if (-not $selected) {
|
|
return $false
|
|
}
|
|
|
|
try {
|
|
& cmd.exe /c ("tscon {0} /dest:console" -f [int]$selected.SessionId) | Out-Null
|
|
return ($LASTEXITCODE -eq 0)
|
|
}
|
|
catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Ensure-ActivityWatchLaunchTaskForUser {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$UserId,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$LaunchScriptPath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ConfigPath
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($UserId) -or -not (Test-Path -LiteralPath $LaunchScriptPath)) {
|
|
return $null
|
|
}
|
|
|
|
$taskName = "ActivityWatch Launch [$((Get-ActivityWatchTaskNameToken -UserId $UserId))]"
|
|
$launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath
|
|
Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath
|
|
|
|
$wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe'
|
|
$action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`""
|
|
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $UserId
|
|
$principal = New-ScheduledTaskPrincipal -UserId $UserId -LogonType Interactive -RunLevel Highest
|
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0)
|
|
|
|
try {
|
|
$existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
|
if ($existingTask) {
|
|
$existingUserId = [string]$existingTask.Principal.UserId
|
|
$existingArgs = @($existingTask.Actions | ForEach-Object { [string]$_.Arguments }) -join ' '
|
|
if ($existingUserId -ieq $UserId -and $existingArgs -like "*$launcherPath*") {
|
|
return $taskName
|
|
}
|
|
|
|
Remove-ActivityWatchScheduledTask -TaskName $taskName
|
|
}
|
|
|
|
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
|
|
return $taskName
|
|
}
|
|
catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Start-ActivityWatchConsoleFallbackIfNeeded {
|
|
param(
|
|
[pscustomobject[]]$TaskDefinitions,
|
|
[object[]]$SessionRecords,
|
|
[pscustomobject]$Config,
|
|
[string]$ConfigPath,
|
|
[bool]$ConfiguredLiveTasksStarted
|
|
)
|
|
|
|
if ($ConfiguredLiveTasksStarted) {
|
|
return
|
|
}
|
|
|
|
$liveSessions = Get-ActivityWatchLiveInteractiveSessions -SessionRecords $SessionRecords
|
|
if (-not $liveSessions -or $liveSessions.Count -eq 0) {
|
|
if (Promote-ActivityWatchDisconnectedSessionToConsole -TaskDefinitions $TaskDefinitions -SessionRecords $SessionRecords) {
|
|
Start-Sleep -Seconds 3
|
|
$SessionRecords = Get-ActivityWatchSessionRecords
|
|
$liveSessions = Get-ActivityWatchLiveInteractiveSessions -SessionRecords $SessionRecords
|
|
}
|
|
}
|
|
if (-not $liveSessions -or $liveSessions.Count -eq 0) {
|
|
return
|
|
}
|
|
|
|
$launchScriptPath = if ($Config.paths.PSObject.Properties.Name -contains 'launchScript') { [string]$Config.paths.launchScript } else { $null }
|
|
if ([string]::IsNullOrWhiteSpace($launchScriptPath)) {
|
|
return
|
|
}
|
|
|
|
$preferredSession = $liveSessions | Select-Object -First 1
|
|
$userId = Resolve-ActivityWatchLiveSessionUserId -SessionRecord $preferredSession -TaskDefinitions $TaskDefinitions
|
|
if ([string]::IsNullOrWhiteSpace($userId)) {
|
|
return
|
|
}
|
|
|
|
$taskName = Ensure-ActivityWatchLaunchTaskForUser -UserId $userId -LaunchScriptPath $launchScriptPath -ConfigPath $ConfigPath
|
|
if ([string]::IsNullOrWhiteSpace($taskName)) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
|
if ($task -and [string]$task.State -ne 'Running') {
|
|
Start-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
|
|
function Invoke-ActivityWatchRecoveryLoop {
|
|
param([string]$ConfigPath)
|
|
|
|
$recoveryLockPath = New-ActivityWatchRecoveryLock -PrimaryConfigPath $ConfigPath
|
|
if (-not $recoveryLockPath) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
while ($true) {
|
|
$sleepSeconds = 180
|
|
try {
|
|
$configPaths = Get-ActivityWatchRecoveryConfigPaths -PrimaryConfigPath $ConfigPath
|
|
$config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
|
|
$taskDefs = Get-ActivityWatchRecoveryTaskDefinitions -ConfigPaths $configPaths
|
|
$sessionRecords = Get-ActivityWatchSessionRecords
|
|
Stop-ActivityWatchProcessesInNonLiveSessions -SessionRecords $sessionRecords -Config $config -TaskDefinitions $taskDefs -PreserveManagedSessions
|
|
$sessionRecords = Get-ActivityWatchSessionRecords
|
|
$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' }
|
|
$worktimeSessionEnabled = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'worktimeSessionEnabled') { [bool]$config.collectors.worktimeSessionEnabled } else { $true }
|
|
$worktimeSessionMode = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'worktimeSessionMode') { [string]$config.collectors.worktimeSessionMode } else { 'powershell_primary' }
|
|
$worktimeLegacyFallbackEnabled = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'worktimeLegacyFallbackEnabled') { [bool]$config.collectors.worktimeLegacyFallbackEnabled } else { $true }
|
|
$rustAgentRunning = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { $_.Name -ieq 'awatch-agent-rs.exe' }).Count -gt 0
|
|
$allowPowerShellWorktime = $worktimeSessionEnabled -and ($worktimeSessionMode -ine 'rust_primary' -or ($worktimeLegacyFallbackEnabled -and -not $rustAgentRunning))
|
|
if ($allowPowerShellWorktime) {
|
|
Start-ActivityWatchCollectorScriptGlobalIfNeeded -ScriptPath $sessionCollectorScript -ConfigPath $ConfigPath
|
|
}
|
|
|
|
$configuredLiveTasksStarted = $false
|
|
foreach ($taskDef in $taskDefs) {
|
|
if (Start-ActivityWatchTaskIfNotRunning -TaskName $taskDef.taskName -UserId $taskDef.userId -SessionRecords $sessionRecords) {
|
|
$configuredLiveTasksStarted = $true
|
|
}
|
|
}
|
|
|
|
Start-ActivityWatchConsoleFallbackIfNeeded -TaskDefinitions $taskDefs -SessionRecords $sessionRecords -Config $config -ConfigPath $ConfigPath -ConfiguredLiveTasksStarted $configuredLiveTasksStarted
|
|
|
|
if ($config -and $config.recovery -and $config.recovery.intervalSeconds) {
|
|
$sleepSeconds = [Math]::Max([int]$config.recovery.intervalSeconds, 30)
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
|
|
Start-Sleep -Seconds $sleepSeconds
|
|
}
|
|
}
|
|
finally {
|
|
if ($recoveryLockPath -and (Test-Path -LiteralPath $recoveryLockPath)) {
|
|
Remove-Item -LiteralPath $recoveryLockPath -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-ActivityWatchHiddenLauncherPath {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ScriptPath
|
|
)
|
|
|
|
$directory = Split-Path -Path $ScriptPath -Parent
|
|
$baseName = [IO.Path]::GetFileNameWithoutExtension($ScriptPath)
|
|
return Join-Path $directory ("{0}-hidden.vbs" -f $baseName)
|
|
}
|
|
|
|
function Write-ActivityWatchHiddenPowerShellWrapper {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ScriptPath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ConfigPath
|
|
)
|
|
|
|
$directory = Split-Path -Path $Path -Parent
|
|
if ($directory) {
|
|
New-ActivityWatchDirectory -Path $directory
|
|
}
|
|
|
|
$powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
$escapedPowerShellExe = $powershellExe.Replace('"', '""')
|
|
$escapedScriptPath = $ScriptPath.Replace('"', '""')
|
|
$escapedConfigPath = $ConfigPath.Replace('"', '""')
|
|
|
|
$content = @"
|
|
On Error Resume Next
|
|
Set shell = CreateObject("WScript.Shell")
|
|
q = Chr(34)
|
|
command = q & "$escapedPowerShellExe" & q & " -NoProfile -ExecutionPolicy Bypass -File " & q & "$escapedScriptPath" & q & " -ConfigPath " & q & "$escapedConfigPath" & q
|
|
shell.Run command, 0, False
|
|
If Err.Number <> 0 Then
|
|
WScript.Quit 1
|
|
End If
|
|
WScript.Quit 0
|
|
"@
|
|
|
|
Set-Content -LiteralPath $Path -Value $content -Encoding ASCII
|
|
}
|
|
|
|
function Remove-LegacyActivityWatchEntries {
|
|
$legacyTaskNames = @(
|
|
'ActivityWatch Watchers',
|
|
'ActivityWatch Guard',
|
|
'ActivityWatch Heal',
|
|
'AWatchRusStandaloneAgent',
|
|
'AWatch Worktime Collector',
|
|
'AW DLP Endpoint ADMIN',
|
|
'AW DLP Endpoint USER1'
|
|
)
|
|
|
|
foreach ($taskName in $legacyTaskNames) {
|
|
Remove-ActivityWatchScheduledTask -TaskName $taskName
|
|
}
|
|
|
|
$legacyTaskPatterns = @(
|
|
'browser-domains-native-collector.ps1',
|
|
'file-operations-collector.ps1',
|
|
'dlp-endpoint-signals-collector.ps1',
|
|
'worktime-session-collector.ps1',
|
|
'aw-standalone-service.ps1'
|
|
)
|
|
$managedTaskNames = @(
|
|
'ActivityWatch Recovery',
|
|
'ActivityWatch Hayabusa Upload',
|
|
'ActivityWatch File1C Upload'
|
|
)
|
|
|
|
$scheduledTasks = @()
|
|
try {
|
|
$scheduledTasks = @(Get-ScheduledTask -ErrorAction Stop)
|
|
}
|
|
catch {
|
|
$scheduledTasks = @()
|
|
}
|
|
|
|
foreach ($task in $scheduledTasks) {
|
|
$taskName = [string]$task.TaskName
|
|
if ([string]::IsNullOrWhiteSpace($taskName) -or $managedTaskNames -contains $taskName -or $taskName -like 'ActivityWatch Launch *') {
|
|
continue
|
|
}
|
|
|
|
$isLegacyCollectorTask = $false
|
|
foreach ($action in @($task.Actions)) {
|
|
$execute = if ($action.PSObject.Properties.Name -contains 'Execute') { [string]$action.Execute } else { '' }
|
|
$arguments = if ($action.PSObject.Properties.Name -contains 'Arguments') { [string]$action.Arguments } else { '' }
|
|
$commandLine = ('{0} {1}' -f $execute, $arguments).Trim()
|
|
if ([string]::IsNullOrWhiteSpace($commandLine)) {
|
|
continue
|
|
}
|
|
foreach ($pattern in $legacyTaskPatterns) {
|
|
if ($commandLine -match [Regex]::Escape($pattern)) {
|
|
$isLegacyCollectorTask = $true
|
|
break
|
|
}
|
|
}
|
|
if ($isLegacyCollectorTask) {
|
|
break
|
|
}
|
|
}
|
|
|
|
if ($isLegacyCollectorTask) {
|
|
Remove-ActivityWatchScheduledTask -TaskName $taskName
|
|
}
|
|
}
|
|
|
|
$runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run'
|
|
foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') {
|
|
Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
function Remove-ActivityWatchScheduledTask {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$TaskName
|
|
)
|
|
|
|
try {
|
|
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction Stop
|
|
}
|
|
catch {
|
|
}
|
|
|
|
& cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null
|
|
if ($LASTEXITCODE -eq 0) {
|
|
return
|
|
}
|
|
|
|
for ($attempt = 0; $attempt -lt 10; $attempt++) {
|
|
$task = $null
|
|
try {
|
|
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop
|
|
}
|
|
catch {
|
|
& cmd.exe /c "schtasks /Query /TN `"$TaskName`" >nul 2>&1" | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
return
|
|
}
|
|
}
|
|
if (-not $task) {
|
|
return
|
|
}
|
|
|
|
Start-Sleep -Milliseconds 300
|
|
}
|
|
}
|
|
|
|
function Set-ActivityWatchScheduledTaskAction {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$TaskName,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Execute,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Arguments
|
|
)
|
|
|
|
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
if (-not $task) {
|
|
return $false
|
|
}
|
|
|
|
$newAction = New-ScheduledTaskAction -Execute $Execute -Argument $Arguments
|
|
try {
|
|
# Non-interactive update path. Avoids schtasks.exe /Change password prompt for user-bound tasks.
|
|
Set-ScheduledTask -TaskName $TaskName -Action $newAction -ErrorAction Stop | Out-Null
|
|
return $true
|
|
}
|
|
catch {
|
|
$taskCommand = ('"{0}" {1}' -f $Execute, $Arguments)
|
|
& schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Host "skip task action update for $TaskName because the existing principal/action cannot be updated non-interactively: $($_.Exception.Message)"
|
|
return $false
|
|
}
|
|
return $true
|
|
}
|
|
}
|
|
|
|
function Get-ActivityWatchScheduledTaskByCommand {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$TaskName,
|
|
[string]$CommandMatch
|
|
)
|
|
|
|
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
if ($task) {
|
|
return $task
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($CommandMatch)) {
|
|
return $null
|
|
}
|
|
|
|
foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) {
|
|
foreach ($action in @($candidate.Actions)) {
|
|
if ([string]$action.Arguments -like "*$CommandMatch*") {
|
|
return $candidate
|
|
}
|
|
}
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Remove-StaleActivityWatchUserTasks {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[pscustomobject[]]$TaskDefinitions,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$LaunchScriptPath
|
|
)
|
|
|
|
$launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath
|
|
$desiredTaskNames = @($TaskDefinitions | ForEach-Object { [string]$_.LaunchTaskName })
|
|
|
|
foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) {
|
|
$taskName = [string]$candidate.TaskName
|
|
if ($desiredTaskNames -contains $taskName) {
|
|
continue
|
|
}
|
|
|
|
$usesCurrentLauncher = $false
|
|
foreach ($action in @($candidate.Actions)) {
|
|
if ([string]$action.Arguments -like "*$launcherPath*") {
|
|
$usesCurrentLauncher = $true
|
|
break
|
|
}
|
|
}
|
|
|
|
if ($usesCurrentLauncher) {
|
|
Remove-ActivityWatchScheduledTask -TaskName $taskName
|
|
}
|
|
}
|
|
}
|
|
|
|
function Register-ActivityWatchUserTasks {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[pscustomobject[]]$TaskDefinitions,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$LaunchScriptPath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ConfigPath
|
|
)
|
|
|
|
$wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe'
|
|
$launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath
|
|
Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath
|
|
Remove-StaleActivityWatchUserTasks -TaskDefinitions $TaskDefinitions -LaunchScriptPath $LaunchScriptPath
|
|
|
|
foreach ($definition in $TaskDefinitions) {
|
|
$action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`""
|
|
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId
|
|
$principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType Interactive -RunLevel Highest
|
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0)
|
|
$existingTask = Get-ActivityWatchScheduledTaskByCommand -TaskName $definition.LaunchTaskName -CommandMatch $ConfigPath
|
|
|
|
if ($existingTask) {
|
|
$updated = Set-ActivityWatchScheduledTaskAction -TaskName $existingTask.TaskName -Execute $wscriptExe -Arguments $action.Arguments
|
|
if ($updated) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
Remove-ActivityWatchScheduledTask -TaskName $definition.LaunchTaskName
|
|
Register-ScheduledTask -TaskName $definition.LaunchTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
|
|
}
|
|
}
|
|
|
|
function Register-ActivityWatchRecoveryTask {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$TaskName,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RecoveryScriptPath,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ConfigPath
|
|
)
|
|
|
|
Remove-ActivityWatchScheduledTask -TaskName $TaskName
|
|
|
|
$wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe'
|
|
$launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath
|
|
Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath
|
|
$action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`""
|
|
$sessionRecords = @()
|
|
try {
|
|
$sessionRecords = @(Get-ActivityWatchSessionRecords)
|
|
}
|
|
catch {
|
|
$sessionRecords = @()
|
|
}
|
|
$liveSession = @(Get-ActivityWatchLiveInteractiveSessions -SessionRecords $sessionRecords) | Select-Object -First 1
|
|
$interactiveUserId = $null
|
|
if ($liveSession -and -not [string]::IsNullOrWhiteSpace([string]$liveSession.UserName)) {
|
|
$rawUser = [string]$liveSession.UserName
|
|
$interactiveUserId = if ($rawUser -match '^[^\\]+\\') { $rawUser } else { ('{0}\{1}' -f $env:COMPUTERNAME, $rawUser) }
|
|
}
|
|
|
|
if ($interactiveUserId) {
|
|
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $interactiveUserId
|
|
$principal = New-ScheduledTaskPrincipal -UserId $interactiveUserId -LogonType Interactive -RunLevel Highest
|
|
}
|
|
else {
|
|
$trigger = New-ScheduledTaskTrigger -AtStartup
|
|
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
|
}
|
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0)
|
|
|
|
try {
|
|
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -ErrorAction Stop | Out-Null
|
|
}
|
|
catch {
|
|
$taskCommand = ('"{0}" {1}' -f $wscriptExe, $action.Arguments)
|
|
if ($interactiveUserId) {
|
|
& schtasks.exe /Create /TN $TaskName /SC ONLOGON /RU $interactiveUserId /IT /RL HIGHEST /F /TR $taskCommand | Out-Null
|
|
}
|
|
else {
|
|
& schtasks.exe /Create /TN $TaskName /SC ONSTART /RU SYSTEM /RL HIGHEST /F /TR $taskCommand | Out-Null
|
|
}
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw
|
|
}
|
|
}
|
|
}
|
|
|
|
function Register-ActivityWatchHayabusaAutoUploadTask {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ConfigPath
|
|
)
|
|
|
|
$config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
|
|
$forensics = $config.forensics
|
|
if ($null -eq $forensics -or $forensics.PSObject.Properties.Name -notcontains 'hayabusaAutomation') {
|
|
return
|
|
}
|
|
|
|
$automation = $forensics.hayabusaAutomation
|
|
$taskName = if ($automation.PSObject.Properties.Name -contains 'taskName' -and -not [string]::IsNullOrWhiteSpace([string]$automation.taskName)) {
|
|
[string]$automation.taskName
|
|
} else {
|
|
'ActivityWatch Hayabusa Upload'
|
|
}
|
|
|
|
if (-not [bool]$automation.enabled) {
|
|
Remove-ActivityWatchScheduledTask -TaskName $taskName
|
|
return
|
|
}
|
|
|
|
$uploadScript = if ($config.paths.PSObject.Properties.Name -contains 'hayabusaUploadScript') { [string]$config.paths.hayabusaUploadScript } else { Join-Path $config.paths.stateRoot 'export-upload-hayabusa-to-aw-server.ps1' }
|
|
if (-not (Test-Path -LiteralPath $uploadScript)) {
|
|
throw "Не найден скрипт Hayabusa upload: $uploadScript"
|
|
}
|
|
|
|
$intervalHours = [Math]::Max(1, [int]$automation.intervalHours)
|
|
$hoursBack = [Math]::Max(1, [int]$automation.hoursBack)
|
|
$mode = if ($automation.PSObject.Properties.Name -contains 'mode' -and -not [string]::IsNullOrWhiteSpace([string]$automation.mode)) { [string]$automation.mode } else { 'incident' }
|
|
$powerShellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
$taskCommand = "`"$powerShellExe`" -NoProfile -ExecutionPolicy Bypass -File `"$uploadScript`" -ConfigPath `"$ConfigPath`" -HoursBack $hoursBack -Mode `"$mode`""
|
|
|
|
Remove-ActivityWatchScheduledTask -TaskName $taskName
|
|
& schtasks.exe /Create /TN $taskName /TR $taskCommand /SC HOURLY /MO $intervalHours /ST 00:00 /RU SYSTEM /RL HIGHEST /F | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Не удалось создать scheduled task $taskName через schtasks.exe"
|
|
}
|
|
}
|
|
|
|
function Register-ActivityWatchFile1CAutoUploadTask {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$ConfigPath
|
|
)
|
|
|
|
$config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
|
|
if ($config.PSObject.Properties.Name -notcontains 'analytics' -or
|
|
$config.analytics.PSObject.Properties.Name -notcontains 'file1cAutomation') {
|
|
return
|
|
}
|
|
|
|
$automation = $config.analytics.file1cAutomation
|
|
$taskName = if ($automation.PSObject.Properties.Name -contains 'taskName' -and -not [string]::IsNullOrWhiteSpace([string]$automation.taskName)) {
|
|
[string]$automation.taskName
|
|
} else {
|
|
'ActivityWatch File1C Upload'
|
|
}
|
|
|
|
if (-not [bool]$automation.enabled) {
|
|
Remove-ActivityWatchScheduledTask -TaskName $taskName
|
|
return
|
|
}
|
|
|
|
$uploadScript = if ($config.paths.PSObject.Properties.Name -contains 'file1cTelemetryScript') { [string]$config.paths.file1cTelemetryScript } else { Join-Path $config.paths.stateRoot 'export-upload-file-1c-telemetry.ps1' }
|
|
$uploadExeCandidates = New-Object System.Collections.Generic.List[string]
|
|
if ($config.paths.PSObject.Properties.Name -contains 'file1cTelemetryExecutable' -and -not [string]::IsNullOrWhiteSpace([string]$config.paths.file1cTelemetryExecutable)) {
|
|
$uploadExeCandidates.Add([string]$config.paths.file1cTelemetryExecutable)
|
|
}
|
|
$uploadExeCandidates.Add((Join-Path (Join-Path ([System.Environment]::GetFolderPath('ProgramFiles')) 'AWatch-rus\windows') 'aw-windows-telemetry.exe'))
|
|
$uploadExeCandidates.Add((Join-Path $config.paths.stateRoot 'aw-windows-telemetry.exe'))
|
|
$uploadExe = @($uploadExeCandidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) -and (Test-Path -LiteralPath $_) } | Select-Object -First 1) | Select-Object -First 1
|
|
if ([string]::IsNullOrWhiteSpace($uploadExe) -and -not (Test-Path -LiteralPath $uploadScript)) {
|
|
throw "Не найден Rust binary или legacy script file-1C telemetry upload: $uploadExeCandidates / $uploadScript"
|
|
}
|
|
|
|
$intervalMinutes = if ($automation.PSObject.Properties.Name -contains 'intervalMinutes') {
|
|
[Math]::Max(1, [int]$automation.intervalMinutes)
|
|
} else {
|
|
[Math]::Max(1, [int]$automation.intervalHours) * 60
|
|
}
|
|
$runnerUserId = $null
|
|
if ($automation.PSObject.Properties.Name -contains 'runAsUser' -and -not [string]::IsNullOrWhiteSpace([string]$automation.runAsUser)) {
|
|
$runnerUserId = [string]$automation.runAsUser
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($runnerUserId) -and $config.PSObject.Properties.Name -contains 'userTasks') {
|
|
$runnerUserId = @(
|
|
@($config.userTasks | ForEach-Object { [string]$_.userId }) |
|
|
Where-Object { $_ -match '(^|\\)(Администратор|Administrator)$' } |
|
|
Select-Object -First 1
|
|
) | Select-Object -First 1
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($runnerUserId) -and $config.PSObject.Properties.Name -contains 'userTasks') {
|
|
$runnerUserId = @($config.userTasks | ForEach-Object { [string]$_.userId } | Select-Object -First 1) | Select-Object -First 1
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($uploadExe)) {
|
|
$action = New-ScheduledTaskAction -Execute $uploadExe -Argument "file1c-upload --config-path `"$ConfigPath`""
|
|
}
|
|
else {
|
|
$powerShellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
$action = New-ScheduledTaskAction -Execute $powerShellExe -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$uploadScript`" -ConfigPath `"$ConfigPath`""
|
|
}
|
|
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).Date) -RepetitionInterval (New-TimeSpan -Minutes $intervalMinutes) -RepetitionDuration (New-TimeSpan -Days 3650)
|
|
if (-not [string]::IsNullOrWhiteSpace($runnerUserId)) {
|
|
$principal = New-ScheduledTaskPrincipal -UserId $runnerUserId -LogonType Interactive -RunLevel Highest
|
|
}
|
|
else {
|
|
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
|
}
|
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 10)
|
|
|
|
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
|
|
}
|
|
|
|
function Set-ActivityWatchAcl {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$InstallRoot,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$StateRoot,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$LogsRoot
|
|
)
|
|
|
|
foreach ($path in $InstallRoot, $StateRoot, $LogsRoot) {
|
|
New-ActivityWatchDirectory -Path $path
|
|
}
|
|
|
|
& icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "icacls завершился с ошибкой для $InstallRoot"
|
|
}
|
|
|
|
& icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "icacls завершился с ошибкой для $StateRoot"
|
|
}
|
|
|
|
& icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "icacls завершился с ошибкой для $LogsRoot"
|
|
}
|
|
}
|
|
|
|
function Start-ActivityWatchTasks {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[pscustomobject[]]$TaskDefinitions,
|
|
[string]$RecoveryTaskName = 'ActivityWatch Recovery'
|
|
)
|
|
|
|
$sessionRecords = Get-ActivityWatchSessionRecords
|
|
|
|
foreach ($definition in $TaskDefinitions) {
|
|
if (Test-ActivityWatchUserHasLiveSession -UserId $definition.UserId -SessionRecords $sessionRecords) {
|
|
Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries
|