731 lines
28 KiB
PowerShell
731 lines
28 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
|
|
[string]$ServerHost,
|
|
[int]$ServerPort,
|
|
[ValidateSet('http', 'https')]
|
|
[string]$ServerScheme,
|
|
[string]$PolicyPath,
|
|
[string]$LogPath,
|
|
[int]$PollSeconds
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
function Get-DeploymentConfig {
|
|
param([string]$Path)
|
|
if ($Path -and (Test-Path -LiteralPath $Path)) {
|
|
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Write-EndpointLog {
|
|
param([string]$Message)
|
|
if (-not $script:LocalAgentLogsEnabled) {
|
|
return
|
|
}
|
|
try {
|
|
Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message)
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
|
|
function Invoke-AwJsonPost {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Uri,
|
|
[Parameter(Mandatory = $true)][string]$Json
|
|
)
|
|
|
|
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
|
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
|
}
|
|
|
|
function Ensure-Bucket {
|
|
param(
|
|
[string]$BucketId,
|
|
[string]$ClientName,
|
|
[string]$BucketType
|
|
)
|
|
|
|
if ($script:KnownBuckets.ContainsKey($BucketId)) {
|
|
return
|
|
}
|
|
|
|
$body = @{
|
|
client = $ClientName
|
|
type = $BucketType
|
|
hostname = $script:Hostname
|
|
} | ConvertTo-Json -Compress
|
|
|
|
Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body
|
|
$script:KnownBuckets[$BucketId] = $true
|
|
}
|
|
|
|
function Send-EndpointSignalHeartbeat {
|
|
param(
|
|
[string]$SignalType,
|
|
[hashtable]$Data
|
|
)
|
|
|
|
$bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname
|
|
Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal'
|
|
|
|
$payload = @{
|
|
timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
|
duration = 0
|
|
data = @{
|
|
signalType = $SignalType
|
|
username = $env:USERNAME
|
|
sessionId = $script:SessionId
|
|
hostname = $script:Hostname
|
|
source = 'endpoint-signals-phase2'
|
|
} + $Data
|
|
} | ConvertTo-Json -Depth 6 -Compress
|
|
|
|
Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload
|
|
}
|
|
|
|
function Send-DlpIncidentHeartbeat {
|
|
param(
|
|
[string]$RuleId,
|
|
[string]$Action,
|
|
[string]$Severity,
|
|
[string]$Message,
|
|
[string]$SignalType,
|
|
[hashtable]$Data
|
|
)
|
|
|
|
$bucketId = 'aw-dlp-incidents_' + $script:Hostname
|
|
Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident'
|
|
|
|
$captureData = @{}
|
|
if ($script:IncidentScreenshotEnabled) {
|
|
try {
|
|
$captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
|
|
$payload = @{
|
|
timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
|
duration = 0
|
|
data = @{
|
|
ruleId = $RuleId
|
|
action = $Action
|
|
severity = $Severity
|
|
message = $Message
|
|
signalType = $SignalType
|
|
username = $env:USERNAME
|
|
sessionId = $script:SessionId
|
|
hostname = $script:Hostname
|
|
source = 'endpoint-signals-phase2'
|
|
} + $Data + $captureData
|
|
} | ConvertTo-Json -Depth 7 -Compress
|
|
|
|
Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload
|
|
}
|
|
|
|
function Get-FileSha256Hex {
|
|
param([Parameter(Mandatory = $true)][string]$Path)
|
|
try {
|
|
$sha = [Security.Cryptography.SHA256]::Create()
|
|
$stream = [IO.File]::OpenRead($Path)
|
|
try {
|
|
($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join ''
|
|
}
|
|
finally {
|
|
$stream.Dispose()
|
|
$sha.Dispose()
|
|
}
|
|
}
|
|
catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Ensure-Directory {
|
|
param([Parameter(Mandatory = $true)][string]$Path)
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
New-Item -Path $Path -ItemType Directory -Force | Out-Null
|
|
}
|
|
}
|
|
|
|
function Get-IncidentScreenshotPath {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$RuleId,
|
|
[Parameter(Mandatory = $true)][string]$SignalType
|
|
)
|
|
|
|
$safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_')
|
|
$safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_')
|
|
$safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_')
|
|
$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff')
|
|
$file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule
|
|
$file = '{0}_{1}' -f $stamp, $file
|
|
return (Join-Path $script:IncidentArtifactsRoot $file)
|
|
}
|
|
|
|
function Ensure-ScreenshotTypesLoaded {
|
|
if ($script:ScreenshotTypesLoaded) {
|
|
return
|
|
}
|
|
Add-Type -AssemblyName System.Windows.Forms | Out-Null
|
|
Add-Type -AssemblyName System.Drawing | Out-Null
|
|
$script:ScreenshotTypesLoaded = $true
|
|
}
|
|
|
|
function Capture-IncidentScreenshot {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$RuleId,
|
|
[Parameter(Mandatory = $true)][string]$SignalType
|
|
)
|
|
|
|
try {
|
|
Ensure-Directory -Path $script:IncidentArtifactsRoot
|
|
Ensure-ScreenshotTypesLoaded
|
|
|
|
$vs = [System.Windows.Forms.SystemInformation]::VirtualScreen
|
|
$bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height)
|
|
$gfx = [System.Drawing.Graphics]::FromImage($bmp)
|
|
try {
|
|
$gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size)
|
|
$path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType
|
|
$bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
}
|
|
finally {
|
|
$gfx.Dispose()
|
|
$bmp.Dispose()
|
|
}
|
|
|
|
return @{
|
|
screenshotPath = $path
|
|
screenshotFormat = 'png'
|
|
screenshotWidth = [int]$vs.Width
|
|
screenshotHeight = [int]$vs.Height
|
|
screenshotSha256 = (Get-FileSha256Hex -Path $path)
|
|
}
|
|
}
|
|
catch {
|
|
Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message)
|
|
return @{}
|
|
}
|
|
}
|
|
|
|
function Get-StringHash {
|
|
param([AllowNull()][string]$Value)
|
|
if ($null -eq $Value) { return $null }
|
|
$bytes = [Text.Encoding]::UTF8.GetBytes($Value)
|
|
$sha = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join ''
|
|
}
|
|
finally {
|
|
$sha.Dispose()
|
|
}
|
|
}
|
|
|
|
function Load-DlpPolicy {
|
|
param([string]$Path)
|
|
|
|
$script:Policy = [ordered]@{
|
|
defaults = [ordered]@{
|
|
enabled = $true
|
|
cooldownSeconds = 300
|
|
action = 'alert'
|
|
severity = 'medium'
|
|
}
|
|
endpoint = [ordered]@{
|
|
clipboard = @()
|
|
usb = @()
|
|
print = @()
|
|
}
|
|
}
|
|
|
|
if (-not $Path -or -not (Test-Path -LiteralPath $Path)) {
|
|
Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path)
|
|
return
|
|
}
|
|
|
|
try {
|
|
$raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
|
if ($raw.defaults) {
|
|
if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled }
|
|
if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds }
|
|
if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action }
|
|
if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity }
|
|
}
|
|
|
|
if ($raw.endpoint) {
|
|
if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) }
|
|
if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) }
|
|
if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) }
|
|
}
|
|
}
|
|
catch {
|
|
Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message)
|
|
}
|
|
}
|
|
|
|
function Should-EmitByCooldown {
|
|
param(
|
|
[string]$Fingerprint,
|
|
[int]$CooldownSeconds
|
|
)
|
|
|
|
$now = (Get-Date).ToUniversalTime()
|
|
if ($script:Cooldown.ContainsKey($Fingerprint)) {
|
|
$last = [datetime]$script:Cooldown[$Fingerprint]
|
|
if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
$script:Cooldown[$Fingerprint] = $now
|
|
return $true
|
|
}
|
|
|
|
function Evaluate-ClipboardRules {
|
|
param(
|
|
[string]$ClipboardText,
|
|
[string]$ClipboardHash
|
|
)
|
|
|
|
foreach ($rule in @($script:Policy.endpoint.clipboard)) {
|
|
if (-not $rule) { continue }
|
|
if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue }
|
|
$ruleId = [string]$rule.id
|
|
if (-not $ruleId) { continue }
|
|
$minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 }
|
|
$regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() }
|
|
if ($ClipboardText.Length -lt $minLength) { continue }
|
|
|
|
$matched = $false
|
|
foreach ($pattern in $regexPatterns) {
|
|
if ($ClipboardText -match [string]$pattern) {
|
|
$matched = $true
|
|
break
|
|
}
|
|
}
|
|
|
|
if (-not $matched) { continue }
|
|
|
|
$cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds }
|
|
$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 }
|
|
$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" }
|
|
|
|
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{
|
|
clipboardHash = $ClipboardHash
|
|
clipboardLength = $ClipboardText.Length
|
|
}
|
|
Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2}" -f $ruleId, $action, $severity)
|
|
}
|
|
}
|
|
|
|
function Evaluate-UsbRules {
|
|
param(
|
|
[string]$DriveLetter,
|
|
[string]$VolumeName
|
|
)
|
|
|
|
foreach ($rule in @($script:Policy.endpoint.usb)) {
|
|
if (-not $rule) { continue }
|
|
if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue }
|
|
$ruleId = [string]$rule.id
|
|
if (-not $ruleId) { continue }
|
|
|
|
$cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds }
|
|
$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 }
|
|
$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" }
|
|
|
|
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{
|
|
driveLetter = $DriveLetter
|
|
volumeName = $VolumeName
|
|
}
|
|
Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3}" -f $ruleId, $action, $severity, $DriveLetter)
|
|
}
|
|
}
|
|
|
|
function Evaluate-PrintRules {
|
|
param(
|
|
[string]$PrinterName,
|
|
[string]$DocumentName,
|
|
[string]$Owner
|
|
)
|
|
|
|
foreach ($rule in @($script:Policy.endpoint.print)) {
|
|
if (-not $rule) { continue }
|
|
if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue }
|
|
$ruleId = [string]$rule.id
|
|
if (-not $ruleId) { continue }
|
|
|
|
$match = $true
|
|
if ($rule.printerRegex) {
|
|
$match = $match -and ($PrinterName -match [string]$rule.printerRegex)
|
|
}
|
|
if ($rule.documentRegex) {
|
|
$match = $match -and ($DocumentName -match [string]$rule.documentRegex)
|
|
}
|
|
if (-not $match) { continue }
|
|
|
|
$cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds }
|
|
$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 }
|
|
$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" }
|
|
|
|
Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{
|
|
printerName = $PrinterName
|
|
documentName = $DocumentName
|
|
owner = $Owner
|
|
}
|
|
Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3}" -f $ruleId, $action, $severity, $PrinterName)
|
|
}
|
|
}
|
|
|
|
function Test-LooksLikeMojibakeQuestionMarks {
|
|
param([AllowNull()][string]$Value)
|
|
if ([string]::IsNullOrWhiteSpace($Value)) { return $true }
|
|
return $Value -match '\?{2,}'
|
|
}
|
|
|
|
function Get-PrintServiceEventSummary {
|
|
param([Parameter(Mandatory = $true)]$Event)
|
|
|
|
$props = @($Event.Properties)
|
|
$propertyValues = @()
|
|
foreach ($prop in $props) {
|
|
$propertyValues += [string]$prop.Value
|
|
}
|
|
|
|
[pscustomobject]@{
|
|
RecordId = [string]$Event.RecordId
|
|
TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' }
|
|
PropertyCount = $props.Count
|
|
DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' }
|
|
Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' }
|
|
PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' }
|
|
PropertyValues = $propertyValues
|
|
}
|
|
}
|
|
|
|
function Get-PrintServiceDocumentFallback {
|
|
param(
|
|
[Parameter(Mandatory = $true)]$EventSummary,
|
|
[string]$Owner,
|
|
[string]$PrinterName
|
|
)
|
|
|
|
$preferred = [string]$EventSummary.DocumentName
|
|
if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') {
|
|
return $preferred
|
|
}
|
|
|
|
$pathCandidates = New-Object System.Collections.Generic.List[string]
|
|
$textCandidates = New-Object System.Collections.Generic.List[string]
|
|
|
|
foreach ($value in @($EventSummary.PropertyValues)) {
|
|
$candidate = [string]$value
|
|
if ([string]::IsNullOrWhiteSpace($candidate)) { continue }
|
|
if ($candidate -eq $preferred) { continue }
|
|
if ($Owner -and $candidate -like "*$Owner*") { continue }
|
|
if ($PrinterName -and $candidate -like "*$PrinterName*") { continue }
|
|
if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue }
|
|
|
|
if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') {
|
|
$pathCandidates.Add($candidate)
|
|
continue
|
|
}
|
|
|
|
if ($candidate -match '^[0-9]+$') {
|
|
continue
|
|
}
|
|
|
|
$textCandidates.Add($candidate)
|
|
}
|
|
|
|
foreach ($candidate in @($pathCandidates)) {
|
|
$leaf = Split-Path -Path $candidate -Leaf
|
|
if (-not [string]::IsNullOrWhiteSpace($leaf)) {
|
|
return $leaf
|
|
}
|
|
return $candidate
|
|
}
|
|
|
|
foreach ($candidate in @($textCandidates)) {
|
|
return $candidate
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Write-PrintServiceEventTrace {
|
|
param(
|
|
[Parameter(Mandatory = $true)]$EventSummary,
|
|
[string]$Phase,
|
|
[string]$MatchReason,
|
|
[string]$ResolvedDocument
|
|
)
|
|
|
|
$properties = if ($EventSummary.PropertyValues) {
|
|
($EventSummary.PropertyValues -join ' | ')
|
|
}
|
|
else {
|
|
''
|
|
}
|
|
|
|
Write-EndpointLog (
|
|
'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f
|
|
$Phase,
|
|
$EventSummary.RecordId,
|
|
$EventSummary.TimeCreated,
|
|
$EventSummary.Owner,
|
|
$EventSummary.PrinterName,
|
|
$EventSummary.DocumentName,
|
|
$ResolvedDocument,
|
|
$properties,
|
|
$MatchReason
|
|
)
|
|
}
|
|
|
|
function Get-BetterDocumentNameFromPrintServiceEvents {
|
|
param(
|
|
[string]$Owner,
|
|
[string]$PrinterName
|
|
)
|
|
|
|
try {
|
|
$startTime = (Get-Date).AddMinutes(-15)
|
|
$events = Get-WinEvent -FilterHashtable @{
|
|
LogName = 'Microsoft-Windows-PrintService/Operational'
|
|
Id = 307
|
|
StartTime = $startTime
|
|
} -MaxEvents 200 -ErrorAction Stop
|
|
|
|
foreach ($event in @($events)) {
|
|
$summary = Get-PrintServiceEventSummary -Event $event
|
|
$matchReason = 'scan'
|
|
$resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName
|
|
|
|
if ($Owner -and $summary.Owner -and ($summary.Owner -notlike "*$Owner*")) {
|
|
$matchReason = 'owner-mismatch'
|
|
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason $matchReason -ResolvedDocument $resolvedDocument
|
|
continue
|
|
}
|
|
|
|
if ($PrinterName -and $summary.PrinterName -and ($summary.PrinterName -notlike "*$PrinterName*")) {
|
|
$matchReason = 'printer-mismatch'
|
|
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason $matchReason -ResolvedDocument $resolvedDocument
|
|
continue
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($resolvedDocument)) {
|
|
$matchReason = 'no-document-candidate'
|
|
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason $matchReason -ResolvedDocument ''
|
|
continue
|
|
}
|
|
|
|
$matchReason = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' }
|
|
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason $matchReason -ResolvedDocument $resolvedDocument
|
|
return $resolvedDocument
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath
|
|
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' }
|
|
$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 }
|
|
$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' }
|
|
$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\ActivityWatch\dlp-policy.json' }
|
|
$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 }
|
|
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\ActivityWatch\logs' }
|
|
$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) }
|
|
$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true }
|
|
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'ActivityWatch-Phase2\\incident-artifacts' }
|
|
$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true }
|
|
|
|
if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) {
|
|
New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null
|
|
}
|
|
|
|
$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort
|
|
$script:Hostname = $env:COMPUTERNAME
|
|
$script:SessionId = (Get-Process -Id $PID).SessionId
|
|
$script:KnownBuckets = @{}
|
|
$script:Cooldown = @{}
|
|
$script:SeenUsb = @{}
|
|
$script:SeenPrintJob = @{}
|
|
$script:SeenPrintEvent = @{}
|
|
$script:LastClipboardHash = $null
|
|
$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30)
|
|
$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled
|
|
$script:LogPath = $resolvedLogPath
|
|
$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot
|
|
$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled
|
|
$script:ScreenshotTypesLoaded = $false
|
|
|
|
Load-DlpPolicy -Path $resolvedPolicyPath
|
|
Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase)
|
|
|
|
while ($true) {
|
|
try {
|
|
if (-not $script:Policy.defaults.enabled) {
|
|
Start-Sleep -Seconds $resolvedPollSeconds
|
|
continue
|
|
}
|
|
|
|
try {
|
|
$clipboardText = Get-Clipboard -Raw -ErrorAction SilentlyContinue
|
|
if ($clipboardText) {
|
|
$clipboardHash = Get-StringHash -Value $clipboardText
|
|
if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) {
|
|
$script:LastClipboardHash = $clipboardHash
|
|
Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{
|
|
clipboardHash = $clipboardHash
|
|
clipboardLength = $clipboardText.Length
|
|
}
|
|
Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
|
|
try {
|
|
$usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue
|
|
$currentUsb = @{}
|
|
foreach ($drive in @($usbDrives)) {
|
|
$deviceId = [string]$drive.DeviceID
|
|
if (-not $deviceId) { continue }
|
|
$currentUsb[$deviceId] = $true
|
|
if (-not $script:SeenUsb.ContainsKey($deviceId)) {
|
|
$script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime()
|
|
$volumeName = [string]$drive.VolumeName
|
|
Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{
|
|
driveLetter = $deviceId
|
|
volumeName = $volumeName
|
|
}
|
|
Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName
|
|
}
|
|
}
|
|
|
|
foreach ($known in @($script:SeenUsb.Keys)) {
|
|
if (-not $currentUsb.ContainsKey($known)) {
|
|
$script:SeenUsb.Remove($known)
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
|
|
try {
|
|
$printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue
|
|
foreach ($job in @($printJobs)) {
|
|
$jobId = [string]$job.JobId
|
|
if (-not $jobId) { continue }
|
|
if ($script:SeenPrintJob.ContainsKey($jobId)) { continue }
|
|
$script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime()
|
|
|
|
$printerName = [string]$job.Name
|
|
$documentName = [string]$job.Document
|
|
$owner = [string]$job.Owner
|
|
$documentNameOriginal = $documentName
|
|
|
|
if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) {
|
|
$eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName
|
|
if ($eventDocumentName) {
|
|
$documentName = $eventDocumentName
|
|
}
|
|
}
|
|
|
|
Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{
|
|
printerName = $printerName
|
|
documentName = $documentName
|
|
documentNameOriginal = $documentNameOriginal
|
|
owner = $owner
|
|
}
|
|
Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner
|
|
}
|
|
|
|
$cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8)
|
|
foreach ($k in @($script:SeenPrintJob.Keys)) {
|
|
$ts = [datetime]$script:SeenPrintJob[$k]
|
|
if ($ts -lt $cleanupBefore) {
|
|
$script:SeenPrintJob.Remove($k)
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
|
|
try {
|
|
$printEvents = Get-WinEvent -FilterHashtable @{
|
|
LogName = 'Microsoft-Windows-PrintService/Operational'
|
|
Id = 307
|
|
StartTime = (Get-Date).AddMinutes(-20)
|
|
} -MaxEvents 200 -ErrorAction SilentlyContinue
|
|
|
|
foreach ($event in @($printEvents)) {
|
|
$recordId = [string]$event.RecordId
|
|
if (-not $recordId) { continue }
|
|
if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue }
|
|
$script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime()
|
|
|
|
$summary = Get-PrintServiceEventSummary -Event $event
|
|
$documentName = [string]$summary.DocumentName
|
|
$owner = [string]$summary.Owner
|
|
$printerName = [string]$summary.PrinterName
|
|
$resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName
|
|
|
|
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") {
|
|
continue
|
|
}
|
|
|
|
Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{
|
|
printerName = $printerName
|
|
documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName }
|
|
documentNameOriginal = $documentName
|
|
owner = $owner
|
|
eventRecordId = $recordId
|
|
eventSource = 'printservice-307'
|
|
}
|
|
Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner
|
|
}
|
|
|
|
$cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8)
|
|
foreach ($k in @($script:SeenPrintEvent.Keys)) {
|
|
$ts = [datetime]$script:SeenPrintEvent[$k]
|
|
if ($ts -lt $cleanupBeforeEvent) {
|
|
$script:SeenPrintEvent.Remove($k)
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
}
|
|
}
|
|
catch {
|
|
Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message)
|
|
}
|
|
|
|
Start-Sleep -Seconds $resolvedPollSeconds
|
|
}
|