import json, urllib.request, ssl, os, datetime from collections import Counter BASE="https://computerguru.syncromsp.com/api/v1" SK=os.environ["SK"]; RMM=os.environ["RMM"]; TOK=os.environ["TOK"] MAXDAYS=int(os.environ.get("MAXDAYS","31")) ctx=ssl.create_default_context() def get(url,hdr=None): return urllib.request.urlopen(urllib.request.Request(url,headers=hdr or {}),context=ctx,timeout=30).read() def clean(b): return bytes(c for c in b if c>=32 or c in (9,10,13)) now=datetime.datetime.now() def lastseen(a): ki=(a.get("properties") or {}).get("kabuto_information") or {} ts=ki.get("last_synced_at") if not ts: return None,None try: d=datetime.datetime.strptime(ts[:19],"%Y-%m-%dT%H:%M:%S") except Exception: return None,None return d,(now-d).days agents=json.loads(clean(get(f"{RMM}/api/agents",{"Authorization":f"Bearer {TOK}"}))) by_client={} for a in agents: by_client.setdefault(a.get("client_name") or "",set()).add((a.get("hostname") or "").lower()) tgts=json.load(open("projects/gps-rmm-audit/targets.json"))["clients"] folder_ctr=Counter(); body=[]; total=0; stale=0; unknown=0 for t in tgts: c=t["client"]; cid=t["cid"] try: data=json.loads(clean(get(f"{BASE}/customer_assets?customer_id={cid}&per_page=100&api_key={SK}"))) except Exception as e: body.append(f"**{c}** - ERROR: {e}\n"); continue dev=[a for a in data.get("assets",[]) if a.get("asset_type")=="Syncro Device" and a.get("name")] rmm=by_client.get(c,set()) gap=[a for a in dev if a["name"].lower() not in rmm] keep=[] for a in gap: d,days=lastseen(a) if d is None: unknown+=1; continue # no check-in data -> exclude if days>MAXDAYS: stale+=1; continue # offline > MAXDAYS -> exclude keep.append((a,d,days)) if not keep: continue keep.sort(key=lambda x:x[0]["name"].lower()) total+=len(keep) body.append(f"**{c}** - {len(keep)} need install (of {len(gap)} not-in-RMM; rest offline >{MAXDAYS}d)") for a,d,days in keep: f=a.get("policy_folder_id") or "-"; folder_ctr[str(f)]+=1 body.append(f" - {a['name']} (folder {f}, seen {d.strftime('%Y-%m-%d')}, {days}d ago)") body.append("") out=["# GPS clients - Syncro-managed machines NOT in GuruRMM (ACTIVE only)","", f"Machines missing from GuruRMM that checked into Syncro within the last {MAXDAYS} days. Excluded: {stale} offline >{MAXDAYS}d, {unknown} with no check-in data. Snapshot 2026-07-03.", "`folder N` = Syncro policy_folder_id.","", "## Folder distribution (active gap machines)"] for f,n in folder_ctr.most_common(): out.append(f"- folder {f}: {n}") out+=["",f"## TOTAL active needing install: {total}",""]+body open("projects/gps-rmm-audit/needs-screenconnect.md","w",encoding="utf-8").write("\n".join(out)) print(f"active={total} excluded_stale={stale} excluded_unknown={unknown}")