sync: Auto-sync from ACG-M-L5090 at 2026-03-10 19:11:00
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>
This commit is contained in:
185
temp/vwp_bec_billing.py
Normal file
185
temp/vwp_bec_billing.py
Normal file
@@ -0,0 +1,185 @@
|
||||
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')
|
||||
Reference in New Issue
Block a user