import subprocess, json, urllib.parse from datetime import datetime, timedelta TENANT = '5c53ae9f-7071-4248-b834-8685b646450f' APP = 'fabb3421-8b34-484b-bc17-e46de9703418' SECRET = '~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO' BILLING_ID = '4f708b80-e537-4f63-92d3-5feedfa28244' BILLING_UPN = 'billing@valleywideplastering.com' ATTACKER_IPS = {'23.234.100.200', '23.234.100.73', '23.234.101.73'} 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('1. INBOX RULES') print('=' * 60) rules = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/mailFolders/inbox/messageRules') if rules.get('value'): 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}"') 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'): print(f' >>> [CRITICAL] Suspicious action!') if len(name) <= 2: print(f' >>> [CRITICAL] Short name - possible attacker rule!') print() else: print(' No inbox rules found') if 'error' in rules: print(f' Error: {rules["error"].get("message", "")[:200]}') # 2. SIGN-IN LOGS print('\n' + '=' * 60) print('2. SIGN-IN LOGS (Last 30 days)') print('=' * 60) token = get_token() month_ago = (datetime.utcnow() - timedelta(days=30)).strftime('%Y-%m-%dT00:00:00Z') filter_str = urllib.parse.quote(f"userPrincipalName eq '{BILLING_UPN}' and createdDateTime ge {month_ago}") signins = graph_get(token, f'https://graph.microsoft.com/v1.0/auditLogs/signIns?%24filter={filter_str}&%24top=100&%24orderby=createdDateTime%20desc') if 'error' in signins: print(f' v1.0 Error: {signins["error"].get("message", "")[:200]}') signins = graph_get(token, f'https://graph.microsoft.com/beta/auditLogs/signIns?%24filter={filter_str}&%24top=100&%24orderby=createdDateTime%20desc') if 'error' in signins: print(f' Beta error: {signins["error"].get("message", "")[:200]}') entries = signins.get('value', []) print(f' Total entries: {len(entries)}') ips_seen = {} for s in entries: dt = s.get('createdDateTime', '') ip = s.get('ipAddress', '?') loc = s.get('location', {}) city = loc.get('city', '?') country = loc.get('countryOrRegion', '?') app = s.get('clientAppUsed', '?') resource = s.get('resourceDisplayName', '?') status = s.get('status', {}) err = status.get('errorCode', 0) risk = s.get('riskLevelDuringSignIn', 'none') is_attacker = ip in ATTACKER_IPS is_foreign = country not in ('US', '?', '') flag = '[CRITICAL]' if is_attacker else ('[SUSPICIOUS]' if is_foreign or (risk and risk != 'none') else ' ') print(f' {flag} {dt} | {ip} | {city}, {country} | {app} | {resource} | err={err} risk={risk}') if ip not in ips_seen: ips_seen[ip] = {'city': city, 'country': country, 'count': 0, 'apps': set(), 'failed': 0, 'attacker': is_attacker} ips_seen[ip]['count'] += 1 ips_seen[ip]['apps'].add(app) if err != 0: ips_seen[ip]['failed'] += 1 print(f'\n Unique IPs:') for ip, info in sorted(ips_seen.items(), key=lambda x: -x[1]['count']): flag = '[CRITICAL]' if info['attacker'] else ('[SUSPICIOUS]' if info['country'] not in ('US', '?', '') else ' ') print(f' {flag} {ip} | {info["city"]}, {info["country"]} | {info["count"]}x ({info["failed"]} failed) | Apps: {", ".join(info["apps"])}') # 3. SENT MAIL print('\n' + '=' * 60) print('3. SENT MAIL (Last 100)') print('=' * 60) token = get_token() sent = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/mailFolders/SentItems/messages?$top=100&$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'] susp_count = 0 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 ' ' if is_suspicious: susp_count += 1 print(f' {flag} {dt} | To: {", ".join(to_list[:3])} | {subj}{attach}') if is_suspicious: preview = m.get("bodyPreview", "")[:200].encode('ascii', 'replace').decode() print(f' Preview: {preview}') print(f'\n Total sent: {len(sent.get("value", []))} | Flagged: {susp_count}') # 4. AUTH METHODS print('\n' + '=' * 60) print('4. AUTHENTICATION METHODS') print('=' * 60) 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"]}') # 5. MAILBOX SETTINGS print('\n' + '=' * 60) print('5. 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", [])}') # 6. MAIL FOLDERS print('\n' + '=' * 60) print('6. MAIL FOLDERS') print('=' * 60) token = get_token() 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 == 'Archive' and count > 0 else ' ' if name == 'Archive' and count > 0: archive_id = f.get('id') if count > 0: print(f' {flag} {name}: {count} items ({unread} unread)') # Check child folders print('\n Child folders:') for f in folders.get('value', []): fid = f.get('id', '') fname = f.get('displayName', '') children = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/mailFolders/{fid}/childFolders?$select=displayName,totalItemCount,id') for child in children.get('value', []): cname = child.get('displayName', '?') ccount = child.get('totalItemCount', 0) if ccount > 0: print(f' {fname}/{cname}: {ccount} items') # 7. OAUTH GRANTS print('\n' + '=' * 60) print('7. OAUTH PERMISSION GRANTS') print('=' * 60) grants = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/oauth2PermissionGrants') if grants.get('value'): for g in grants['value']: print(f' [SUSPICIOUS] Client: {g.get("clientId")} | Scope: {g.get("scope")} | ConsentType: {g.get("consentType")}') else: print(' [OK] No OAuth grants found') # 8. RECENT INBOX print('\n' + '=' * 60) print('8. RECENT INBOX (Last 7 days)') print('=' * 60) token = get_token() week_ago = (datetime.utcnow() - timedelta(days=7)).strftime('%Y-%m-%dT00:00:00Z') inbox = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/mailFolders/inbox/messages?$top=50&$orderby=receivedDateTime%20desc&$select=subject,from,receivedDateTime,hasAttachments,isRead&$filter=receivedDateTime%20ge%20{week_ago}') box_count = 0 for m in inbox.get('value', []): dt = m.get('receivedDateTime', '') subj = m.get('subject', '(no subject)') sender = m.get('from', {}).get('emailAddress', {}).get('address', '?') attach = ' [ATTACH]' if m.get('hasAttachments') else '' is_box = 'box.com' in (subj or '').lower() or 'box.com' in sender.lower() is_reset = 'password' in (subj or '').lower() or 'reset' in (subj or '').lower() flag = '[CHECK]' if is_box or is_reset else ' ' if is_box: box_count += 1 print(f' {flag} {dt} | From: {sender} | {subj}{attach}') print(f'\n Box.com related: {box_count}') # 9. DELETED ITEMS print('\n' + '=' * 60) print('9. DELETED ITEMS (Last 50)') print('=' * 60) deleted = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/mailFolders/deleteditems/messages?$top=50&$orderby=receivedDateTime%20desc&$select=subject,from,receivedDateTime,hasAttachments') for m in deleted.get('value', []): dt = m.get('receivedDateTime', '') subj = m.get('subject', '(no subject)') sender = m.get('from', {}).get('emailAddress', {}).get('address', '?') attach = ' [ATTACH]' if m.get('hasAttachments') else '' is_box = 'box.com' in (subj or '').lower() or 'box.com' in sender.lower() flag = '[CHECK]' if is_box else ' ' print(f' {flag} {dt} | From: {sender} | {subj}{attach}') # 10. ARCHIVE print('\n' + '=' * 60) print('10. ARCHIVE FOLDER') print('=' * 60) if archive_id: archive_msgs = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{BILLING_ID}/mailFolders/{archive_id}/messages?$top=50&$orderby=receivedDateTime%20desc&$select=subject,from,receivedDateTime,isRead') items = archive_msgs.get('value', []) print(f' Archive items found: {len(items)}') for m in items: dt = m.get('receivedDateTime', '') subj = m.get('subject', '(no subject)') sender = m.get('from', {}).get('emailAddress', {}).get('address', '?') read_status = '' if m.get('isRead') else ' [UNREAD]' print(f' {dt} | From: {sender} | {subj}{read_status}') else: print(' [OK] No items in Archive') print('\n' + '=' * 60) print('[DONE] Billing account deep check complete') print('=' * 60)