diff --git a/scripts/rdp-worktime-report.sh b/scripts/rdp-worktime-report.sh new file mode 100644 index 0000000..0ff435d --- /dev/null +++ b/scripts/rdp-worktime-report.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +set -euo pipefail + +DAY="" +FROM="" +TO="" +AW_BASE_URL="${AW_BASE_URL:-http://10.10.10.13:5600/api/0}" +OUT_DIR="${OUT_DIR:-reports}" + +usage() { + cat <&2; usage; exit 2 ;; + esac +done + +if [[ -n "$DAY" ]]; then + if [[ "$DAY" == "today" ]]; then + FROM="$(date +%F)" + TO="$FROM" + elif [[ "$DAY" == "yesterday" ]]; then + FROM="$(date -d 'yesterday' +%F)" + TO="$FROM" + else + echo "Invalid --day: $DAY" >&2 + exit 2 + fi +fi + +if [[ -z "$FROM" || -z "$TO" ]]; then + usage + exit 2 +fi + +mkdir -p "$OUT_DIR" +CSV_OUT="${OUT_DIR}/rdp-worktime-${FROM}_${TO}.csv" +JSON_OUT="${OUT_DIR}/rdp-worktime-${FROM}_${TO}.json" + +python3 - "$AW_BASE_URL" "$FROM" "$TO" "$CSV_OUT" "$JSON_OUT" <<'PY' +import csv +import json +import sys +import urllib.request +from datetime import datetime, timedelta, timezone + +base, from_d, to_d, csv_out, json_out = sys.argv[1:6] + +def get_json(url: str): + with urllib.request.urlopen(url, timeout=30) as r: + return json.loads(r.read().decode()) + +def parse_ts(s): + if not s: + return None + return datetime.fromisoformat(s.replace("Z", "+00:00")).astimezone(timezone.utc) + +buckets = get_json(f"{base}/buckets") +sessions_bucket = None +for k in buckets.keys(): + if k.startswith("aw-worktime-sessions_"): + sessions_bucket = k + break + +if not sessions_bucket: + raise SystemExit("No aw-worktime-sessions_* bucket found") + +start = datetime.fromisoformat(from_d + "T00:00:00+00:00") +end = datetime.fromisoformat(to_d + "T23:59:59+00:00") + +ev = get_json(f"{base}/buckets/{sessions_bucket}/events?limit=20000") +by_user = {} +for e in ev: + ts = parse_ts(e.get("timestamp")) + if ts is None or ts < start or ts > end: + continue + d = e.get("data") or {} + user = (d.get("username") or "").strip() + if not user: + continue + state = (d.get("state") or "").strip().lower() + is_active = ("актив" in state) or (state == "active") + rec = by_user.setdefault(user, {"active_ts": set(), "first": None, "last": None, "rows": 0}) + rec["rows"] += 1 + if is_active: + rec["active_ts"].add(ts.replace(microsecond=0)) + rec["first"] = ts if rec["first"] is None or ts < rec["first"] else rec["first"] + rec["last"] = ts if rec["last"] is None or ts > rec["last"] else rec["last"] + +rows = [] +full_range = int((end - start).total_seconds()) +for user in sorted(by_user.keys()): + rec = by_user[user] + active = len(rec["active_ts"]) + idle = max(0, full_range - active) + rows.append({ + "user": user, + "active_seconds": int(active), + "active_hhmm": f"{int(active)//3600:02d}:{(int(active)%3600)//60:02d}", + "first_activity": rec["first"].isoformat().replace("+00:00","Z") if rec["first"] else "", + "last_activity": rec["last"].isoformat().replace("+00:00","Z") if rec["last"] else "", + "idle_seconds": int(idle), + "sessions_count": rec["rows"], + }) + +with open(csv_out, "w", newline="", encoding="utf-8") as f: + w = csv.DictWriter(f, fieldnames=[ + "user","active_seconds","active_hhmm","first_activity","last_activity","idle_seconds","sessions_count" + ]) + w.writeheader() + w.writerows(rows) + +with open(json_out, "w", encoding="utf-8") as f: + json.dump({ + "from": from_d, + "to": to_d, + "generated_at_utc": datetime.now(timezone.utc).isoformat().replace("+00:00","Z"), + "rows": rows + }, f, ensure_ascii=False, indent=2) + +print(csv_out) +print(json_out) +PY + +echo "CSV: ${CSV_OUT}" +echo "JSON: ${JSON_OUT}" diff --git a/windows/browser-domains-native-collector.ps1 b/windows/browser-domains-native-collector.ps1 index d0c809f..b220b3d 100755 --- a/windows/browser-domains-native-collector.ps1 +++ b/windows/browser-domains-native-collector.ps1 @@ -541,7 +541,7 @@ function Send-DlpIncidentHeartbeat { } + $captureData } | ConvertTo-Json -Depth 5 -Compress - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event | Out-Null + Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null } function Get-FileSha256Hex { @@ -707,7 +707,7 @@ function Ensure-Bucket { hostname = $script:Hostname } | ConvertTo-Json -Compress - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId" -ContentType 'application/json' -Body $body | Out-Null + Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId" -ContentType 'application/json' -Body $body -TimeoutSec 15 -DisableKeepAlive | Out-Null $script:KnownBuckets[$BucketId] = $true } @@ -733,7 +733,7 @@ function Send-Heartbeat { } } | ConvertTo-Json -Depth 4 -Compress - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event | Out-Null + Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null } function Send-CategoryHeartbeat { @@ -770,7 +770,7 @@ function Send-CategoryHeartbeat { } } | ConvertTo-Json -Depth 4 -Compress - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event | Out-Null + Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null } Load-CustomCategoryRules -Path $resolvedRulesPath diff --git a/windows/dlp-endpoint-signals-collector.ps1 b/windows/dlp-endpoint-signals-collector.ps1 index af3494c..829153e 100644 --- a/windows/dlp-endpoint-signals-collector.ps1 +++ b/windows/dlp-endpoint-signals-collector.ps1 @@ -40,7 +40,7 @@ function Invoke-AwJsonPost { ) $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null + Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null } function Ensure-Bucket { @@ -321,6 +321,44 @@ function Get-StringHash { } } +function Get-ClipboardTextSafe { + [OutputType([string])] + param() + + try { + $v = Get-Clipboard -Raw -ErrorAction Stop + if ($null -ne $v) { return [string]$v } + } + catch { + Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) + } + + # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. + try { + Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null + $result = [string]::Empty + $thread = [System.Threading.Thread]{ + try { + $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() + } + catch { + $script:__aw_clip = $null + } + } + $thread.SetApartmentState([System.Threading.ApartmentState]::STA) + $thread.Start() + $thread.Join(3000) | Out-Null + if ($thread.IsAlive) { $thread.Abort() } + $result = [string]$script:__aw_clip + Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue + return $result + } + catch { + Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) + return $null + } +} + function Load-DlpPolicy { param([string]$Path) @@ -772,7 +810,7 @@ while ($true) { } try { - $clipboardText = Get-Clipboard -Raw -ErrorAction SilentlyContinue + $clipboardText = Get-ClipboardTextSafe if ($clipboardText) { $clipboardHash = Get-StringHash -Value $clipboardText if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) {