fix(collectors): stabilize browser+DLP collectors and add rdp worktime report script
This commit is contained in:
@@ -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 <<EOF
|
||||||
|
Usage:
|
||||||
|
$0 --day today|yesterday
|
||||||
|
$0 --from YYYY-MM-DD --to YYYY-MM-DD
|
||||||
|
Env:
|
||||||
|
AW_BASE_URL (default: ${AW_BASE_URL})
|
||||||
|
OUT_DIR (default: ${OUT_DIR})
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--day) DAY="${2:-}"; shift 2 ;;
|
||||||
|
--from) FROM="${2:-}"; shift 2 ;;
|
||||||
|
--to) TO="${2:-}"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "Unknown arg: $1" >&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}"
|
||||||
@@ -541,7 +541,7 @@ function Send-DlpIncidentHeartbeat {
|
|||||||
} + $captureData
|
} + $captureData
|
||||||
} | ConvertTo-Json -Depth 5 -Compress
|
} | 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 {
|
function Get-FileSha256Hex {
|
||||||
@@ -707,7 +707,7 @@ function Ensure-Bucket {
|
|||||||
hostname = $script:Hostname
|
hostname = $script:Hostname
|
||||||
} | ConvertTo-Json -Compress
|
} | 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
|
$script:KnownBuckets[$BucketId] = $true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -733,7 +733,7 @@ function Send-Heartbeat {
|
|||||||
}
|
}
|
||||||
} | ConvertTo-Json -Depth 4 -Compress
|
} | 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 {
|
function Send-CategoryHeartbeat {
|
||||||
@@ -770,7 +770,7 @@ function Send-CategoryHeartbeat {
|
|||||||
}
|
}
|
||||||
} | ConvertTo-Json -Depth 4 -Compress
|
} | 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
|
Load-CustomCategoryRules -Path $resolvedRulesPath
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function Invoke-AwJsonPost {
|
|||||||
)
|
)
|
||||||
|
|
||||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
$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 {
|
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 {
|
function Load-DlpPolicy {
|
||||||
param([string]$Path)
|
param([string]$Path)
|
||||||
|
|
||||||
@@ -772,7 +810,7 @@ while ($true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$clipboardText = Get-Clipboard -Raw -ErrorAction SilentlyContinue
|
$clipboardText = Get-ClipboardTextSafe
|
||||||
if ($clipboardText) {
|
if ($clipboardText) {
|
||||||
$clipboardHash = Get-StringHash -Value $clipboardText
|
$clipboardHash = Get-StringHash -Value $clipboardText
|
||||||
if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) {
|
if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) {
|
||||||
|
|||||||
Reference in New Issue
Block a user