import subprocess, json, urllib.parse, secrets, string TENANT = '5c53ae9f-7071-4248-b834-8685b646450f' APP = 'fabb3421-8b34-484b-bc17-e46de9703418' SECRET = '~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO' BILLING_ID = '4f708b80-e537-4f63-92d3-5feedfa28244' def get_token(): r = subprocess.run(['curl','-s','-X','POST', f'https://login.microsoftonline.com/{TENANT}/oauth2/v2.0/token', '-d', f'client_id={APP}&client_secret={SECRET}&scope=https://graph.microsoft.com/.default&grant_type=client_credentials'], capture_output=True, text=True) return json.loads(r.stdout)['access_token'] def graph_get(token, url): r = subprocess.run(['curl','-s','-H',f'Authorization: Bearer {token}', url], capture_output=True, text=True) try: return json.loads(r.stdout) except: return {'error': {'message': r.stdout[:300] if r.stdout else 'empty'}} token = get_token() print('[OK] Token acquired') # 1. INBOX RULES print('\n' + '=' * 60) print('[CRITICAL] INBOX RULES') print('=' * 60) rules = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/mailFolders/inbox/messageRules') malicious_rules = [] for r in rules.get('value', []): enabled = '[ENABLED]' if r.get('isEnabled') else '[DISABLED]' name = r.get('displayName', '?') rid = r.get('id', '') print(f' {enabled} Rule: "{name}" (ID: {rid})') if r.get('conditions'): print(f' Conditions: {json.dumps(r["conditions"], indent=6)}') if r.get('actions'): print(f' Actions: {json.dumps(r["actions"], indent=6)}') actions = r.get('actions', {}) if actions.get('markAsRead') or actions.get('forwardTo') or actions.get('redirectTo') or len(name) <= 2: malicious_rules.append(r) print(f' >>> [SUSPICIOUS] Flagged for deletion') print() # Delete malicious rules if malicious_rules: print(f'Deleting {len(malicious_rules)} suspicious rules...') token = get_token() for r in malicious_rules: rid = r.get('id', '') encoded_id = urllib.parse.quote(rid, safe='') dr = subprocess.run(['curl','-s','-o','/dev/null','-w','%{http_code}','-X','DELETE', '-H', f'Authorization: Bearer {token}', f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/mailFolders/inbox/messageRules/{encoded_id}'], capture_output=True, text=True) status = '[OK] DELETED' if dr.stdout == '204' else f'[ERROR] HTTP {dr.stdout}' print(f' Rule "{r.get("displayName")}": {status}') else: print(' No suspicious rules found') # 2. AUTH METHODS print('\n' + '=' * 60) print('AUTHENTICATION METHODS') print('=' * 60) token = get_token() auth = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/authentication/methods') for m in auth.get('value', []): mtype = m.get('@odata.type', '').replace('#microsoft.graph.', '') name = m.get('displayName', '') mid = m.get('id', '') created = m.get('createdDateTime', 'unknown') print(f' {mtype} | {name} | ID: {mid} | Created: {created}') if 'phoneNumber' in m: print(f' Phone: {m["phoneNumber"]}') # 3. MAILBOX SETTINGS print('\n' + '=' * 60) print('MAILBOX SETTINGS') print('=' * 60) settings = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/mailboxSettings') auto = settings.get('automaticRepliesSetting', {}) print(f' Auto-replies: {auto.get("status", "unknown")}') if auto.get('status') != 'disabled': print(f' [SUSPICIOUS] Internal: {auto.get("internalReplyMessage", "")[:200]}') print(f' [SUSPICIOUS] External: {auto.get("externalReplyMessage", "")[:200]}') fwd = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}?$select=mail,otherMails,proxyAddresses') print(f' Mail: {fwd.get("mail")}') print(f' Other mails: {fwd.get("otherMails", [])}') print(f' Proxy addresses: {fwd.get("proxyAddresses", [])}') # 4. MAIL FOLDERS print('\n' + '=' * 60) print('MAIL FOLDERS') print('=' * 60) folders = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/mailFolders?$top=50&$select=displayName,totalItemCount,unreadItemCount,id') archive_id = None for f in folders.get('value', []): count = f.get('totalItemCount', 0) unread = f.get('unreadItemCount', 0) name = f.get('displayName', '?') flag = '[CHECK]' if name in ('Archive', 'RSS Feeds', 'RSS Subscriptions') and count > 0 else ' ' if count > 0: print(f' {flag} {name}: {count} items ({unread} unread)') if name == 'Archive' and count > 0: archive_id = f.get('id') # 5. SENT MAIL print('\n' + '=' * 60) print('RECENT SENT MAIL (Last 30)') print('=' * 60) sent = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/mailFolders/SentItems/messages?$top=30&$orderby=sentDateTime%20desc&$select=subject,toRecipients,sentDateTime,bodyPreview,hasAttachments') suspicious_words = ['invoice', 'payment', 'urgent', 'wire', 'transfer', 'docusign', 'password', 'verify', 'confirm', 'bank', 'account', 'shared', 'document', 'sign'] for m in sent.get('value', []): dt = m.get('sentDateTime', '') subj = m.get('subject', '(no subject)') to_list = [r.get('emailAddress', {}).get('address', '') for r in m.get('toRecipients', [])] attach = ' [ATTACH]' if m.get('hasAttachments') else '' is_suspicious = any(w in (subj or '').lower() for w in suspicious_words) flag = '[SUSPICIOUS]' if is_suspicious else ' ' print(f' {flag} {dt} | To: {", ".join(to_list)} | {subj}{attach}') if is_suspicious: preview = m.get("bodyPreview", "")[:200].encode('ascii', 'replace').decode() print(f' Preview: {preview}') # 6. RESET PASSWORD & REVOKE print('\n' + '=' * 60) print('RESET PASSWORD & REVOKE SESSIONS') print('=' * 60) token = get_token() new_pass = ''.join(secrets.choice(string.ascii_letters + string.digits + '!@#%^&*') for _ in range(16)) reset_r = subprocess.run(['curl','-s','-o','/dev/null','-w','%{http_code}','-X','PATCH', '-H', f'Authorization: Bearer {token}', '-H', 'Content-Type: application/json', '-d', json.dumps({'passwordProfile': {'forceChangePasswordNextSignIn': True, 'password': new_pass}}), f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}'], capture_output=True, text=True) print(f' Password reset: HTTP {reset_r.stdout} - {"[OK]" if reset_r.stdout == "204" else "[ERROR]"}') if reset_r.stdout == '204': print(f' New temp password: {new_pass}') print(f' (Force change on next sign-in)') revoke_r = subprocess.run(['curl','-s','-o','/dev/null','-w','%{http_code}','-X','POST', '-H', f'Authorization: Bearer {token}', '-H', 'Content-Type: application/json', f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/revokeSignInSessions'], capture_output=True, text=True) print(f' Revoke sessions: HTTP {revoke_r.stdout} - {"[OK]" if revoke_r.stdout == "200" else "[ERROR]"}') # 7. Move Archive back to Inbox if needed if archive_id: print(f'\n' + '=' * 60) print(f'MOVING ARCHIVE MESSAGES BACK TO INBOX') print('=' * 60) token = get_token() moved = 0 errors = 0 batch = 0 while True: batch += 1 if batch % 5 == 0: token = get_token() msgs = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/mailFolders/{archive_id}/messages?$top=20&$select=id,subject&$orderby=receivedDateTime%20desc') items = msgs.get('value', []) if not items: break for m in items: mr = subprocess.run(['curl','-s','-o','/dev/null','-w','%{http_code}','-X','POST', '-H', f'Authorization: Bearer {token}', '-H', 'Content-Type: application/json', '-d', json.dumps({'destinationId': 'inbox'}), f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/messages/{m["id"]}/move'], capture_output=True, text=True) if mr.stdout in ('200', '201'): moved += 1 else: errors += 1 print(f' Batch {batch}: {moved} moved, {errors} errors') if moved + errors > 2000: break print(f' [DONE] Moved {moved} messages back to Inbox ({errors} errors)') print('\n[DONE] Billing account remediation complete')