From e71a486db25f8e0f7d55e3fe17772566591963c7 Mon Sep 17 00:00:00 2001 From: Howard Enos Date: Fri, 3 Jul 2026 23:03:20 -0700 Subject: [PATCH] sync: auto-sync from HOWARD-HOME at 2026-07-03 23:02:54 Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-07-03 23:02:54 --- ...026-07-03-howard-gps-rmm-coverage-audit.md | 6 ++ projects/gps-rmm-audit/tools/sc-cleanup.py | 78 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 projects/gps-rmm-audit/tools/sc-cleanup.py diff --git a/projects/gps-rmm-audit/session-logs/2026-07/2026-07-03-howard-gps-rmm-coverage-audit.md b/projects/gps-rmm-audit/session-logs/2026-07/2026-07-03-howard-gps-rmm-coverage-audit.md index dac284f9..ad995512 100644 --- a/projects/gps-rmm-audit/session-logs/2026-07/2026-07-03-howard-gps-rmm-coverage-audit.md +++ b/projects/gps-rmm-audit/session-logs/2026-07/2026-07-03-howard-gps-rmm-coverage-audit.md @@ -149,3 +149,9 @@ Dataforth RMM sites: created a D2 site (id ed1d28c7-3f22-4578-a3f8-cabe6100382a, Cascades (next client, single campus, department-focused): normalized Company -> 'Cascades of Tucson' (was Cascades of Tucson 24 / Cascades 6 / blank 1); set Device Type on all (matched Cascades' existing 'Desktop'/'Laptop'/'Server' vocab, NOT Dataforth's 'Workstation'); set Department on 24 from hostname roles + the WIKI person->machine map (wiki/clients/cascades-tucson.md maps e.g. Ashley Jensen->Accounting DESKTOP-U2DHAP0, Chris Knight->Accounting DESKTOP-N5G1ROO, Shelby Trozzi->MemCare MDIRECTOR-PC, Sharon Edwards->Life Enrichment DESKTOP-DLTAGOI, caregiver laptops Laptop2/DRQ5L558/E0STJJE8->Nursing); fixed 'Accouting' typo. 9 departments still unknown (ANN-PC, ASSISTMAN-PC, megan, Laptop4, LAPTOP-8P7HDSEI, DESKTOP-F94M8UT/-LPOPV30/-MD6UQI3/-ROK7VNM). RECEPTIONIST-PC dup (2 RMM agents + 2 SC sessions) needs manual console removal + investigation. 2 no SC session: DESKTOP-KQSL232 (Lois Lane/CareTakers EOL), Health-Services-Director. Workflow captured (memory feedback_screenconnect_cleanup_wiki_source): SC/RMM cleanup uses the client wiki as source of truth for machine->dept/location; where missing, enrich the wiki as we learn. Next sites ranked earlier: Valley Wide (cloud UDM), Grabb/Russo (small multi-site + UniFi); Safesite deferred (no UniFi, mobile fleet). + +## Update: 21:05 PT — ScreenConnect fleet easy-win pass (Company + Device Type) + +Built projects/gps-rmm-audit/tools/sc-cleanup.py (DIRECT ScreenConnect API via urllib - CTRLAuthHeader + Origin to the RESTApi extension Service.ashx; ~10x faster than subprocess-per-call which timed out at 9min on ~500 sessions). Ran the safe no-guess pass across all ~45 worked RMM clients (skip: AZ Computer Guru, Unassigned, Dataforth, Cascades). Company (CP1) -> RMM client name (trimmed), keeping deliberate 'CODE - Name' conventions (GND -, LAB -); Device Type (CP4) from hostname/os; Department (CP3) only on high-confidence hostname tokens; Site (CP2) untouched. + +Result: 112 Company sessions normalized (fixed person-surname values Osgood->Design and Brand Envoys, Parkinson->Leeann Maddux; trimmed 'Patriot Internal Medicine '), Device Type set fleet-wide, 1 Department (Valley Wide). 4 Company changes to eyeball: Shinn,Sharon 'Starr Pass Realty'->'Shinn, Sharon' (maybe rename the RMM client to Starr Pass Realty instead), Sombra->Sombra Residential LLC, VWP dropped '(VWP)', Wolkin->Wolkin, Robert. 11 duplicate SC sessions flagged for manual console removal (mostly SERVER-named reinstalls). Departments + Site stay for per-client wiki passes (feedback_screenconnect_cleanup_wiki_source). diff --git a/projects/gps-rmm-audit/tools/sc-cleanup.py b/projects/gps-rmm-audit/tools/sc-cleanup.py new file mode 100644 index 00000000..7f9e713f --- /dev/null +++ b/projects/gps-rmm-audit/tools/sc-cleanup.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# sc-cleanup.py — safe fleet-wide ScreenConnect metadata cleanup (direct API, fast). +# Per client: Company (CP1) -> RMM client name (trimmed), keeping a deliberate 'CODE - Name' +# convention; Device Type (CP4) from hostname/OS; Department (CP3) only on high-confidence +# hostname tokens. Never touches Site (CP2), never guesses departments. +# Env: RMM, TOK (GuruRMM), SC_SECRET (ScreenConnect). Args: client names (default = all real +# RMM clients minus skip-list). --dry = report only. +import json,re,os,sys,ssl,urllib.request +from collections import Counter +RMM=os.environ["RMM"]; TOK=os.environ["TOK"]; SEC=os.environ["SC_SECRET"] +ctx=ssl.create_default_context() +SCBASE="https://computerguru.screenconnect.com/App_Extensions/2d558935-686a-4bd0-9991-07539f5fe749/Service.ashx" +DRY="--dry" in sys.argv +args=[a for a in sys.argv[1:] if not a.startswith("--")] +SKIP={"AZ Computer Guru","Unassigned","Dataforth Corp","Cascades of Tucson"} +def scpost(method,body): + req=urllib.request.Request(f"{SCBASE}/{method}",data=json.dumps(body).encode(),method="POST", + headers={"CTRLAuthHeader":SEC,"Origin":"https://computerguru.screenconnect.com","Content-Type":"application/json"}) + try: + raw=urllib.request.urlopen(req,context=ctx,timeout=25).read().decode("utf-8","replace") + return json.loads("".join(c for c in raw if ord(c)>=32 or c in "\t\n\r")) + except Exception as e: + return {"__err":str(e)} +def getS(h): + r=scpost("GetSessionsByName",{"sessionName":h}); return r if isinstance(r,list) else [] +def setP(sid,arr): return scpost("UpdateSessionCustomProperties",[sid,arr]) +def dtype(h,os_): + u=h.upper();o=(os_ or"").lower() + if "server" in o:return "Server" + if re.search(r'(SERVER|-SRV|\bSVR\b|SQL|HYPERV|-DC\d?$|\bNAS\b|EXCHANGE|-VM$|PROXESS|-TS\d)',u):return "Server" + if re.search(r'(LAPTOP|-LT$|YOGA|SURFACE|MACBOOK|\bLT\d)',u):return "Laptop" + return "Desktop" +DEPT=[("ACCT","Accounting"),("NURSE","Nursing"),("MFGR","Manufacturing"),("MFG","Manufacturing"), + ("RCVG","Receiving"),("QCINSP","Quality"),("PROQC","Quality"),("QC","Quality"),("MAINT","Maintenance"), + ("RECEPT","Reception"),("SHIP","Shipping"),("CHEF","Dietary"),("MEMRECEPT","Memory Care"), + ("MDIRECTOR","Memory Care"),("SALES","Sales/Marketing"),("CONF","Conference"),("ENGI","Engineering")] +def dept(h): + u=h.upper() + for pat,d in DEPT: + if pat in u: return d + return "" +agents=json.loads("".join(c for c in urllib.request.urlopen(urllib.request.Request(f"{RMM}/api/agents",headers={"Authorization":f"Bearer {TOK}"}),context=ctx,timeout=30).read().decode("utf-8","replace") if ord(c)>=32 or c in "\t\n\r")) +byc={} +for a in agents: byc.setdefault(a.get("client_name") or "",[]).append(a) +targets=args or [c for c in byc if c and c not in SKIP] +tc=td=tp=0; dupes=[]; flags=[] +for c in sorted(targets): + ms=byc.get(c,[]) + sess={}; comps=Counter() + for a in ms: + ss=getS(a["hostname"]) + if not ss: continue + sess[a["hostname"]]=(a,ss) + if len(ss)>1: dupes.append(f"{c}:{a['hostname']}") + cp=ss[0].get("CustomPropertyValues") or [] + if cp and cp[0]: comps[cp[0]]+=1 + maj=comps.most_common(1)[0][0] if comps else "" + canon = maj if re.match(r'^[A-Za-z]{2,5}\s*-\s',maj) else c.strip() + if maj and maj!=canon and not maj.strip()==canon: flags.append(f"{c}: '{maj}' -> '{canon}'") + cc=dtc=dpc=0 + for h,(a,ss) in sess.items(): + dt=dtype(h,a.get("os_type","")); dp=dept(h) + for s in ss: + cur=list(s.get("CustomPropertyValues") or []) + while len(cur)<8: cur.append("") + new=cur[:]; new[0]=canon; new[3]=dt + if dp and not new[2]: new[2]=dp + if new!=cur: + if new[0]!=cur[0]: cc+=1 + if new[3]!=cur[3]: dtc+=1 + if new[2]!=cur[2]: dpc+=1 + if not DRY: setP(s["SessionID"],new) + tc+=cc; td+=dtc; tp+=dpc + if cc or dtc or dpc or len(sess): print(f"{c}: company->'{canon}' ({cc}), devType {dtc}, dept {dpc} [{len(sess)} sess]") +print(f"\nTOTALS: company {tc}, deviceType {td}, department {tp}") +print(f"DUPLICATES ({len(dupes)}): {', '.join(dupes) or 'none'}") +print(f"COMPANY CHANGES worth a glance ({len(flags)}):") +for f in flags: print(" "+f)