#!/usr/bin/env python3 from http.server import BaseHTTPRequestHandler, HTTPServer import csv import io import json import os import urllib.request from datetime import datetime, timezone, timedelta from zoneinfo import ZoneInfo AW = "http://127.0.0.1:5600/api/0" REPORT_TZ = ZoneInfo(os.environ.get("AW_WORKTIME_TZ", "Europe/Moscow")) IOC_DIR = os.environ.get("AW_DLP_IOC_DIR", "/opt/activitywatch/dlp-ioc/output") def get(u): with urllib.request.urlopen(u, timeout=30) as r: return json.loads(r.read().decode()) def pts(s): return datetime.fromisoformat(s.replace("Z", "+00:00")).astimezone(timezone.utc) def _is_machine_user(user: str) -> bool: u = (user or "").strip().lower() return u.endswith("$") or u in {"system", "localservice", "networkservice"} def _is_active_sample(data: dict) -> bool: state = str(data.get("state") or "").strip().lower() if isinstance(data.get("active"), bool): if data.get("active"): return True if ("актив" in state) or (state == "active"): return True # query user can intermittently return "Unknown" on RDP hosts; if session id is valid # and user is not a machine/service account, treat it as activity sample. if state == "unknown": try: sid = int(data.get("sessionId")) except Exception: sid = -1 user = str(data.get("username") or "").strip() session_name = str(data.get("sessionName") or "").strip().lower() if sid > 0 and user and (not _is_machine_user(user)) and (session_name.startswith("rdp-") or session_name == "console"): return True return False def report_today(): now_local = datetime.now(REPORT_TZ) start_local = datetime(now_local.year, now_local.month, now_local.day, tzinfo=REPORT_TZ) end_local = start_local + timedelta(days=1) - timedelta(seconds=1) start = start_local.astimezone(timezone.utc) end = end_local.astimezone(timezone.utc) b = get(AW + "/buckets") sb = next((k for k in b if k.startswith("aw-worktime-sessions_")), None) if not sb: return [] ev = get(f"{AW}/buckets/{sb}/events?limit=50000") by = {} for e in ev: ts = pts(e.get("timestamp")) if ts < start or ts > end: continue d = e.get("data") or {} user = (d.get("username") or "").strip() if not user: continue active = _is_active_sample(d) row = by.setdefault(user, {"active": set(), "first": None, "last": None, "rows": 0}) row["rows"] += 1 if active: second = ts.replace(microsecond=0) row["active"].add(second) row["first"] = second if row["first"] is None or second < row["first"] else row["first"] row["last"] = second if row["last"] is None or second > row["last"] else row["last"] rows = [] full = int((end_local - start_local).total_seconds()) for user in sorted(by): row = by[user] active_seconds = len(row["active"]) rows.append({ "user": user, "active_seconds": active_seconds, "active_hhmm": "%02d:%02d" % (active_seconds // 3600, (active_seconds % 3600) // 60), "first_activity": row["first"].isoformat().replace("+00:00", "Z") if row["first"] else "", "last_activity": row["last"].isoformat().replace("+00:00", "Z") if row["last"] else "", "idle_seconds": max(0, full - active_seconds), "sessions_count": row["rows"], }) return rows def render_html(rows): generated = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") date_local = datetime.now(REPORT_TZ).strftime("%Y-%m-%d") trs = [] for row in rows: trs.append( "
| User | Active | Active sec | First activity | Last activity | Idle sec | Samples |
|---|