Synced files: - Quote wizard frontend (all components, hooks, types, config) - API updates (config, models, routers, schemas, services) - Client work (bg-builders, gurushow) - Scripts (BGB Lesley termination, CIPP, Datto, migration) - Temp files (Bardach contacts, VWP investigation, misc) - Credentials and session logs - Email service, PHP API, session logs Machine: ACG-M-L5090 Timestamp: 2026-03-10 19:11:00 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
148 lines
6.7 KiB
Python
148 lines
6.7 KiB
Python
import subprocess, json, sys, urllib.parse
|
|
from datetime import datetime, timedelta
|
|
|
|
TENANT = '5c53ae9f-7071-4248-b834-8685b646450f'
|
|
APP = 'fabb3421-8b34-484b-bc17-e46de9703418'
|
|
SECRET = '~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO'
|
|
JR_UPN = 'j-r@valleywideplastering.com'
|
|
JR_ID = '0af923d0-48c5-4cc1-8553-c60625802815'
|
|
|
|
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)
|
|
return json.loads(r.stdout)
|
|
|
|
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/{JR_ID}/mailFolders/inbox/messageRules')
|
|
for r in rules.get('value', []):
|
|
enabled = '[ENABLED]' if r.get('isEnabled') else '[DISABLED]'
|
|
name = r.get('displayName', '?')
|
|
print(f' {enabled} Rule: "{name}"')
|
|
print(f' ID: {r.get("id")}')
|
|
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)}')
|
|
print()
|
|
|
|
# 2. Mailbox settings
|
|
print('=' * 60)
|
|
print('MAILBOX SETTINGS')
|
|
print('=' * 60)
|
|
settings = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{JR_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/{JR_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", [])}')
|
|
|
|
# 3. Auth methods
|
|
print('\n' + '=' * 60)
|
|
print('AUTHENTICATION METHODS')
|
|
print('=' * 60)
|
|
auth = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{JR_ID}/authentication/methods')
|
|
for m in auth.get('value', []):
|
|
mtype = m.get('@odata.type', '').replace('#microsoft.graph.', '')
|
|
print(f' {mtype}: {m.get("displayName", "")} (created: {m.get("createdDateTime", "unknown")})')
|
|
if 'phoneNumber' in m:
|
|
print(f' Phone: {m["phoneNumber"]}')
|
|
|
|
# 4. Sign-in logs
|
|
print('\n' + '=' * 60)
|
|
print('SIGN-IN LOGS (Last 14 days)')
|
|
print('=' * 60)
|
|
week_ago = (datetime.utcnow() - timedelta(days=14)).strftime('%Y-%m-%dT00:00:00Z')
|
|
filter_str = urllib.parse.quote(f"userPrincipalName eq '{JR_UPN}' and createdDateTime ge {week_ago}")
|
|
signins = graph_get(token, f'https://graph.microsoft.com/v1.0/auditLogs/signIns?$filter={filter_str}&$top=50&$orderby=createdDateTime desc')
|
|
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?$filter={filter_str}&$top=50&$orderby=createdDateTime desc')
|
|
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[:50]:
|
|
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')
|
|
flag = '[SUSPICIOUS]' if (risk and risk != 'none') or (country and country not in ('US', '?', '')) 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, 'first': dt, 'count': 0}
|
|
ips_seen[ip]['count'] += 1
|
|
|
|
print(f'\n Unique IPs:')
|
|
for ip, info in sorted(ips_seen.items(), key=lambda x: -x[1]['count']):
|
|
flag = '[SUSPICIOUS]' if info['country'] not in ('US', '?', '') else ' '
|
|
print(f' {flag} {ip} | {info["city"]}, {info["country"]} | {info["count"]}x')
|
|
|
|
# 5. OAuth grants
|
|
print('\n' + '=' * 60)
|
|
print('OAUTH PERMISSION GRANTS')
|
|
print('=' * 60)
|
|
grants = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{JR_ID}/oauth2PermissionGrants')
|
|
if grants.get('value'):
|
|
for g in grants['value']:
|
|
print(f' Client: {g.get("clientId")} | Scope: {g.get("scope")} | ConsentType: {g.get("consentType")}')
|
|
else:
|
|
print(' No OAuth grants found')
|
|
|
|
# 6. Recent sent mail - check for suspicious patterns
|
|
print('\n' + '=' * 60)
|
|
print('RECENT SENT MAIL (Last 50)')
|
|
print('=' * 60)
|
|
sent = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{JR_ID}/mailFolders/SentItems/messages?$top=50&$orderby=sentDateTime desc&$select=subject,toRecipients,sentDateTime,bodyPreview,hasAttachments')
|
|
suspicious_words = ['invoice', 'payment', 'urgent', 'wire', 'transfer', 'docusign', 'password', 'verify', 'confirm', 'bank', 'account']
|
|
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 = ' [HAS ATTACHMENTS]' 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:
|
|
print(f' Preview: {m.get("bodyPreview", "")[:200]}')
|
|
|
|
# 7. Resolve the folder that rules move to
|
|
print('\n' + '=' * 60)
|
|
print('RESOLVE RULE TARGET FOLDERS')
|
|
print('=' * 60)
|
|
for r in rules.get('value', []):
|
|
if r.get('actions') and r['actions'].get('moveToFolder'):
|
|
folder_id = r['actions']['moveToFolder']
|
|
try:
|
|
folder = graph_get(token, f'https://graph.microsoft.com/v1.0/users/{JR_ID}/mailFolders/{folder_id}?$select=displayName,totalItemCount,unreadItemCount')
|
|
print(f' Rule "{r.get("displayName")}" moves to: {folder.get("displayName", "?")} ({folder.get("totalItemCount", "?")} items, {folder.get("unreadItemCount", "?")} unread)')
|
|
except:
|
|
print(f' Could not resolve folder {folder_id[:40]}...')
|
|
|
|
print('\n[DONE] Investigation complete')
|