#!/usr/bin/env python3 """GORTNET Network Assessment: read-only macOS/Linux probe (Python 3.8+). Only local discovery; no arbitrary targets; private identifiers are never uploaded. """ import argparse, json, os, platform, re, shutil, socket, ssl, subprocess, sys, urllib.request, urllib.error from pathlib import Path CHECKS = [] def check(cid, category, state, title, detail): CHECKS.append({"id": cid, "category": category, "status": state, "title": title, "detail": detail}) def run(args, timeout=6): try: p=subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, encoding='utf-8', errors='replace', timeout=timeout, check=False) return p.stdout.strip() if p.returncode==0 else "" except (OSError, subprocess.TimeoutExpired): return "" def detect_domain(): if sys.platform=="darwin": text=run(["dsconfigad","-show"]) m=re.search(r"Active Directory Domain\s*=\s*([\w.-]+)", text, re.I) return m.group(1).lower() if m else None text=run(["realm","list"]) m=re.search(r"^realm-name:\s*([\w.-]+)",text,re.I|re.M) return m.group(1).lower() if m else None def srv(name): if not shutil.which("dig"): return [] out=run(["dig","+time=2","+tries=1","+short","SRV",name]) targets=[] for line in out.splitlines(): x=line.strip().split() if len(x)==4 and x[2].isdigit(): host=x[3].rstrip(".") if re.fullmatch(r"[A-Za-z0-9.-]{1,253}",host): targets.append(host) return sorted(set(targets))[:4] def tcp(host,port,seconds=1.3): try: with socket.create_connection((host,port),timeout=seconds): return True except (OSError,TimeoutError): return False def main(): ap=argparse.ArgumentParser(description="GORTNET read-only network assessment") ap.add_argument("--assessment-id",required=True) ap.add_argument("--upload-token",required=True) ap.add_argument("--api-base",default="https://network.gortnet.de") ap.add_argument("--local-only",action="store_true",help="write sanitized JSON without uploading") ap.add_argument("--output",default="gortnet-network-result.json") args=ap.parse_args() if not re.fullmatch(r"[a-fA-F0-9-]{36}",args.assessment_id): ap.error("Invalid assessment ID") if not args.api_base.startswith("https://network.gortnet.de"): ap.error("Untrusted API origin") domain=detect_domain() osname="macOS" if sys.platform=="darwin" else "Linux" check("platform","Network","pass","Operating system",osname+" detected") if domain: check("domain-join","AD Discovery","pass","Directory membership","Directory domain detected locally; its name is never uploaded.") else: check("domain-join","AD Discovery","warning","Directory membership","No joined directory domain detected. Domain-specific checks unavailable.") dns_ok=bool(socket.getaddrinfo("example.com",443,type=socket.SOCK_STREAM)) check("dns-resolver","DNS","pass" if dns_ok else "fail","Public DNS resolution","Public DNS lookup succeeded." if dns_ok else "Public DNS lookup failed.") dcs=srv("_ldap._tcp.dc._msdcs."+domain) if domain else [] if domain: check("dns-ldap-srv","DNS","pass" if dcs else "warning","Active Directory discovery","Directory SRV targets discovered." if dcs else "No AD SRV targets discovered or dig unavailable.") kerberos=srv("_kerberos._tcp."+domain) check("dns-kerberos-srv","Kerberos","pass" if kerberos else "warning","Kerberos discovery","Kerberos SRV targets discovered." if kerberos else "Kerberos SRV records unavailable.") ports=[(53,"DNS"),(88,"Kerberos"),(389,"LDAP/LDAPS"),(445,"SMB"),(636,"LDAP/LDAPS"),(3268,"LDAP/LDAPS")] for idx, host in enumerate(dcs[:2],1): for port, category in ports: ok=tcp(host,port) check("tcp-%s-dc%s"%(port,idx),category,"pass" if ok else "fail","DC TCP/"+str(port),"Discovered domain controller port reachable." if ok else "Discovered domain controller port not reachable.") if dcs: try: conn=socket.create_connection((dcs[0],636),timeout=2) ctx=ssl.create_default_context() with ctx.wrap_socket(conn,server_hostname=dcs[0]) as tls: check("ldaps-tls","LDAP/LDAPS","pass","LDAPS certificate","TLS certificate successfully validated.") except Exception: check("ldaps-tls","LDAP/LDAPS","warning","LDAPS certificate","Certificate or connection could not be validated.") if shutil.which("klist"): listing=run(["klist"]) check("kerberos-ticket-cache","Kerberos","pass" if "krbtgt/" in listing.lower() else "warning","Kerberos ticket cache","Kerberos ticket detected." if "krbtgt/" in listing.lower() else "No TGT detected.") payload={"schemaVersion":1,"environment":{"domain":None,"domainJoined":bool(domain),"activeDirectorySite":None,"os":osname+" "+platform.release(),"dnsServers":[],"domainControllers":[]},"checks":CHECKS} Path(args.output).write_text(json.dumps(payload,indent=2),encoding="utf-8") print("Checks: %s | sanitized report: %s"%(len(CHECKS),args.output)) if args.local_only: return api=args.api_base.rstrip("/")+"/v1/assessments/"+args.assessment_id+"/results" data=json.dumps(payload).encode("utf-8") req=urllib.request.Request(api,data=data,headers={"Authorization":"Bearer "+args.upload_token,"Content-Type":"application/json"},method="POST") try: with urllib.request.urlopen(req,timeout=30) as response: print("Server response:",response.status,response.read(800).decode("utf-8","replace")) except urllib.error.HTTPError as exc: print("Upload refused:",exc.code,exc.read(400).decode("utf-8","replace"),file=sys.stderr) sys.exit(2) if __name__=="__main__": main()