feat(detmir): add rust-first operations tooling
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.ServiceProcess;
|
||||
|
||||
namespace AWatchRus
|
||||
{
|
||||
public sealed class CollectorGuardService : ServiceBase
|
||||
{
|
||||
private Process child;
|
||||
private readonly ServiceOptions options;
|
||||
|
||||
public CollectorGuardService(ServiceOptions options)
|
||||
{
|
||||
this.options = options;
|
||||
ServiceName = options.ServiceName;
|
||||
CanStop = true;
|
||||
CanShutdown = true;
|
||||
}
|
||||
|
||||
protected override void OnStart(string[] args)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(options.LogPath));
|
||||
File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " service starting" + Environment.NewLine);
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = options.PowerShellPath,
|
||||
Arguments = string.Format(
|
||||
"-NoProfile -ExecutionPolicy Bypass -File \"{0}\" -ConfigPath \"{1}\" -Mode {2} -LoopSeconds {3}",
|
||||
options.ScriptPath,
|
||||
options.ConfigPath,
|
||||
options.Mode,
|
||||
options.LoopSeconds),
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = false,
|
||||
RedirectStandardError = false,
|
||||
};
|
||||
child = Process.Start(psi);
|
||||
File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " child pid=" + child.Id + Environment.NewLine);
|
||||
}
|
||||
|
||||
protected override void OnStop()
|
||||
{
|
||||
StopChild("service stopping");
|
||||
}
|
||||
|
||||
protected override void OnShutdown()
|
||||
{
|
||||
StopChild("system shutdown");
|
||||
}
|
||||
|
||||
private void StopChild(string reason)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " " + reason + Environment.NewLine);
|
||||
if (child != null && !child.HasExited)
|
||||
{
|
||||
child.Kill();
|
||||
child.WaitForExit(10000);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " stop error: " + ex.Message + Environment.NewLine);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ServiceOptions
|
||||
{
|
||||
public string ServiceName = "AWatchRusCollectorGuard";
|
||||
public string PowerShellPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32\\WindowsPowerShell\\v1.0\\powershell.exe");
|
||||
public string ScriptPath = @"C:\Program Files\AWatch-rus\windows\aw-collector-guard.ps1";
|
||||
public string ConfigPath = @"C:\ProgramData\AWatch-rus\deployment-config.json";
|
||||
public string Mode = "shadow";
|
||||
public int LoopSeconds = 60;
|
||||
public string LogPath = @"C:\ProgramData\AWatch-rus\logs\collector-guard-service.log";
|
||||
}
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
var options = Parse(args);
|
||||
ServiceBase.Run(new CollectorGuardService(options));
|
||||
}
|
||||
|
||||
private static ServiceOptions Parse(string[] args)
|
||||
{
|
||||
var options = new ServiceOptions();
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
var key = args[i].ToLowerInvariant();
|
||||
var value = i + 1 < args.Length ? args[i + 1] : null;
|
||||
if (value == null || value.StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (key == "--service-name") options.ServiceName = value;
|
||||
else if (key == "--script") options.ScriptPath = value;
|
||||
else if (key == "--config") options.ConfigPath = value;
|
||||
else if (key == "--mode") options.Mode = value;
|
||||
else if (key == "--loop")
|
||||
{
|
||||
int parsed;
|
||||
if (int.TryParse(value, out parsed)) options.LoopSeconds = parsed;
|
||||
}
|
||||
else if (key == "--log") options.LogPath = value;
|
||||
i++;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
Set-StrictMode -Version Latest
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$script:ActivityWatchBuiltInAdministratorName = $null
|
||||
|
||||
@@ -431,6 +431,22 @@ function Get-ActivityWatchTaskNameToken {
|
||||
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)]
|
||||
@@ -633,6 +649,119 @@ function Test-ActivityWatchUserHasLiveSession {
|
||||
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)]
|
||||
@@ -777,7 +906,7 @@ function New-ActivityWatchDeploymentConfig {
|
||||
[int]$EvtxRetentionDays = 14,
|
||||
[string[]]$EvtxChannels = @(),
|
||||
[bool]$LogonMarkerEnabled = $true,
|
||||
[bool]$ProcessEventsEnabled = $true,
|
||||
[bool]$ProcessEventsEnabled = $false,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LaunchScriptPath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
@@ -1410,6 +1539,10 @@ function Get-ActivityWatchRecoveryTaskDefinitions {
|
||||
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
|
||||
@@ -1679,13 +1812,24 @@ function Stop-ActivityWatchProcessesInNonLiveSessions {
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object[]]$SessionRecords,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Config
|
||||
[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 } |
|
||||
Where-Object { -not $_.IsLive -and $_.SessionId -gt 0 -and ($preservedSessionIds -notcontains [int]$_.SessionId) } |
|
||||
ForEach-Object { [int]$_.SessionId } |
|
||||
Sort-Object -Unique
|
||||
)
|
||||
@@ -1704,7 +1848,7 @@ function Stop-ActivityWatchProcessesInNonLiveSessions {
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($session in @($SessionRecords | Where-Object { -not $_.IsLive -and $_.SessionId -gt 0 })) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1885,7 +2029,7 @@ function Invoke-ActivityWatchRecoveryLoop {
|
||||
$config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
|
||||
$taskDefs = Get-ActivityWatchRecoveryTaskDefinitions -ConfigPaths $configPaths
|
||||
$sessionRecords = Get-ActivityWatchSessionRecords
|
||||
Stop-ActivityWatchProcessesInNonLiveSessions -SessionRecords $sessionRecords -Config $config
|
||||
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' }
|
||||
|
||||
@@ -0,0 +1,681 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[ValidateSet('shadow', 'enforce')]
|
||||
[string]$Mode = 'shadow',
|
||||
[int]$LoopSeconds = 60,
|
||||
[int]$InteractiveMaxAgeSeconds = 900,
|
||||
[int]$HeadlessMaxAgeSeconds = 900,
|
||||
[int]$RestartWindowSeconds = 600,
|
||||
[int]$MaxRestarts = 3,
|
||||
[int]$ActionCooldownSeconds = 300,
|
||||
[int]$InteractiveActionCooldownSeconds = 60,
|
||||
[switch]$Once,
|
||||
[switch]$HeadlessEndpointEnabled,
|
||||
[switch]$HeadlessFileOpsEnabled,
|
||||
[switch]$SelfTest
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1'
|
||||
Import-Module $modulePath -Force
|
||||
|
||||
function New-GuardLock {
|
||||
param([string]$StateRoot)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $StateRoot)) {
|
||||
New-Item -Path $StateRoot -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$lockPath = Join-Path $StateRoot 'collector-guard.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 Write-GuardLog {
|
||||
param(
|
||||
[string]$LogPath,
|
||||
[string]$Message
|
||||
)
|
||||
|
||||
try {
|
||||
$directory = Split-Path -Path $LogPath -Parent
|
||||
if ($directory -and -not (Test-Path -LiteralPath $directory)) {
|
||||
New-Item -Path $directory -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
Add-Content -LiteralPath $LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message)
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AwApiBase {
|
||||
param([pscustomobject]$Config)
|
||||
|
||||
$scheme = if ($Config.server.PSObject.Properties.Name -contains 'scheme') { [string]$Config.server.scheme } else { 'http' }
|
||||
$hostName = [string]$Config.server.host
|
||||
$port = [int]$Config.server.port
|
||||
return ('{0}://{1}:{2}/api/0' -f $scheme, $hostName, $port)
|
||||
}
|
||||
|
||||
function Invoke-AwJson {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Method,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Uri,
|
||||
[object]$Body
|
||||
)
|
||||
|
||||
$params = @{
|
||||
Method = $Method
|
||||
Uri = $Uri
|
||||
TimeoutSec = 15
|
||||
ErrorAction = 'Stop'
|
||||
}
|
||||
if ($null -ne $Body) {
|
||||
$params.Body = ($Body | ConvertTo-Json -Depth 16 -Compress)
|
||||
$params.ContentType = 'application/json'
|
||||
}
|
||||
return Invoke-RestMethod @params
|
||||
}
|
||||
|
||||
function Ensure-AwBucket {
|
||||
param(
|
||||
[string]$ApiBase,
|
||||
[string]$BucketId,
|
||||
[string]$ClientName,
|
||||
[string]$BucketType,
|
||||
[string]$Hostname
|
||||
)
|
||||
|
||||
try {
|
||||
Invoke-AwJson -Method 'GET' -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
return $true
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
try {
|
||||
$body = @{
|
||||
client = $ClientName
|
||||
type = $BucketType
|
||||
hostname = $Hostname
|
||||
}
|
||||
Invoke-AwJson -Method 'POST' -Uri "$ApiBase/buckets/$BucketId" -Body $body | Out-Null
|
||||
return $true
|
||||
}
|
||||
catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-LatestBucketAge {
|
||||
param(
|
||||
[string]$ApiBase,
|
||||
[string]$BucketId
|
||||
)
|
||||
|
||||
try {
|
||||
$events = Invoke-AwJson -Method 'GET' -Uri "$ApiBase/buckets/$BucketId/events?limit=20"
|
||||
$latest = @($events | Where-Object { $null -ne $_.timestamp } | Sort-Object timestamp -Descending | Select-Object -First 1)
|
||||
if (-not $latest) {
|
||||
return [pscustomobject]@{ bucket = $BucketId; found = $false; timestamp = $null; ageSeconds = $null }
|
||||
}
|
||||
$ts = [DateTimeOffset]::Parse([string]$latest.timestamp).UtcDateTime
|
||||
$age = [Math]::Max(0, [int]((Get-Date).ToUniversalTime() - $ts).TotalSeconds)
|
||||
return [pscustomobject]@{ bucket = $BucketId; found = $true; timestamp = [string]$latest.timestamp; ageSeconds = $age }
|
||||
}
|
||||
catch {
|
||||
return [pscustomobject]@{ bucket = $BucketId; found = $false; timestamp = $null; ageSeconds = $null; error = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
|
||||
function Send-GuardHeartbeat {
|
||||
param(
|
||||
[string]$ApiBase,
|
||||
[string]$Hostname,
|
||||
[object]$State,
|
||||
[int]$PulseSeconds
|
||||
)
|
||||
|
||||
$bucketId = "aw-rus-collector-guard_$Hostname"
|
||||
if (-not (Ensure-AwBucket -ApiBase $ApiBase -BucketId $bucketId -ClientName 'aw-rus-collector-guard' -BucketType 'aw.rus.collector.guard' -Hostname $Hostname)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$event = @{
|
||||
timestamp = (Get-Date).ToUniversalTime().ToString('o')
|
||||
duration = 0
|
||||
data = $State
|
||||
}
|
||||
|
||||
try {
|
||||
Invoke-AwJson -Method 'POST' -Uri "$ApiBase/buckets/$bucketId/heartbeat?pulsetime=$PulseSeconds" -Body $event | Out-Null
|
||||
return $true
|
||||
}
|
||||
catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Read-GuardRuntime {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
return [pscustomobject]@{ restartHistory = @{}; lastAction = @{}; quarantine = @{} }
|
||||
}
|
||||
try {
|
||||
$state = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||
if ($null -eq $state.restartHistory) { $state | Add-Member -NotePropertyName restartHistory -NotePropertyValue @{} }
|
||||
if ($null -eq $state.lastAction) { $state | Add-Member -NotePropertyName lastAction -NotePropertyValue @{} }
|
||||
if ($null -eq $state.quarantine) { $state | Add-Member -NotePropertyName quarantine -NotePropertyValue @{} }
|
||||
return $state
|
||||
}
|
||||
catch {
|
||||
return [pscustomobject]@{ restartHistory = @{}; lastAction = @{}; quarantine = @{} }
|
||||
}
|
||||
}
|
||||
|
||||
function Write-GuardRuntime {
|
||||
param(
|
||||
[string]$Path,
|
||||
[object]$Runtime
|
||||
)
|
||||
|
||||
$directory = Split-Path -Path $Path -Parent
|
||||
if ($directory -and -not (Test-Path -LiteralPath $directory)) {
|
||||
New-Item -Path $directory -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
$Runtime | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $Path -Encoding UTF8
|
||||
}
|
||||
|
||||
function Get-RuntimeMapValue {
|
||||
param(
|
||||
[object]$Map,
|
||||
[string]$Key
|
||||
)
|
||||
|
||||
if ($null -eq $Map) {
|
||||
return $null
|
||||
}
|
||||
if ($Map -is [hashtable] -and $Map.ContainsKey($Key)) {
|
||||
return $Map[$Key]
|
||||
}
|
||||
$propertyNames = @($Map.PSObject.Properties | ForEach-Object { $_.Name })
|
||||
if ($propertyNames -contains $Key) {
|
||||
return $Map.$Key
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Set-RuntimeMapValue {
|
||||
param(
|
||||
[object]$Map,
|
||||
[string]$Key,
|
||||
[object]$Value
|
||||
)
|
||||
|
||||
if ($Map -is [hashtable]) {
|
||||
$Map[$Key] = $Value
|
||||
return
|
||||
}
|
||||
$propertyNames = @($Map.PSObject.Properties | ForEach-Object { $_.Name })
|
||||
if ($propertyNames -contains $Key) {
|
||||
$Map.$Key = $Value
|
||||
}
|
||||
else {
|
||||
$Map | Add-Member -NotePropertyName $Key -NotePropertyValue $Value -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-RuntimeMapValue {
|
||||
param(
|
||||
[object]$Map,
|
||||
[string]$Key
|
||||
)
|
||||
|
||||
if ($null -eq $Map) {
|
||||
return
|
||||
}
|
||||
if ($Map -is [hashtable]) {
|
||||
if ($Map.ContainsKey($Key)) {
|
||||
$Map.Remove($Key)
|
||||
}
|
||||
return
|
||||
}
|
||||
$property = $Map.PSObject.Properties[$Key]
|
||||
if ($null -ne $property) {
|
||||
$Map.PSObject.Properties.Remove($Key)
|
||||
}
|
||||
}
|
||||
|
||||
function Reset-GuardActionBudget {
|
||||
param(
|
||||
[object]$Runtime,
|
||||
[string]$Key
|
||||
)
|
||||
|
||||
Remove-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key
|
||||
Remove-RuntimeMapValue -Map $Runtime.lastAction -Key $Key
|
||||
Remove-RuntimeMapValue -Map $Runtime.quarantine -Key $Key
|
||||
}
|
||||
|
||||
function Invoke-GuardSelfTest {
|
||||
$emptyObject = [pscustomobject]@{}
|
||||
if ($null -ne (Get-RuntimeMapValue -Map $emptyObject -Key 'missing')) {
|
||||
throw 'empty PSCustomObject should not return a missing runtime-map value'
|
||||
}
|
||||
Set-RuntimeMapValue -Map $emptyObject -Key 'headless:worktime-session' -Value 123
|
||||
if ((Get-RuntimeMapValue -Map $emptyObject -Key 'headless:worktime-session') -ne 123) {
|
||||
throw 'failed to set runtime-map value on empty PSCustomObject'
|
||||
}
|
||||
|
||||
$hash = @{}
|
||||
Set-RuntimeMapValue -Map $hash -Key 'headless:worktime-session' -Value @(1, 2)
|
||||
$hashValue = @(Get-RuntimeMapValue -Map $hash -Key 'headless:worktime-session')
|
||||
if ($hashValue.Count -ne 2) {
|
||||
throw 'failed to round-trip runtime-map value on hashtable'
|
||||
}
|
||||
|
||||
$runtime = [pscustomobject]@{ restartHistory = [pscustomobject]@{}; lastAction = [pscustomobject]@{}; quarantine = [pscustomobject]@{} }
|
||||
$allowed = Test-ActionAllowed -Runtime $runtime -Key 'headless:worktime-session' -CooldownSeconds 1 -WindowSeconds 60 -MaxCount 3
|
||||
if (-not $allowed.allowed) {
|
||||
throw "expected action to be allowed, got $($allowed.reason)"
|
||||
}
|
||||
Register-GuardAction -Runtime $runtime -Key 'headless:worktime-session'
|
||||
$blocked = Test-ActionAllowed -Runtime $runtime -Key 'headless:worktime-session' -CooldownSeconds 300 -WindowSeconds 60 -MaxCount 3
|
||||
if ($blocked.allowed -or $blocked.reason -ne 'cooldown') {
|
||||
throw 'expected cooldown after registering guard action'
|
||||
}
|
||||
$budgetRuntime = [pscustomobject]@{ restartHistory = [pscustomobject]@{}; lastAction = [pscustomobject]@{}; quarantine = [pscustomobject]@{} }
|
||||
foreach ($i in 1..3) {
|
||||
Register-GuardAction -Runtime $budgetRuntime -Key 'task:test'
|
||||
}
|
||||
$budgetBlocked = Test-ActionAllowed -Runtime $budgetRuntime -Key 'task:test' -CooldownSeconds 0 -WindowSeconds 600 -MaxCount 3
|
||||
if ($budgetBlocked.allowed -or $budgetBlocked.reason -ne 'quarantine') {
|
||||
throw 'expected quarantine when restart budget is exhausted'
|
||||
}
|
||||
Reset-GuardActionBudget -Runtime $budgetRuntime -Key 'task:test'
|
||||
$budgetAllowed = Test-ActionAllowed -Runtime $budgetRuntime -Key 'task:test' -CooldownSeconds 0 -WindowSeconds 600 -MaxCount 3
|
||||
if (-not $budgetAllowed.allowed) {
|
||||
throw 'expected reset action budget to clear quarantine'
|
||||
}
|
||||
|
||||
$oldComputerName = $env:COMPUTERNAME
|
||||
try {
|
||||
$env:COMPUTERNAME = 'SHARKON2025'
|
||||
$sessionRecords = @(
|
||||
[pscustomobject]@{ SessionName = 'USER5'; UserName = 'USER5'; SessionId = 2; State = 'Disc'; IsLive = $false },
|
||||
[pscustomobject]@{ SessionName = 'console'; UserName = ''; SessionId = 1; State = 'Conn'; IsLive = $true }
|
||||
)
|
||||
$taskDefs = @(
|
||||
[pscustomobject]@{ taskName = 'ActivityWatch Launch [SHARKON2025_user5]'; userId = 'SHARKON2025\user5' }
|
||||
)
|
||||
if (-not (Test-ActivityWatchUserHasManagedSession -UserId 'SHARKON2025\user5' -SessionRecords $sessionRecords -IncludeDisconnected)) {
|
||||
throw 'expected disconnected managed session to match task user'
|
||||
}
|
||||
if (Test-ActivityWatchUserHasManagedSession -UserId 'SHARKON2025\user5' -SessionRecords $sessionRecords -IncludeLive) {
|
||||
throw 'disconnected managed session should not match live-only filter'
|
||||
}
|
||||
$managed = @(Get-ActivityWatchManagedInteractiveSessions -TaskDefinitions $taskDefs -SessionRecords $sessionRecords -IncludeDisconnected)
|
||||
if ($managed.Count -ne 1 -or [int]$managed[0].SessionId -ne 2) {
|
||||
throw 'failed to enumerate disconnected managed session'
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($null -eq $oldComputerName) {
|
||||
Remove-Item Env:COMPUTERNAME -ErrorAction SilentlyContinue
|
||||
}
|
||||
else {
|
||||
$env:COMPUTERNAME = $oldComputerName
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output 'collector guard self-test OK'
|
||||
}
|
||||
|
||||
function Test-ActionAllowed {
|
||||
param(
|
||||
[object]$Runtime,
|
||||
[string]$Key,
|
||||
[int]$CooldownSeconds,
|
||||
[int]$WindowSeconds,
|
||||
[int]$MaxCount
|
||||
)
|
||||
|
||||
$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
||||
$last = Get-RuntimeMapValue -Map $Runtime.lastAction -Key $Key
|
||||
if ($null -ne $last -and ($now - [int64]$last) -lt $CooldownSeconds) {
|
||||
return [pscustomobject]@{ allowed = $false; reason = 'cooldown' }
|
||||
}
|
||||
|
||||
$history = @(Get-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key)
|
||||
$history = @($history | Where-Object { ($now - [int64]$_) -le $WindowSeconds })
|
||||
Set-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key -Value @($history)
|
||||
if ($history.Count -ge $MaxCount) {
|
||||
Set-RuntimeMapValue -Map $Runtime.quarantine -Key $Key -Value @{
|
||||
since = (Get-Date).ToUniversalTime().ToString('o')
|
||||
reason = 'restart-budget-exhausted'
|
||||
count = $history.Count
|
||||
}
|
||||
return [pscustomobject]@{ allowed = $false; reason = 'quarantine' }
|
||||
}
|
||||
|
||||
Remove-RuntimeMapValue -Map $Runtime.quarantine -Key $Key
|
||||
return [pscustomobject]@{ allowed = $true; reason = 'ok' }
|
||||
}
|
||||
|
||||
function Register-GuardAction {
|
||||
param(
|
||||
[object]$Runtime,
|
||||
[string]$Key
|
||||
)
|
||||
|
||||
$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
||||
$history = @(Get-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key)
|
||||
$history += $now
|
||||
Set-RuntimeMapValue -Map $Runtime.restartHistory -Key $Key -Value @($history)
|
||||
Set-RuntimeMapValue -Map $Runtime.lastAction -Key $Key -Value $now
|
||||
}
|
||||
|
||||
function Get-CollectorProcessSnapshot {
|
||||
param([pscustomobject]$Config)
|
||||
|
||||
$scriptPaths = [ordered]@{}
|
||||
foreach ($name in @('collectorScript', 'endpointCollectorScript', 'fileCollectorScript', 'emailCollectorScript', 'sessionCollectorScript')) {
|
||||
if ($Config.paths.PSObject.Properties.Name -contains $name) {
|
||||
$value = [string]$Config.paths.$name
|
||||
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||
$scriptPaths[$name] = $value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$powershellCollectors = @()
|
||||
try {
|
||||
$processes = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe' })
|
||||
foreach ($proc in $processes) {
|
||||
$commandLine = [string]$proc.CommandLine
|
||||
foreach ($entry in $scriptPaths.GetEnumerator()) {
|
||||
if ($commandLine -match [Regex]::Escape([string]$entry.Value)) {
|
||||
$powershellCollectors += [pscustomobject]@{
|
||||
name = [string]$entry.Key
|
||||
processId = [int]$proc.ProcessId
|
||||
sessionId = [int]$proc.SessionId
|
||||
scriptPath = [string]$entry.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
$watchers = @()
|
||||
try {
|
||||
$watchers = @(Get-Process -Name 'aw-watcher-afk','aw-watcher-window' -ErrorAction SilentlyContinue |
|
||||
Select-Object @{Name='name'; Expression={$_.Name}}, @{Name='processId'; Expression={$_.Id}}, @{Name='sessionId'; Expression={$_.SessionId}})
|
||||
}
|
||||
catch {
|
||||
$watchers = @()
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
watchers = @($watchers)
|
||||
collectors = @($powershellCollectors)
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-ExactTaskRun {
|
||||
param([string]$TaskName)
|
||||
|
||||
& schtasks.exe /Run /TN $TaskName | Out-Null
|
||||
return ($LASTEXITCODE -eq 0)
|
||||
}
|
||||
|
||||
function Invoke-GuardCycle {
|
||||
param(
|
||||
[object]$Runtime,
|
||||
[string]$RuntimePath,
|
||||
[string]$LogPath
|
||||
)
|
||||
|
||||
$config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
|
||||
$stateRoot = if ($config.paths.PSObject.Properties.Name -contains 'stateRoot') { [string]$config.paths.stateRoot } else { Split-Path -Path $ConfigPath -Parent }
|
||||
$hostname = if ($config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$config.awHostname)) { [string]$config.awHostname } else { [string]$env:COMPUTERNAME }
|
||||
$apiBase = Get-AwApiBase -Config $config
|
||||
$configPaths = Get-ActivityWatchRecoveryConfigPaths -PrimaryConfigPath $ConfigPath
|
||||
$taskDefs = @(Get-ActivityWatchRecoveryTaskDefinitions -ConfigPaths $configPaths)
|
||||
$sessionRecords = @(Get-ActivityWatchSessionRecords)
|
||||
$liveSessions = @(Get-ActivityWatchLiveInteractiveSessions -SessionRecords $sessionRecords)
|
||||
$managedInteractiveSessions = @(Get-ActivityWatchManagedInteractiveSessions -TaskDefinitions $taskDefs -SessionRecords $sessionRecords -IncludeLive -IncludeDisconnected)
|
||||
$processSnapshot = Get-CollectorProcessSnapshot -Config $config
|
||||
$liveSessionIds = @($liveSessions | ForEach-Object { [int]$_.SessionId })
|
||||
$managedSessionIds = @($managedInteractiveSessions | ForEach-Object { [int]$_.SessionId } | Sort-Object -Unique)
|
||||
|
||||
$bucketChecks = [ordered]@{}
|
||||
foreach ($bucket in @(
|
||||
"aw-worktime-sessions_$hostname",
|
||||
"aw-watcher-afk_$hostname",
|
||||
"aw-watcher-window_$hostname",
|
||||
"aw-dlp-endpoint-signals_$hostname"
|
||||
)) {
|
||||
$bucketChecks[$bucket] = Get-LatestBucketAge -ApiBase $apiBase -BucketId $bucket
|
||||
}
|
||||
|
||||
$actions = New-Object System.Collections.Generic.List[object]
|
||||
$problems = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
$worktimeAge = $bucketChecks["aw-worktime-sessions_$hostname"].ageSeconds
|
||||
$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' }
|
||||
$sessionCollectorRunning = Test-ActivityWatchCollectorRunningGlobal -ScriptPath $sessionCollectorScript
|
||||
$headlessKey = 'headless:worktime-session'
|
||||
$needsHeadlessAction = (-not $sessionCollectorRunning -or $null -eq $worktimeAge -or [int]$worktimeAge -gt $HeadlessMaxAgeSeconds)
|
||||
if ($needsHeadlessAction) {
|
||||
$key = $headlessKey
|
||||
$allowed = Test-ActionAllowed -Runtime $Runtime -Key $key -CooldownSeconds $ActionCooldownSeconds -WindowSeconds $RestartWindowSeconds -MaxCount $MaxRestarts
|
||||
if ($allowed.allowed) {
|
||||
if ($Mode -eq 'enforce') {
|
||||
Start-ActivityWatchCollectorScriptGlobalIfNeeded -ScriptPath $sessionCollectorScript -ConfigPath $ConfigPath
|
||||
Register-GuardAction -Runtime $Runtime -Key $key
|
||||
Write-GuardLog -LogPath $LogPath -Message "started $key"
|
||||
$actions.Add([pscustomobject]@{ action = 'start'; target = $key; applied = $true }) | Out-Null
|
||||
}
|
||||
else {
|
||||
$actions.Add([pscustomobject]@{ action = 'start'; target = $key; applied = $false; mode = 'shadow' }) | Out-Null
|
||||
}
|
||||
}
|
||||
else {
|
||||
$problems.Add("$key action blocked: $($allowed.reason)") | Out-Null
|
||||
}
|
||||
}
|
||||
else {
|
||||
Reset-GuardActionBudget -Runtime $Runtime -Key $headlessKey
|
||||
}
|
||||
|
||||
if ($Mode -eq 'enforce') {
|
||||
Stop-ActivityWatchProcessesInNonLiveSessions -SessionRecords $sessionRecords -Config $config -TaskDefinitions $taskDefs -PreserveManagedSessions
|
||||
}
|
||||
|
||||
$interactiveStale = $false
|
||||
foreach ($bucket in @("aw-watcher-afk_$hostname", "aw-watcher-window_$hostname", "aw-dlp-endpoint-signals_$hostname")) {
|
||||
$age = $bucketChecks[$bucket].ageSeconds
|
||||
if ($null -eq $age -or [int]$age -gt $InteractiveMaxAgeSeconds) {
|
||||
$interactiveStale = $true
|
||||
}
|
||||
}
|
||||
|
||||
$watchersInLiveSessions = @($processSnapshot.watchers | Where-Object { $liveSessionIds -contains [int]$_.sessionId })
|
||||
$watchersInManagedSessions = @($processSnapshot.watchers | Where-Object { $managedSessionIds -contains [int]$_.sessionId })
|
||||
$liveWatcherMissing = $false
|
||||
if ($liveSessions.Count -gt 0) {
|
||||
$hasAfk = @($watchersInLiveSessions | Where-Object { [string]$_.name -ieq 'aw-watcher-afk' }).Count -gt 0
|
||||
$hasWindow = @($watchersInLiveSessions | Where-Object { [string]$_.name -ieq 'aw-watcher-window' }).Count -gt 0
|
||||
$liveWatcherMissing = (-not $hasAfk) -or (-not $hasWindow)
|
||||
}
|
||||
$managedWatcherMissing = $false
|
||||
if ($managedInteractiveSessions.Count -gt 0) {
|
||||
$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 }
|
||||
foreach ($managedSession in @($managedInteractiveSessions)) {
|
||||
$sessionId = [int]$managedSession.SessionId
|
||||
$sessionWatchers = @($watchersInManagedSessions | Where-Object { [int]$_.sessionId -eq $sessionId })
|
||||
$hasManagedAfk = @($sessionWatchers | Where-Object { [string]$_.name -ieq 'aw-watcher-afk' }).Count -gt 0
|
||||
$hasManagedWindow = @($sessionWatchers | Where-Object { [string]$_.name -ieq 'aw-watcher-window' }).Count -gt 0
|
||||
if (($afkEnabled -and -not $hasManagedAfk) -or ($windowEnabled -and -not $hasManagedWindow)) {
|
||||
$managedWatcherMissing = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($managedInteractiveSessions.Count -eq 0 -and $liveSessions.Count -gt 0 -and $interactiveStale) {
|
||||
$problems.Add('interactive buckets stale but no managed interactive sessions found') | Out-Null
|
||||
}
|
||||
|
||||
$needsInteractiveTaskAction = $managedInteractiveSessions.Count -gt 0 -and (
|
||||
$liveWatcherMissing -or
|
||||
$managedWatcherMissing -or
|
||||
($interactiveStale -and $liveSessions.Count -gt 0)
|
||||
)
|
||||
|
||||
if ($needsInteractiveTaskAction) {
|
||||
foreach ($taskDef in $taskDefs) {
|
||||
if (-not (Test-ActivityWatchUserHasManagedSession -UserId ([string]$taskDef.userId) -SessionRecords $sessionRecords -IncludeLive -IncludeDisconnected)) {
|
||||
continue
|
||||
}
|
||||
$key = "task:$($taskDef.taskName)"
|
||||
$allowed = Test-ActionAllowed -Runtime $Runtime -Key $key -CooldownSeconds $InteractiveActionCooldownSeconds -WindowSeconds $RestartWindowSeconds -MaxCount $MaxRestarts
|
||||
if (-not $allowed.allowed) {
|
||||
$problems.Add("$key action blocked: $($allowed.reason)") | Out-Null
|
||||
continue
|
||||
}
|
||||
|
||||
if ($Mode -eq 'enforce') {
|
||||
$ok = Invoke-ExactTaskRun -TaskName ([string]$taskDef.taskName)
|
||||
if ($ok) {
|
||||
Register-GuardAction -Runtime $Runtime -Key $key
|
||||
}
|
||||
Write-GuardLog -LogPath $LogPath -Message ("run {0} ok={1}" -f $key, $ok)
|
||||
$actions.Add([pscustomobject]@{ action = 'run-task'; target = [string]$taskDef.taskName; applied = $true; ok = $ok }) | Out-Null
|
||||
}
|
||||
else {
|
||||
$actions.Add([pscustomobject]@{ action = 'run-task'; target = [string]$taskDef.taskName; applied = $false; mode = 'shadow' }) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
foreach ($taskDef in $taskDefs) {
|
||||
if (Test-ActivityWatchUserHasManagedSession -UserId ([string]$taskDef.userId) -SessionRecords $sessionRecords -IncludeLive -IncludeDisconnected) {
|
||||
Reset-GuardActionBudget -Runtime $Runtime -Key "task:$($taskDef.taskName)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$status = 'ok'
|
||||
if ($problems.Count -gt 0) {
|
||||
$status = 'warn'
|
||||
}
|
||||
if ($managedInteractiveSessions.Count -gt 0 -and $interactiveStale -and $Mode -eq 'shadow') {
|
||||
$status = 'warn'
|
||||
}
|
||||
|
||||
$sessionState = @(
|
||||
foreach ($session in @($sessionRecords)) {
|
||||
[pscustomobject]@{
|
||||
SessionName = [string]$session.SessionName
|
||||
UserName = [string]$session.UserName
|
||||
SessionId = [int]$session.SessionId
|
||||
State = [string]$session.State
|
||||
IsLive = [bool]$session.IsLive
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$state = @{}
|
||||
$state['status'] = $status
|
||||
$state['mode'] = $Mode
|
||||
$state['host'] = $hostname
|
||||
$state['generatedAtUtc'] = (Get-Date).ToUniversalTime().ToString('o')
|
||||
$state['pid'] = $PID
|
||||
$state['sessions'] = @($sessionState)
|
||||
$state['liveSessionCount'] = $liveSessions.Count
|
||||
$state['managedSessionCount'] = $managedInteractiveSessions.Count
|
||||
$state['managedSessions'] = @($managedInteractiveSessions)
|
||||
$bucketState = @{}
|
||||
foreach ($key in $bucketChecks.Keys) {
|
||||
$bucketState[$key] = $bucketChecks[$key]
|
||||
}
|
||||
|
||||
$state['processes'] = $processSnapshot
|
||||
$state['buckets'] = $bucketState
|
||||
$state['actions'] = @($actions.ToArray())
|
||||
$state['problems'] = @($problems.ToArray())
|
||||
$state['quarantine'] = $Runtime.quarantine
|
||||
|
||||
$statePath = Join-Path $stateRoot 'collector-guard-state.json'
|
||||
$state | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $statePath -Encoding UTF8
|
||||
Write-GuardRuntime -Path $RuntimePath -Runtime $Runtime
|
||||
[void](Send-GuardHeartbeat -ApiBase $apiBase -Hostname $hostname -State $state -PulseSeconds ([Math]::Max($LoopSeconds * 2, 60)))
|
||||
return $state
|
||||
}
|
||||
|
||||
if ($SelfTest) {
|
||||
Invoke-GuardSelfTest
|
||||
exit 0
|
||||
}
|
||||
|
||||
$initialConfig = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
|
||||
$initialStateRoot = if ($initialConfig.paths.PSObject.Properties.Name -contains 'stateRoot') { [string]$initialConfig.paths.stateRoot } else { Split-Path -Path $ConfigPath -Parent }
|
||||
$initialLogsRoot = if ($initialConfig.paths.PSObject.Properties.Name -contains 'logsRoot') { [string]$initialConfig.paths.logsRoot } else { Join-Path $initialStateRoot 'logs' }
|
||||
$logPath = Join-Path $initialLogsRoot 'collector-guard.log'
|
||||
$runtimePath = Join-Path $initialStateRoot 'collector-guard-runtime.json'
|
||||
$lockPath = New-GuardLock -StateRoot $initialStateRoot
|
||||
if (-not $lockPath) {
|
||||
Write-GuardLog -LogPath $logPath -Message 'another collector guard instance is already running'
|
||||
exit 0
|
||||
}
|
||||
|
||||
try {
|
||||
$runtime = Read-GuardRuntime -Path $runtimePath
|
||||
Write-GuardLog -LogPath $logPath -Message "collector guard started mode=$Mode loop=$LoopSeconds once=$($Once.IsPresent)"
|
||||
while ($true) {
|
||||
try {
|
||||
Invoke-GuardCycle -Runtime $runtime -RuntimePath $runtimePath -LogPath $logPath | Out-Null
|
||||
}
|
||||
catch {
|
||||
Write-GuardLog -LogPath $logPath -Message ("cycle error: {0}; at {1}" -f $_.Exception.Message, $_.ScriptStackTrace)
|
||||
}
|
||||
|
||||
if ($Once) {
|
||||
break
|
||||
}
|
||||
Start-Sleep -Seconds ([Math]::Max($LoopSeconds, 15))
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($lockPath -and (Test-Path -LiteralPath $lockPath)) {
|
||||
Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Write-GuardLog -LogPath $logPath -Message 'collector guard stopped'
|
||||
}
|
||||
@@ -27,7 +27,7 @@ param(
|
||||
[int]$EvtxRetentionDays = 14,
|
||||
[string[]]$EvtxChannels = @(),
|
||||
[bool]$LogonMarkerEnabled = $true,
|
||||
[bool]$ProcessEventsEnabled = $true,
|
||||
[bool]$ProcessEventsEnabled = $false,
|
||||
[string]$AwHostname,
|
||||
[string]$CustomRulesPath,
|
||||
[string]$CustomPolicyPath,
|
||||
|
||||
@@ -27,7 +27,7 @@ param(
|
||||
[int]$EvtxRetentionDays = 14,
|
||||
[string[]]$EvtxChannels = @(),
|
||||
[bool]$LogonMarkerEnabled = $true,
|
||||
[bool]$ProcessEventsEnabled = $true,
|
||||
[bool]$ProcessEventsEnabled = $false,
|
||||
[string]$AwHostname,
|
||||
[string]$CustomRulesPath,
|
||||
[string]$CustomPolicyPath,
|
||||
|
||||
@@ -25,7 +25,7 @@ param(
|
||||
[int]$EvtxRetentionDays = 14,
|
||||
[string[]]$EvtxChannels = @(),
|
||||
[bool]$LogonMarkerEnabled = $true,
|
||||
[bool]$ProcessEventsEnabled = $true,
|
||||
[bool]$ProcessEventsEnabled = $false,
|
||||
[string]$AwHostname,
|
||||
[string]$CustomRulesPath,
|
||||
[string]$CustomPolicyPath
|
||||
|
||||
@@ -15,7 +15,8 @@ param(
|
||||
[int]$PolicyRefreshSeconds,
|
||||
[string]$PolicyCachePath,
|
||||
[string]$LogPath,
|
||||
[int]$PollSeconds
|
||||
[int]$PollSeconds,
|
||||
[switch]$SelfTestSuppressedBlock
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -30,6 +31,7 @@ catch {
|
||||
|
||||
$script:TransportQueuePath = $null
|
||||
$script:TransportQueueLockPath = $null
|
||||
$script:SessionId = [System.Diagnostics.Process]::GetCurrentProcess().SessionId
|
||||
$script:TransportMetrics = @{
|
||||
eventsEnqueued = 0
|
||||
eventsFlushed = 0
|
||||
@@ -71,6 +73,21 @@ function Write-EndpointLog {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-QueueNameToken {
|
||||
param(
|
||||
[string]$UserName,
|
||||
[int]$SessionId
|
||||
)
|
||||
$token = ('{0}-s{1}' -f $UserName, $SessionId)
|
||||
foreach ($ch in [System.IO.Path]::GetInvalidFileNameChars()) {
|
||||
$token = $token.Replace([string]$ch, '_')
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($token)) {
|
||||
return ('session-{0}' -f $SessionId)
|
||||
}
|
||||
return $token
|
||||
}
|
||||
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
@@ -129,11 +146,20 @@ function Invoke-AwJsonPost {
|
||||
|
||||
function Initialize-TransportQueue {
|
||||
param([Parameter(Mandatory = $true)][string]$StateRoot)
|
||||
$script:TransportQueuePath = Join-Path $StateRoot 'dlp-endpoint-signals-queue.jsonl'
|
||||
$script:TransportQueueLockPath = Join-Path $StateRoot 'dlp-endpoint-signals-queue.lock'
|
||||
$queueToken = Get-QueueNameToken -UserName $env:USERNAME -SessionId $script:SessionId
|
||||
$script:TransportQueuePath = Join-Path $StateRoot ("dlp-endpoint-signals-queue-{0}.jsonl" -f $queueToken)
|
||||
$script:TransportQueueLockPath = Join-Path $StateRoot ("dlp-endpoint-signals-queue-{0}.lock" -f $queueToken)
|
||||
if (-not (Test-Path -LiteralPath $script:TransportQueuePath)) {
|
||||
New-Item -Path $script:TransportQueuePath -ItemType File -Force | Out-Null
|
||||
}
|
||||
$legacyQueuePath = Join-Path $StateRoot 'dlp-endpoint-signals-queue.jsonl'
|
||||
if (Test-Path -LiteralPath $legacyQueuePath) {
|
||||
$legacyItems = @(Get-Content -LiteralPath $legacyQueuePath -ErrorAction SilentlyContinue | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
if ($legacyItems.Count -gt 0) {
|
||||
Add-Content -LiteralPath $script:TransportQueuePath -Value $legacyItems -Encoding UTF8
|
||||
Clear-Content -LiteralPath $legacyQueuePath -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-TransportQueueLock {
|
||||
@@ -617,6 +643,17 @@ function Load-DlpPolicy {
|
||||
regexPack = $null
|
||||
ocrEnabled = $false
|
||||
}
|
||||
nativeControls = [ordered]@{
|
||||
mode = 'monitor'
|
||||
rollout = [ordered]@{
|
||||
allowGlobalBlock = $false
|
||||
}
|
||||
channels = [ordered]@{
|
||||
clipboard = [ordered]@{ action = 'audit' }
|
||||
usb = [ordered]@{ action = 'audit' }
|
||||
print = [ordered]@{ action = 'audit' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$script:PolicySource = 'defaults'
|
||||
@@ -656,6 +693,28 @@ function Load-DlpPolicy {
|
||||
$script:Policy.contentAnalysis.ocrEnabled = [bool]$raw.contentAnalysis.ocrEnabled
|
||||
}
|
||||
}
|
||||
if ($raw.nativeControls) {
|
||||
$nativeProps = @($raw.nativeControls.PSObject.Properties.Name)
|
||||
if ($nativeProps -contains 'mode' -and $raw.nativeControls.mode) {
|
||||
$script:Policy.nativeControls.mode = ([string]$raw.nativeControls.mode).ToLowerInvariant()
|
||||
}
|
||||
if ($nativeProps -contains 'rollout' -and $raw.nativeControls.rollout) {
|
||||
$rolloutProps = @($raw.nativeControls.rollout.PSObject.Properties.Name)
|
||||
if ($rolloutProps -contains 'allowGlobalBlock') {
|
||||
$script:Policy.nativeControls.rollout.allowGlobalBlock = [bool]$raw.nativeControls.rollout.allowGlobalBlock
|
||||
}
|
||||
}
|
||||
if ($nativeProps -contains 'channels' -and $raw.nativeControls.channels) {
|
||||
foreach ($channelName in @('clipboard', 'usb', 'print')) {
|
||||
if (@($raw.nativeControls.channels.PSObject.Properties.Name) -contains $channelName) {
|
||||
$channel = $raw.nativeControls.channels.$channelName
|
||||
if ($channel -and (@($channel.PSObject.Properties.Name) -contains 'action') -and $channel.action) {
|
||||
$script:Policy.nativeControls.channels[$channelName].action = ([string]$channel.action).ToLowerInvariant()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$script:PolicySource = 'local'
|
||||
}
|
||||
catch {
|
||||
@@ -663,6 +722,68 @@ function Load-DlpPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-DlpEffectiveAction {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$RequestedAction,
|
||||
[Parameter(Mandatory = $true)][ValidateSet('clipboard', 'usb', 'print')][string]$Channel
|
||||
)
|
||||
|
||||
$requested = $RequestedAction.ToLowerInvariant()
|
||||
$mode = ([string]$script:Policy.nativeControls.mode).ToLowerInvariant()
|
||||
$allowGlobalBlock = [bool]$script:Policy.nativeControls.rollout.allowGlobalBlock
|
||||
$channelAction = 'audit'
|
||||
try {
|
||||
$channelAction = ([string]$script:Policy.nativeControls.channels[$Channel].action).ToLowerInvariant()
|
||||
}
|
||||
catch {
|
||||
$channelAction = 'audit'
|
||||
}
|
||||
|
||||
$suppressed = $false
|
||||
$effective = $requested
|
||||
if ($requested -eq 'block') {
|
||||
$channelAllowsBlock = $channelAction -in @('block', 'blockwithoverride')
|
||||
if ($mode -ne 'enforce' -or -not $allowGlobalBlock -or -not $channelAllowsBlock) {
|
||||
$effective = 'alert'
|
||||
$suppressed = $true
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
requestedAction = $requested
|
||||
action = $effective
|
||||
enforcementMode = $mode
|
||||
nativeChannelAction = $channelAction
|
||||
enforcementSuppressed = $suppressed
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-SuppressedBlockSelfTest {
|
||||
$decisions = @()
|
||||
foreach ($channel in @('clipboard', 'usb', 'print')) {
|
||||
$decisions += (Resolve-DlpEffectiveAction -RequestedAction 'block' -Channel $channel)
|
||||
}
|
||||
|
||||
$failed = @(
|
||||
$decisions |
|
||||
Where-Object { $_.action -eq 'block' -or -not [bool]$_.enforcementSuppressed }
|
||||
)
|
||||
|
||||
$result = [ordered]@{
|
||||
ok = (@($failed).Count -eq 0)
|
||||
test = 'suppressed-block-in-monitor'
|
||||
policySource = $script:PolicySource
|
||||
policyMode = $script:PolicyMode
|
||||
decisions = @($decisions)
|
||||
}
|
||||
|
||||
$result | ConvertTo-Json -Depth 6
|
||||
if (-not $result.ok) {
|
||||
exit 2
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
|
||||
function Test-ValidInn {
|
||||
param([string]$Value)
|
||||
$digits = ($Value -replace '\D', '')
|
||||
@@ -925,7 +1046,9 @@ function Evaluate-ClipboardRules {
|
||||
$fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME"
|
||||
if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue }
|
||||
|
||||
$action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action }
|
||||
$requestedAction = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action }
|
||||
$actionDecision = Resolve-DlpEffectiveAction -RequestedAction $requestedAction -Channel 'clipboard'
|
||||
$action = [string]$actionDecision.action
|
||||
$severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity }
|
||||
$message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" }
|
||||
|
||||
@@ -939,13 +1062,17 @@ function Evaluate-ClipboardRules {
|
||||
clipboardHash = $ClipboardHash
|
||||
clipboardLength = $ClipboardText.Length
|
||||
enforced = $enforced
|
||||
requestedAction = [string]$actionDecision.requestedAction
|
||||
enforcementMode = [string]$actionDecision.enforcementMode
|
||||
nativeChannelAction = [string]$actionDecision.nativeChannelAction
|
||||
enforcementSuppressed = [bool]$actionDecision.enforcementSuppressed
|
||||
dictionaryPack = $dictionaryPack
|
||||
regexPack = $regexPack
|
||||
dictionaryMatches = @($advanced.dictionaryMatches)
|
||||
regexMatches = @($advanced.regexMatches)
|
||||
ocrRequested = $ocrEnabled
|
||||
}
|
||||
Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced)
|
||||
Write-EndpointLog ("incident clipboard rule={0} requested={1} action={2} severity={3} enforced={4} suppressed={5}" -f $ruleId, $requestedAction, $action, $severity, $enforced, [bool]$actionDecision.enforcementSuppressed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -965,7 +1092,9 @@ function Evaluate-UsbRules {
|
||||
$fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME"
|
||||
if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue }
|
||||
|
||||
$action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action }
|
||||
$requestedAction = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action }
|
||||
$actionDecision = Resolve-DlpEffectiveAction -RequestedAction $requestedAction -Channel 'usb'
|
||||
$action = [string]$actionDecision.action
|
||||
$severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity }
|
||||
$message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" }
|
||||
|
||||
@@ -979,8 +1108,12 @@ function Evaluate-UsbRules {
|
||||
driveLetter = $DriveLetter
|
||||
volumeName = $VolumeName
|
||||
enforced = $enforced
|
||||
requestedAction = [string]$actionDecision.requestedAction
|
||||
enforcementMode = [string]$actionDecision.enforcementMode
|
||||
nativeChannelAction = [string]$actionDecision.nativeChannelAction
|
||||
enforcementSuppressed = [bool]$actionDecision.enforcementSuppressed
|
||||
}
|
||||
Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced)
|
||||
Write-EndpointLog ("incident usb rule={0} requested={1} action={2} severity={3} drive={4} enforced={5} suppressed={6}" -f $ruleId, $requestedAction, $action, $severity, $DriveLetter, $enforced, [bool]$actionDecision.enforcementSuppressed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1016,7 +1149,9 @@ function Evaluate-PrintRules {
|
||||
$fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME"
|
||||
if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue }
|
||||
|
||||
$action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action }
|
||||
$requestedAction = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action }
|
||||
$actionDecision = Resolve-DlpEffectiveAction -RequestedAction $requestedAction -Channel 'print'
|
||||
$action = [string]$actionDecision.action
|
||||
$severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity }
|
||||
$message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" }
|
||||
|
||||
@@ -1031,13 +1166,17 @@ function Evaluate-PrintRules {
|
||||
documentName = $DocumentName
|
||||
owner = $Owner
|
||||
enforced = $enforced
|
||||
requestedAction = [string]$actionDecision.requestedAction
|
||||
enforcementMode = [string]$actionDecision.enforcementMode
|
||||
nativeChannelAction = [string]$actionDecision.nativeChannelAction
|
||||
enforcementSuppressed = [bool]$actionDecision.enforcementSuppressed
|
||||
dictionaryPack = $dictionaryPack
|
||||
regexPack = $regexPack
|
||||
dictionaryMatches = @($advanced.dictionaryMatches)
|
||||
regexMatches = @($advanced.regexMatches)
|
||||
ocrRequested = $ocrEnabled
|
||||
}
|
||||
Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced)
|
||||
Write-EndpointLog ("incident print rule={0} requested={1} action={2} severity={3} printer={4} enforced={5} suppressed={6}" -f $ruleId, $requestedAction, $action, $severity, $PrinterName, $enforced, [bool]$actionDecision.enforcementSuppressed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1320,6 +1459,9 @@ $script:LastEventTime = $null
|
||||
|
||||
Initialize-TransportQueue -StateRoot $resolvedStateRoot
|
||||
Initialize-DlpPolicy
|
||||
if ($SelfTestSuppressedBlock) {
|
||||
Invoke-SuppressedBlockSelfTest
|
||||
}
|
||||
Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase)
|
||||
|
||||
while ($true) {
|
||||
|
||||
@@ -154,4 +154,6 @@ function Save-CachedDlpPolicyBundle {
|
||||
Set-Content -LiteralPath $CachePath -Value $json -Encoding UTF8
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-DlpPolicyGetJson, Get-RemoteDlpPolicyBundle, Get-RemoteDlpPolicyDesired, Send-DlpPolicyAgentHeartbeat, Read-CachedDlpPolicyBundle, Save-CachedDlpPolicyBundle
|
||||
if ($ExecutionContext.SessionState.Module) {
|
||||
Export-ModuleMember -Function Invoke-DlpPolicyGetJson, Get-RemoteDlpPolicyBundle, Get-RemoteDlpPolicyDesired, Send-DlpPolicyAgentHeartbeat, Read-CachedDlpPolicyBundle, Save-CachedDlpPolicyBundle
|
||||
}
|
||||
|
||||
@@ -6,6 +6,19 @@
|
||||
"action": "log",
|
||||
"severity": "low"
|
||||
},
|
||||
"nativeControls": {
|
||||
"mode": "monitor",
|
||||
"rollout": {
|
||||
"baselineDays": 7,
|
||||
"requireGuardHeartbeat": true,
|
||||
"allowGlobalBlock": false
|
||||
},
|
||||
"channels": {
|
||||
"clipboard": {"action": "audit"},
|
||||
"usb": {"action": "audit"},
|
||||
"print": {"action": "audit"}
|
||||
}
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"id": "personal-web-during-workhours",
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"version": 1,
|
||||
"defaults": {
|
||||
"enabled": true,
|
||||
"cooldownSeconds": 300,
|
||||
"action": "alert",
|
||||
"severity": "medium"
|
||||
},
|
||||
"nativeControls": {
|
||||
"mode": "monitor",
|
||||
"rollout": {
|
||||
"baselineDays": 7,
|
||||
"requireGuardHeartbeat": true,
|
||||
"allowGlobalBlock": false
|
||||
},
|
||||
"channels": {
|
||||
"removableStorage": {
|
||||
"action": "audit",
|
||||
"windows": {
|
||||
"mechanisms": ["gpo-device-restrictions", "set-disk-readonly"],
|
||||
"target": "write"
|
||||
},
|
||||
"linux": {
|
||||
"mechanisms": ["fanotify", "auditd"],
|
||||
"target": "mountpoints"
|
||||
},
|
||||
"macos": {
|
||||
"mechanisms": ["mdm-restrictions", "endpoint-security"],
|
||||
"target": "managed-devices"
|
||||
},
|
||||
"chromeos": {
|
||||
"mechanisms": ["data-controls"],
|
||||
"target": "removable-storage"
|
||||
}
|
||||
},
|
||||
"print": {
|
||||
"action": "audit",
|
||||
"windows": {
|
||||
"mechanisms": ["printservice-operational-log", "spooler-cancel-job"]
|
||||
},
|
||||
"linux": {
|
||||
"mechanisms": ["cups-logs"],
|
||||
"enforcement": "monitor-only"
|
||||
},
|
||||
"macos": {
|
||||
"mechanisms": ["mdm-printing-restrictions"],
|
||||
"enforcement": "managed-only"
|
||||
},
|
||||
"chromeos": {
|
||||
"mechanisms": ["data-controls"]
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"action": "audit",
|
||||
"windows": {
|
||||
"mechanisms": ["clipboard-monitor", "clear-clipboard"],
|
||||
"blockScope": "high-confidence-only"
|
||||
},
|
||||
"linux": {
|
||||
"mechanisms": ["desktop-clipboard-monitor"],
|
||||
"enforcement": "monitor-only"
|
||||
},
|
||||
"macos": {
|
||||
"mechanisms": ["endpoint-monitor", "mdm-restrictions"],
|
||||
"enforcement": "monitor-first"
|
||||
},
|
||||
"chromeos": {
|
||||
"mechanisms": ["data-controls"]
|
||||
}
|
||||
},
|
||||
"browserUpload": {
|
||||
"action": "audit",
|
||||
"windows": {
|
||||
"mechanisms": ["managed-browser-policy", "browser-extension"]
|
||||
},
|
||||
"linux": {
|
||||
"mechanisms": ["managed-browser-policy", "proxy-logs"]
|
||||
},
|
||||
"macos": {
|
||||
"mechanisms": ["managed-browser-policy", "network-extension"]
|
||||
},
|
||||
"chromeos": {
|
||||
"mechanisms": ["data-controls"]
|
||||
}
|
||||
},
|
||||
"appExecution": {
|
||||
"action": "audit",
|
||||
"windows": {
|
||||
"mechanisms": ["applocker", "wdac"]
|
||||
},
|
||||
"linux": {
|
||||
"mechanisms": ["auditd", "fanotify", "bpf-lsm"],
|
||||
"enforcement": "fanotify-or-lsm-only"
|
||||
},
|
||||
"macos": {
|
||||
"mechanisms": ["mdm-restrictions", "endpoint-security"]
|
||||
},
|
||||
"chromeos": {
|
||||
"mechanisms": ["admin-console-app-policy"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"endpoint": {
|
||||
"clipboard": [
|
||||
{
|
||||
"id": "clipboard-sensitive-keywords",
|
||||
"enabled": true,
|
||||
"cooldownSeconds": 300,
|
||||
"action": "alert",
|
||||
"severity": "high",
|
||||
"message": "В буфере обнаружены чувствительные ключевые слова",
|
||||
"minLength": 20,
|
||||
"regexPatterns": [
|
||||
"(?i)парол(ь|и)",
|
||||
"(?i)password",
|
||||
"(?i)secret",
|
||||
"(?i)cvv",
|
||||
"(?i)паспорт"
|
||||
]
|
||||
}
|
||||
],
|
||||
"usb": [
|
||||
{
|
||||
"id": "usb-media-connected",
|
||||
"enabled": true,
|
||||
"cooldownSeconds": 300,
|
||||
"action": "alert",
|
||||
"severity": "medium",
|
||||
"message": "Подключен съемный носитель"
|
||||
}
|
||||
],
|
||||
"print": [
|
||||
{
|
||||
"id": "print-sensitive-docs",
|
||||
"enabled": true,
|
||||
"cooldownSeconds": 300,
|
||||
"action": "alert",
|
||||
"severity": "high",
|
||||
"message": "Печать документа с признаками чувствительных данных",
|
||||
"documentRegex": "(?i)(salary|зарплат|passport|паспорт|договор|contract)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,21 @@ function Write-StartupTrace {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-QueueNameToken {
|
||||
param(
|
||||
[string]$UserName,
|
||||
[int]$SessionId
|
||||
)
|
||||
$token = ('{0}-s{1}' -f $UserName, $SessionId)
|
||||
foreach ($ch in [System.IO.Path]::GetInvalidFileNameChars()) {
|
||||
$token = $token.Replace([string]$ch, '_')
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($token)) {
|
||||
return ('session-{0}' -f $SessionId)
|
||||
}
|
||||
return $token
|
||||
}
|
||||
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
@@ -97,11 +112,20 @@ function Initialize-TransportQueue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$StateRoot
|
||||
)
|
||||
$script:TransportQueuePath = Join-Path $StateRoot 'file-operations-queue.jsonl'
|
||||
$script:TransportQueueLockPath = Join-Path $StateRoot 'file-operations-queue.lock'
|
||||
$queueToken = Get-QueueNameToken -UserName $env:USERNAME -SessionId $script:SessionId
|
||||
$script:TransportQueuePath = Join-Path $StateRoot ("file-operations-queue-{0}.jsonl" -f $queueToken)
|
||||
$script:TransportQueueLockPath = Join-Path $StateRoot ("file-operations-queue-{0}.lock" -f $queueToken)
|
||||
if (-not (Test-Path -LiteralPath $script:TransportQueuePath)) {
|
||||
New-Item -Path $script:TransportQueuePath -ItemType File -Force | Out-Null
|
||||
}
|
||||
$legacyQueuePath = Join-Path $StateRoot 'file-operations-queue.jsonl'
|
||||
if (Test-Path -LiteralPath $legacyQueuePath) {
|
||||
$legacyItems = @(Get-Content -LiteralPath $legacyQueuePath -ErrorAction SilentlyContinue | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
if ($legacyItems.Count -gt 0) {
|
||||
Add-Content -LiteralPath $script:TransportQueuePath -Value $legacyItems -Encoding UTF8
|
||||
Clear-Content -LiteralPath $legacyQueuePath -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-TransportQueueLock {
|
||||
|
||||
@@ -94,7 +94,7 @@ $effectiveEvtxExportRoot = if ($PSBoundParameters.ContainsKey('EvtxExportRoot')
|
||||
$effectiveEvtxRetentionDays = if ($PSBoundParameters.ContainsKey('EvtxRetentionDays')) { [int]$EvtxRetentionDays } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'retentionDays') { [int]$existingConfig.forensics.retentionDays } else { 14 }
|
||||
$effectiveEvtxChannels = if ($PSBoundParameters.ContainsKey('EvtxChannels')) { @($EvtxChannels) } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'forensics' -and $existingConfig.forensics.PSObject.Properties.Name -contains 'evtxChannels') { @($existingConfig.forensics.evtxChannels) } else { @() }
|
||||
$effectiveLogonMarkerEnabled = if ($PSBoundParameters.ContainsKey('LogonMarkerEnabled')) { [bool]$LogonMarkerEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'sessionEvents' -and $existingConfig.sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$existingConfig.sessionEvents.logonEnabled } else { $true }
|
||||
$effectiveProcessEventsEnabled = if ($PSBoundParameters.ContainsKey('ProcessEventsEnabled')) { [bool]$ProcessEventsEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'sessionEvents' -and $existingConfig.sessionEvents.PSObject.Properties.Name -contains 'processEventsEnabled') { [bool]$existingConfig.sessionEvents.processEventsEnabled } else { $true }
|
||||
$effectiveProcessEventsEnabled = if ($PSBoundParameters.ContainsKey('ProcessEventsEnabled')) { [bool]$ProcessEventsEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'sessionEvents' -and $existingConfig.sessionEvents.PSObject.Properties.Name -contains 'processEventsEnabled') { [bool]$existingConfig.sessionEvents.processEventsEnabled } else { $false }
|
||||
$effectiveAwHostname = if ($PSBoundParameters.ContainsKey('AwHostname') -and -not [string]::IsNullOrWhiteSpace($AwHostname)) { [string]$AwHostname } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$existingConfig.awHostname)) { [string]$existingConfig.awHostname } else { [string]$env:COMPUTERNAME }
|
||||
$effectiveVersion = if ($Version) { $Version } elseif ($existingConfig) { [string]$existingConfig.package.version } else { 'v0.13.2' }
|
||||
$effectivePolicyMode = if ($PSBoundParameters.ContainsKey('PolicyMode') -and $PolicyMode) { [string]$PolicyMode } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'policyEngine' -and $existingConfig.policyEngine.PSObject.Properties.Name -contains 'mode') { [string]$existingConfig.policyEngine.mode } else { 'local' }
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[ValidateSet('shadow', 'enforce')]
|
||||
[string]$Mode = 'shadow',
|
||||
[string]$ServiceName = 'AWatchRusCollectorGuard',
|
||||
[int]$LoopSeconds = 60,
|
||||
[switch]$DisableRecoveryTask
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Assert-Admin {
|
||||
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]::new($id)
|
||||
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
throw 'Run as Administrator.'
|
||||
}
|
||||
}
|
||||
|
||||
Assert-Admin
|
||||
|
||||
$guardScriptPath = Join-Path $PSScriptRoot 'aw-collector-guard.ps1'
|
||||
$serviceSourcePath = Join-Path $PSScriptRoot 'AWatchRusCollectorGuardService.cs'
|
||||
$serviceExePath = Join-Path $PSScriptRoot 'AWatchRusCollectorGuardService.exe'
|
||||
if (-not (Test-Path -LiteralPath $guardScriptPath)) {
|
||||
throw "Collector guard script not found: $guardScriptPath"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $serviceSourcePath)) {
|
||||
throw "Collector guard service source not found: $serviceSourcePath"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $ConfigPath)) {
|
||||
throw "Config not found: $ConfigPath"
|
||||
}
|
||||
|
||||
$cscCandidates = @(
|
||||
(Join-Path $env:WINDIR 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'),
|
||||
(Join-Path $env:WINDIR 'Microsoft.NET\Framework\v4.0.30319\csc.exe')
|
||||
)
|
||||
$csc = @($cscCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1)
|
||||
if (-not $csc) {
|
||||
throw 'C# compiler not found. Install .NET Framework build tools or provide AWatchRusCollectorGuardService.exe.'
|
||||
}
|
||||
|
||||
$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||
if ($existing) {
|
||||
if ($existing.Status -ne 'Stopped') {
|
||||
Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
|
||||
try {
|
||||
$existing.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(20))
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
sc.exe delete $ServiceName | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
& $csc /nologo /target:exe /optimize+ /out:$serviceExePath /reference:System.ServiceProcess.dll $serviceSourcePath | Out-Null
|
||||
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $serviceExePath)) {
|
||||
throw "Failed to compile $serviceExePath"
|
||||
}
|
||||
|
||||
$logsRoot = Join-Path (Split-Path -Path $ConfigPath -Parent) 'logs'
|
||||
$serviceLogPath = Join-Path $logsRoot 'collector-guard-service.log'
|
||||
$binPath = "`"$serviceExePath`" --service-name `"$ServiceName`" --script `"$guardScriptPath`" --config `"$ConfigPath`" --mode $Mode --loop $LoopSeconds --log `"$serviceLogPath`""
|
||||
|
||||
New-Service -Name $ServiceName -BinaryPathName $binPath -DisplayName 'AWatch-rus Collector Guard' -StartupType Automatic | Out-Null
|
||||
sc.exe description $ServiceName "Session-aware ActivityWatch collector guard for AWatch-rus" | Out-Null
|
||||
sc.exe failure $ServiceName reset= 300 actions= restart/5000/restart/15000/restart/60000 | Out-Null
|
||||
|
||||
if ($DisableRecoveryTask) {
|
||||
Write-Warning 'DisableRecoveryTask is deprecated and ignored: ActivityWatch Recovery must remain enabled as collector guard fallback.'
|
||||
}
|
||||
else {
|
||||
try {
|
||||
Enable-ScheduledTask -TaskName 'ActivityWatch Recovery' -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
sc.exe start $ServiceName | Out-Null
|
||||
|
||||
Write-Output "Collector guard service installed: $ServiceName"
|
||||
Write-Output "Mode: $Mode"
|
||||
Write-Output "Config: $ConfigPath"
|
||||
@@ -44,6 +44,9 @@ Source: "..\..\aw-standalone-service.ps1"; DestDir: "{app}\windows"; Flags: igno
|
||||
Source: "..\..\deploy-single-user.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\deploy-domain-users.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\deploy-ensemble.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\AWatchRusCollectorGuardService.cs"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\aw-collector-guard.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\install-collector-guard-service.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\hardening-recovery.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\validate-deployment.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\migrate-awatch-rus-paths.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
@@ -54,6 +57,7 @@ Source: "..\..\file-operations-collector.ps1"; DestDir: "{app}\windows"; Flags:
|
||||
Source: "..\..\email-outbound-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\web-category-rules.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\dlp-policy.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
Source: "..\..\dlp-policy.native-cross-os.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion
|
||||
; Offline payload (optional): place ZIP into windows/installkit/innosetup/payload/ before compiling.
|
||||
Source: "payload\{#AwDefaultZipName}"; DestDir: "{app}\payload"; Flags: ignoreversion skipifsourcedoesntexist
|
||||
Source: "innosetup-rdp-package-filelist.md"; DestDir: "{app}\windows\installkit\innosetup"; Flags: ignoreversion
|
||||
|
||||
Binary file not shown.
@@ -29,6 +29,9 @@
|
||||
- `windows/deploy-single-user.ps1`
|
||||
- `windows/deploy-domain-users.ps1`
|
||||
- `windows/deploy-ensemble.ps1`
|
||||
- `windows/AWatchRusCollectorGuardService.cs`
|
||||
- `windows/aw-collector-guard.ps1`
|
||||
- `windows/install-collector-guard-service.ps1`
|
||||
- `windows/hardening-recovery.ps1`
|
||||
- `windows/validate-deployment.ps1`
|
||||
- `windows/migrate-awatch-rus-paths.ps1`
|
||||
@@ -41,6 +44,7 @@
|
||||
### 1.4 Шаблоны конфигурации
|
||||
- `windows/web-category-rules.example.json`
|
||||
- `windows/dlp-policy.example.json`
|
||||
- `windows/dlp-policy.native-cross-os.example.json`
|
||||
|
||||
## 2) Бинарный payload ActivityWatch
|
||||
|
||||
@@ -83,6 +87,7 @@
|
||||
- `windows\dlp-endpoint-signals-collector.ps1`
|
||||
- `windows\web-category-rules.example.json`
|
||||
- `windows\dlp-policy.example.json`
|
||||
- `windows\dlp-policy.native-cross-os.example.json`
|
||||
- `payload\activitywatch-v0.13.2-windows-x86_64.zip` (только для offline-режима)
|
||||
|
||||
## 6) Контроль перед сборкой .iss
|
||||
|
||||
+105
-12
@@ -1,4 +1,4 @@
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json'
|
||||
)
|
||||
@@ -38,7 +38,7 @@ $windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -a
|
||||
$fileOpsExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]$config.collectors.fileOpsEnabled } else { $true }
|
||||
$sessionEventsConfig = if ($config.PSObject.Properties.Name -contains 'sessionEvents') { $config.sessionEvents } else { $null }
|
||||
$sessionLogonEnabled = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$sessionEventsConfig.logonEnabled } else { $false }
|
||||
$sessionProcessEventsEnabled = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'processEventsEnabled') { [bool]$sessionEventsConfig.processEventsEnabled } else { $true }
|
||||
$sessionProcessEventsEnabled = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'processEventsEnabled') { [bool]$sessionEventsConfig.processEventsEnabled } else { $false }
|
||||
$sessionEventsBucketId = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]$sessionEventsConfig.bucketPrefix)) {
|
||||
('{0}_{1}' -f [string]$sessionEventsConfig.bucketPrefix, $awHostname)
|
||||
}
|
||||
@@ -301,6 +301,94 @@ function Get-TransportQueueHealth {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-TransportQueueGroupHealth {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$StateRoot,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$QueuePattern,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$StaleAfterSeconds,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$MaxDepth,
|
||||
[int]$ActiveProcessCount = 0,
|
||||
[bool]$Required = $true
|
||||
)
|
||||
|
||||
$queues = @(Get-ChildItem -LiteralPath $StateRoot -Filter $QueuePattern -ErrorAction SilentlyContinue | Sort-Object Name)
|
||||
if ($queues.Count -eq 0) {
|
||||
return [pscustomobject]@{
|
||||
name = $Name
|
||||
required = [bool]$Required
|
||||
queuePattern = $QueuePattern
|
||||
queueCount = 0
|
||||
queues = @()
|
||||
depth = 0
|
||||
sizeBytes = 0
|
||||
activeProcessCount = [int]$ActiveProcessCount
|
||||
staleAfterSeconds = [int]$StaleAfterSeconds
|
||||
maxDepth = [int]$MaxDepth
|
||||
ok = [bool](-not $Required)
|
||||
}
|
||||
}
|
||||
|
||||
$items = @()
|
||||
foreach ($queue in $queues) {
|
||||
$lockPath = [System.IO.Path]::ChangeExtension($queue.FullName, '.lock')
|
||||
$items += Get-TransportQueueHealth -Name $queue.BaseName -QueuePath $queue.FullName -LockPath $lockPath -StaleAfterSeconds $StaleAfterSeconds -MaxDepth $MaxDepth -ActiveProcessCount $ActiveProcessCount -Required $Required
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
name = $Name
|
||||
required = [bool]$Required
|
||||
queuePattern = $QueuePattern
|
||||
queueCount = [int]$items.Count
|
||||
queues = @($items)
|
||||
depth = [int](($items | Measure-Object -Property depth -Sum).Sum)
|
||||
sizeBytes = [int64](($items | Measure-Object -Property sizeBytes -Sum).Sum)
|
||||
activeProcessCount = [int]$ActiveProcessCount
|
||||
staleAfterSeconds = [int]$StaleAfterSeconds
|
||||
maxDepth = [int]$MaxDepth
|
||||
ok = [bool](-not ($items | Where-Object { -not $_.ok }))
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-ActivityWatchLaunchTaskName {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TaskName
|
||||
)
|
||||
|
||||
if ($TaskName -notmatch '\[[^\]]+_Administrator\]') {
|
||||
return $TaskName
|
||||
}
|
||||
|
||||
$localizedCandidate = 'ActivityWatch Launch [{0}_Администратор]' -f $awHostname
|
||||
$localizedTask = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $localizedCandidate } | Select-Object -First 1
|
||||
if ($localizedTask) {
|
||||
return $localizedCandidate
|
||||
}
|
||||
|
||||
try {
|
||||
$builtinAdmin = Get-LocalUser -ErrorAction Stop |
|
||||
Where-Object { [string]$_.SID -match '-500$' } |
|
||||
Select-Object -First 1
|
||||
if ($builtinAdmin -and -not [string]::IsNullOrWhiteSpace([string]$builtinAdmin.Name)) {
|
||||
$candidate = 'ActivityWatch Launch [{0}_{1}]' -f $awHostname, [string]$builtinAdmin.Name
|
||||
$existing = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $candidate } | Select-Object -First 1
|
||||
if ($existing) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
return $TaskName
|
||||
}
|
||||
|
||||
function Get-TaskSnapshot {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
@@ -337,14 +425,13 @@ function Get-TaskSnapshot {
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
[pscustomobject]@{
|
||||
taskName = [string]$task.TaskName
|
||||
present = $true
|
||||
enabled = [bool]$enabled
|
||||
state = [string]$task.State
|
||||
lastResult = if ($taskInfo) { [int64]$taskInfo.LastTaskResult } else { $null }
|
||||
ok = [bool]($enabled)
|
||||
ok = [bool]$enabled
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -413,10 +500,14 @@ else {
|
||||
0
|
||||
}
|
||||
$sessionScopedCollectorsRequired = ($sessionScopedExpectedCount -gt 0)
|
||||
$liveSessionScopedCollectorsRequired = ($liveSessionBoundUsers.Count -gt 0)
|
||||
|
||||
$collectorGuardService = Get-Service -Name 'AWatchRusCollectorGuard' -ErrorAction SilentlyContinue
|
||||
$collectorGuardActive = [bool]($collectorGuardService -and $collectorGuardService.Status -eq 'Running')
|
||||
|
||||
$taskNames = @()
|
||||
if ($config.userTasks) {
|
||||
$taskNames += @($config.userTasks | ForEach-Object { [string]$_.launchTaskName })
|
||||
$taskNames += @($config.userTasks | ForEach-Object { Resolve-ActivityWatchLaunchTaskName -TaskName ([string]$_.launchTaskName) })
|
||||
}
|
||||
$taskNames += [string]$config.recovery.taskName
|
||||
$tasks = @(Get-TaskSnapshot -TaskNames $taskNames)
|
||||
@@ -438,24 +529,24 @@ foreach ($watcher in $runningWatchers) {
|
||||
$bucketChecks = @(
|
||||
Get-BucketHealth -BucketId ('aw-worktime-sessions_' + $awHostname) -MaxAgeSeconds $sessionFreshnessSeconds -Required $true -RequireFreshEvent $true
|
||||
)
|
||||
if ($sessionScopedCollectorsRequired -and $afkExpected) {
|
||||
if ($liveSessionScopedCollectorsRequired -and $afkExpected) {
|
||||
$bucketChecks += Get-BucketHealth -BucketId ('aw-watcher-afk_' + $awHostname) -MaxAgeSeconds $freshnessSeconds -Required $true -RequireFreshEvent $false
|
||||
}
|
||||
if ($sessionScopedCollectorsRequired -and $windowExpected) {
|
||||
if ($liveSessionScopedCollectorsRequired -and $windowExpected) {
|
||||
$bucketChecks += Get-BucketHealth -BucketId ('aw-watcher-window_' + $awHostname) -MaxAgeSeconds $freshnessSeconds -Required $true -RequireFreshEvent $false
|
||||
}
|
||||
if ($sessionScopedCollectorsRequired) {
|
||||
if ($liveSessionScopedCollectorsRequired) {
|
||||
$bucketChecks += Get-BucketHealth -BucketId ('aw-dlp-endpoint-signals_' + $awHostname) -MaxAgeSeconds $endpointFreshnessSeconds -Required $true -RequireFreshEvent $true
|
||||
}
|
||||
if ($sessionScopedCollectorsRequired -and $fileOpsExpected) {
|
||||
if ($liveSessionScopedCollectorsRequired -and $fileOpsExpected) {
|
||||
$bucketChecks += Get-BucketHealth -BucketId ('aw-file-operations_' + $awHostname) -MaxAgeSeconds $transportStaleSeconds -Required $false -RequireFreshEvent $true
|
||||
}
|
||||
|
||||
$queueChecks = @(
|
||||
Get-TransportQueueHealth -Name 'endpoint' -QueuePath (Join-Path $stateRoot 'dlp-endpoint-signals-queue.jsonl') -LockPath (Join-Path $stateRoot 'dlp-endpoint-signals-queue.lock') -StaleAfterSeconds $transportStaleSeconds -MaxDepth $queueMaxDepth -ActiveProcessCount @($endpointCollectorProcesses).Count -Required $sessionScopedCollectorsRequired
|
||||
Get-TransportQueueGroupHealth -Name 'endpoint' -StateRoot $stateRoot -QueuePattern 'dlp-endpoint-signals-queue*.jsonl' -StaleAfterSeconds $transportStaleSeconds -MaxDepth $queueMaxDepth -ActiveProcessCount @($endpointCollectorProcesses).Count -Required $liveSessionScopedCollectorsRequired
|
||||
)
|
||||
if ($fileOpsExpected) {
|
||||
$queueChecks += Get-TransportQueueHealth -Name 'fileops' -QueuePath (Join-Path $stateRoot 'file-operations-queue.jsonl') -LockPath (Join-Path $stateRoot 'file-operations-queue.lock') -StaleAfterSeconds $transportStaleSeconds -MaxDepth $queueMaxDepth -ActiveProcessCount @($fileCollectorProcesses).Count -Required $sessionScopedCollectorsRequired
|
||||
$queueChecks += Get-TransportQueueGroupHealth -Name 'fileops' -StateRoot $stateRoot -QueuePattern 'file-operations-queue*.jsonl' -StaleAfterSeconds $transportStaleSeconds -MaxDepth $queueMaxDepth -ActiveProcessCount @($fileCollectorProcesses).Count -Required $liveSessionScopedCollectorsRequired
|
||||
}
|
||||
|
||||
$printServiceOperationalEnabled = $false
|
||||
@@ -502,12 +593,14 @@ $result = [ordered]@{
|
||||
}
|
||||
tasks = [ordered]@{
|
||||
list = $tasks
|
||||
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present -or -not $_.enabled }))
|
||||
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.ok }))
|
||||
}
|
||||
processes = [ordered]@{
|
||||
liveSessionBoundUsers = $liveSessionBoundUsers
|
||||
sessionBoundUsers = $interactiveSessionBoundUsers
|
||||
sessionScopedExpectedCount = [int]$sessionScopedExpectedCount
|
||||
liveSessionScopedCollectorsRequired = [bool]$liveSessionScopedCollectorsRequired
|
||||
collectorGuardServiceActive = [bool]$collectorGuardActive
|
||||
watchers = @($runningWatchers)
|
||||
watcherDuplicates = @($watcherDuplicates)
|
||||
sessionCollectors = @($sessionCollectorProcesses)
|
||||
|
||||
Reference in New Issue
Block a user