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:
443
temp/vwp_exchange_trace.py
Normal file
443
temp/vwp_exchange_trace.py
Normal file
@@ -0,0 +1,443 @@
|
||||
"""
|
||||
VWP BEC Investigation - Exchange Email Trace
|
||||
Find recipient email addresses that received phishing Box.com links from JR's account.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# Credentials
|
||||
TENANT_ID = "5c53ae9f-7071-4248-b834-8685b646450f"
|
||||
APP_ID = "fabb3421-8b34-484b-bc17-e46de9703418"
|
||||
SECRET = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO"
|
||||
JR_ID = "0af923d0-48c5-4cc1-8553-c60625802815"
|
||||
|
||||
GRAPH_BASE = "https://graph.microsoft.com/v1.0"
|
||||
|
||||
def get_token():
|
||||
"""Get OAuth token via client credentials flow."""
|
||||
url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token"
|
||||
result = subprocess.run([
|
||||
"curl", "-s", "-X", "POST", url,
|
||||
"-H", "Content-Type: application/x-www-form-urlencoded",
|
||||
"-d", f"client_id={APP_ID}&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default&client_secret={SECRET}&grant_type=client_credentials"
|
||||
], capture_output=True, text=True)
|
||||
data = json.loads(result.stdout)
|
||||
if "access_token" not in data:
|
||||
print(f"[ERROR] Token acquisition failed: {json.dumps(data, indent=2)}")
|
||||
sys.exit(1)
|
||||
print("[OK] Token acquired successfully")
|
||||
return data["access_token"]
|
||||
|
||||
|
||||
def graph_get(token, url):
|
||||
"""Make a GET request to Graph API."""
|
||||
result = subprocess.run([
|
||||
"curl", "-s", "-X", "GET", url,
|
||||
"-H", f"Authorization: Bearer {token}",
|
||||
"-H", "Content-Type: application/json"
|
||||
], capture_output=True, text=True)
|
||||
try:
|
||||
return json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
print(f"[ERROR] Failed to parse response from {url}")
|
||||
print(f" Raw: {result.stdout[:500]}")
|
||||
return None
|
||||
|
||||
|
||||
def extract_recipients(msg):
|
||||
"""Extract all recipient email addresses from a message."""
|
||||
recipients = set()
|
||||
for field in ["toRecipients", "ccRecipients", "bccRecipients"]:
|
||||
for r in msg.get(field, []) or []:
|
||||
email = r.get("emailAddress", {}).get("address", "")
|
||||
if email:
|
||||
recipients.add(email.lower())
|
||||
return recipients
|
||||
|
||||
|
||||
def extract_emails_from_html(html_text):
|
||||
"""Extract email addresses from HTML body content."""
|
||||
if not html_text:
|
||||
return set()
|
||||
# Standard email regex
|
||||
pattern = r'[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}'
|
||||
found = set(e.lower() for e in re.findall(pattern, html_text))
|
||||
# Filter out common system/noreply addresses
|
||||
system_addrs = {"noreply@box.com", "no-reply@box.com", "support@box.com"}
|
||||
return found - system_addrs
|
||||
|
||||
|
||||
def approach1_search_sent_box(token):
|
||||
"""Search JR's sent mail for messages containing box.com links."""
|
||||
print("\n" + "="*70)
|
||||
print("APPROACH 1: Search JR's Sent Items for Box.com links")
|
||||
print("="*70)
|
||||
|
||||
all_messages = []
|
||||
all_recipients = set()
|
||||
|
||||
# Search 1: "box.com" in all mail
|
||||
url = (f"{GRAPH_BASE}/users/{JR_ID}/messages"
|
||||
f"?$search=%22box.com%22"
|
||||
f"&$top=100"
|
||||
f"&$select=subject,toRecipients,ccRecipients,bccRecipients,sentDateTime,from,sender,parentFolderId")
|
||||
|
||||
print("\n[INFO] Searching all mail for 'box.com'...")
|
||||
data = graph_get(token, url)
|
||||
if data and "value" in data:
|
||||
msgs = data["value"]
|
||||
print(f" Found {len(msgs)} messages matching 'box.com'")
|
||||
for msg in msgs:
|
||||
sender = msg.get("from", {}).get("emailAddress", {}).get("address", "unknown")
|
||||
subj = msg.get("subject", "(no subject)")
|
||||
sent = msg.get("sentDateTime", "")
|
||||
recips = extract_recipients(msg)
|
||||
all_recipients.update(recips)
|
||||
all_messages.append({
|
||||
"subject": subj,
|
||||
"sender": sender,
|
||||
"sentDateTime": sent,
|
||||
"recipients": list(recips),
|
||||
"source": "approach1_box_search"
|
||||
})
|
||||
recip_str = ", ".join(recips) if recips else "(none visible)"
|
||||
print(f" [{sent[:10] if sent else '??'}] From: {sender}")
|
||||
print(f" Subject: {subj[:80]}")
|
||||
print(f" To: {recip_str}")
|
||||
|
||||
# Check for pagination
|
||||
next_link = data.get("@odata.nextLink")
|
||||
page = 2
|
||||
while next_link and page <= 5:
|
||||
print(f" Fetching page {page}...")
|
||||
data = graph_get(token, next_link)
|
||||
if data and "value" in data:
|
||||
for msg in data["value"]:
|
||||
sender = msg.get("from", {}).get("emailAddress", {}).get("address", "unknown")
|
||||
subj = msg.get("subject", "(no subject)")
|
||||
sent = msg.get("sentDateTime", "")
|
||||
recips = extract_recipients(msg)
|
||||
all_recipients.update(recips)
|
||||
all_messages.append({
|
||||
"subject": subj,
|
||||
"sender": sender,
|
||||
"sentDateTime": sent,
|
||||
"recipients": list(recips),
|
||||
"source": "approach1_box_search_page" + str(page)
|
||||
})
|
||||
recip_str = ", ".join(recips) if recips else "(none visible)"
|
||||
print(f" [{sent[:10] if sent else '??'}] From: {sender}")
|
||||
print(f" Subject: {subj[:80]}")
|
||||
print(f" To: {recip_str}")
|
||||
next_link = data.get("@odata.nextLink")
|
||||
else:
|
||||
break
|
||||
page += 1
|
||||
else:
|
||||
print(f" [WARNING] No results or error: {json.dumps(data, indent=2)[:300] if data else 'None'}")
|
||||
|
||||
# Search 2: "Valley Wide Plastering" in all mail
|
||||
url2 = (f"{GRAPH_BASE}/users/{JR_ID}/messages"
|
||||
f"?$search=%22Valley+Wide+Plastering%22"
|
||||
f"&$top=100"
|
||||
f"&$select=subject,toRecipients,ccRecipients,bccRecipients,sentDateTime,from,sender")
|
||||
|
||||
print("\n[INFO] Searching all mail for 'Valley Wide Plastering'...")
|
||||
data2 = graph_get(token, url2)
|
||||
if data2 and "value" in data2:
|
||||
msgs = data2["value"]
|
||||
print(f" Found {len(msgs)} messages matching 'Valley Wide Plastering'")
|
||||
for msg in msgs:
|
||||
sender = msg.get("from", {}).get("emailAddress", {}).get("address", "unknown")
|
||||
subj = msg.get("subject", "(no subject)")
|
||||
sent = msg.get("sentDateTime", "")
|
||||
recips = extract_recipients(msg)
|
||||
all_recipients.update(recips)
|
||||
# Avoid duplicates in message list
|
||||
all_messages.append({
|
||||
"subject": subj,
|
||||
"sender": sender,
|
||||
"sentDateTime": sent,
|
||||
"recipients": list(recips),
|
||||
"source": "approach1_vwp_search"
|
||||
})
|
||||
recip_str = ", ".join(recips) if recips else "(none visible)"
|
||||
print(f" [{sent[:10] if sent else '??'}] From: {sender}")
|
||||
print(f" Subject: {subj[:80]}")
|
||||
print(f" To: {recip_str}")
|
||||
else:
|
||||
print(f" [WARNING] No results or error: {json.dumps(data2, indent=2)[:300] if data2 else 'None'}")
|
||||
|
||||
print(f"\n[INFO] Approach 1 unique recipients: {len(all_recipients)}")
|
||||
return all_messages, all_recipients
|
||||
|
||||
|
||||
def approach2_box_notifications(token):
|
||||
"""Get full HTML body of Box acceptance notification emails."""
|
||||
print("\n" + "="*70)
|
||||
print("APPROACH 2: Full HTML body of Box acceptance notifications")
|
||||
print("="*70)
|
||||
|
||||
all_recipients = set()
|
||||
all_messages = []
|
||||
|
||||
# Search for noreply@box.com messages
|
||||
url = (f"{GRAPH_BASE}/users/{JR_ID}/messages"
|
||||
f"?$search=%22from%3Anoreply%40box.com%22"
|
||||
f"&$top=10"
|
||||
f"&$select=subject,toRecipients,ccRecipients,bccRecipients,sentDateTime,from,body")
|
||||
|
||||
print("\n[INFO] Searching for emails from noreply@box.com (with full body)...")
|
||||
data = graph_get(token, url)
|
||||
if data and "value" in data:
|
||||
msgs = data["value"]
|
||||
print(f" Found {len(msgs)} messages from Box notifications")
|
||||
for i, msg in enumerate(msgs[:5]):
|
||||
subj = msg.get("subject", "(no subject)")
|
||||
sent = msg.get("sentDateTime", "")
|
||||
body = msg.get("body", {})
|
||||
body_content = body.get("content", "") if body else ""
|
||||
|
||||
print(f"\n --- Notification {i+1} ---")
|
||||
print(f" Subject: {subj[:80]}")
|
||||
print(f" Date: {sent}")
|
||||
|
||||
# Extract emails from HTML body
|
||||
emails_found = extract_emails_from_html(body_content)
|
||||
recips = extract_recipients(msg)
|
||||
all_recipients.update(recips)
|
||||
all_recipients.update(emails_found)
|
||||
|
||||
if emails_found:
|
||||
print(f" Emails in body: {', '.join(emails_found)}")
|
||||
else:
|
||||
print(f" No extra emails found in HTML body")
|
||||
|
||||
if recips:
|
||||
print(f" Header recipients: {', '.join(recips)}")
|
||||
|
||||
# Show a snippet of body for debugging
|
||||
if body_content:
|
||||
# Strip HTML tags for preview
|
||||
text_preview = re.sub(r'<[^>]+>', ' ', body_content)
|
||||
text_preview = re.sub(r'\s+', ' ', text_preview).strip()
|
||||
print(f" Body preview: {text_preview[:200]}...")
|
||||
|
||||
all_messages.append({
|
||||
"subject": subj,
|
||||
"sentDateTime": sent,
|
||||
"emails_in_body": list(emails_found),
|
||||
"header_recipients": list(recips),
|
||||
"source": "approach2_box_notification"
|
||||
})
|
||||
else:
|
||||
print(f" [WARNING] No results or error: {json.dumps(data, indent=2)[:300] if data else 'None'}")
|
||||
|
||||
# Also try searching for "accepted" from box.com
|
||||
url2 = (f"{GRAPH_BASE}/users/{JR_ID}/messages"
|
||||
f"?$search=%22accepted%22+%22box.com%22"
|
||||
f"&$top=10"
|
||||
f"&$select=subject,toRecipients,ccRecipients,bccRecipients,sentDateTime,from,body")
|
||||
|
||||
print("\n[INFO] Searching for 'accepted' + 'box.com' messages (with full body)...")
|
||||
data2 = graph_get(token, url2)
|
||||
if data2 and "value" in data2:
|
||||
msgs = data2["value"]
|
||||
print(f" Found {len(msgs)} messages")
|
||||
for i, msg in enumerate(msgs[:5]):
|
||||
subj = msg.get("subject", "(no subject)")
|
||||
sent = msg.get("sentDateTime", "")
|
||||
body = msg.get("body", {})
|
||||
body_content = body.get("content", "") if body else ""
|
||||
|
||||
print(f"\n --- Message {i+1} ---")
|
||||
print(f" Subject: {subj[:80]}")
|
||||
print(f" Date: {sent}")
|
||||
|
||||
emails_found = extract_emails_from_html(body_content)
|
||||
recips = extract_recipients(msg)
|
||||
all_recipients.update(recips)
|
||||
all_recipients.update(emails_found)
|
||||
|
||||
if emails_found:
|
||||
print(f" Emails in body: {', '.join(emails_found)}")
|
||||
if recips:
|
||||
print(f" Header recipients: {', '.join(recips)}")
|
||||
|
||||
all_messages.append({
|
||||
"subject": subj,
|
||||
"sentDateTime": sent,
|
||||
"emails_in_body": list(emails_found),
|
||||
"header_recipients": list(recips),
|
||||
"source": "approach2_accepted_search"
|
||||
})
|
||||
else:
|
||||
print(f" [WARNING] No results or error")
|
||||
|
||||
print(f"\n[INFO] Approach 2 unique recipients: {len(all_recipients)}")
|
||||
return all_messages, all_recipients
|
||||
|
||||
|
||||
def approach3_box_invitations(token):
|
||||
"""Search for Box invitation emails (invited you / shared)."""
|
||||
print("\n" + "="*70)
|
||||
print("APPROACH 3: Search for Box invitation/sharing emails")
|
||||
print("="*70)
|
||||
|
||||
all_recipients = set()
|
||||
all_messages = []
|
||||
|
||||
searches = [
|
||||
("invited you", f"{GRAPH_BASE}/users/{JR_ID}/messages"
|
||||
f"?$search=%22invited+you%22"
|
||||
f"&$top=50"
|
||||
f"&$select=subject,toRecipients,ccRecipients,bccRecipients,sentDateTime,from,sender,body"),
|
||||
("shared with you box", f"{GRAPH_BASE}/users/{JR_ID}/messages"
|
||||
f"?$search=%22shared%22+%22box.com%22"
|
||||
f"&$top=50"
|
||||
f"&$select=subject,toRecipients,ccRecipients,bccRecipients,sentDateTime,from,sender,body"),
|
||||
("invitation box", f"{GRAPH_BASE}/users/{JR_ID}/messages"
|
||||
f"?$search=%22invitation%22+%22box%22"
|
||||
f"&$top=50"
|
||||
f"&$select=subject,toRecipients,ccRecipients,bccRecipients,sentDateTime,from,sender,body"),
|
||||
]
|
||||
|
||||
for label, url in searches:
|
||||
print(f"\n[INFO] Searching for '{label}'...")
|
||||
data = graph_get(token, url)
|
||||
if data and "value" in data:
|
||||
msgs = data["value"]
|
||||
print(f" Found {len(msgs)} messages")
|
||||
for msg in msgs:
|
||||
sender = msg.get("from", {}).get("emailAddress", {}).get("address", "unknown")
|
||||
subj = msg.get("subject", "(no subject)")
|
||||
sent = msg.get("sentDateTime", "")
|
||||
body = msg.get("body", {})
|
||||
body_content = body.get("content", "") if body else ""
|
||||
|
||||
recips = extract_recipients(msg)
|
||||
emails_in_body = extract_emails_from_html(body_content)
|
||||
all_recipients.update(recips)
|
||||
all_recipients.update(emails_in_body)
|
||||
|
||||
recip_str = ", ".join(recips) if recips else "(none)"
|
||||
body_emails_str = ", ".join(emails_in_body) if emails_in_body else "(none)"
|
||||
|
||||
try:
|
||||
safe_subj = subj[:80].encode('ascii', errors='replace').decode('ascii')
|
||||
print(f" [{sent[:10] if sent else '??'}] From: {sender}")
|
||||
print(f" Subject: {safe_subj}")
|
||||
print(f" Header recipients: {recip_str}")
|
||||
if emails_in_body:
|
||||
print(f" Body emails: {body_emails_str}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
all_messages.append({
|
||||
"subject": subj[:80],
|
||||
"sender": sender,
|
||||
"sentDateTime": sent,
|
||||
"recipients": list(recips),
|
||||
"emails_in_body": list(emails_in_body),
|
||||
"source": f"approach3_{label.replace(' ', '_')}"
|
||||
})
|
||||
else:
|
||||
print(f" [WARNING] No results or error")
|
||||
|
||||
print(f"\n[INFO] Approach 3 unique recipients: {len(all_recipients)}")
|
||||
return all_messages, all_recipients
|
||||
|
||||
|
||||
def main():
|
||||
print("VWP BEC Investigation - Exchange Email Trace")
|
||||
print(f"Timestamp: {datetime.now().isoformat()}")
|
||||
print(f"Target: JR ({JR_ID})")
|
||||
print("="*70)
|
||||
sys.stdout.flush()
|
||||
|
||||
token = get_token()
|
||||
sys.stdout.flush()
|
||||
|
||||
# Run all three approaches
|
||||
try:
|
||||
msgs1, recips1 = approach1_search_sent_box(token)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Approach 1 failed: {e}")
|
||||
msgs1, recips1 = [], set()
|
||||
sys.stdout.flush()
|
||||
|
||||
try:
|
||||
msgs2, recips2 = approach2_box_notifications(token)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Approach 2 failed: {e}")
|
||||
msgs2, recips2 = [], set()
|
||||
sys.stdout.flush()
|
||||
|
||||
try:
|
||||
msgs3, recips3 = approach3_box_invitations(token)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Approach 3 failed: {e}")
|
||||
msgs3, recips3 = [], set()
|
||||
sys.stdout.flush()
|
||||
|
||||
# Combine all results
|
||||
all_recipients = recips1 | recips2 | recips3
|
||||
all_messages = msgs1 + msgs2 + msgs3
|
||||
|
||||
# Identify JR's own email addresses
|
||||
jr_emails = {"j-r@valleywideplastering.com"}
|
||||
for msg in all_messages:
|
||||
sender = msg.get("sender", "")
|
||||
if sender and "valleywideplastering" in sender.lower():
|
||||
jr_emails.add(sender.lower())
|
||||
|
||||
# Filter out JR's own addresses and noreply
|
||||
system_addrs = {"noreply@box.com", "no-reply@box.com"}
|
||||
victim_recipients = all_recipients - jr_emails - system_addrs
|
||||
|
||||
# Save results FIRST before printing summary
|
||||
output = {
|
||||
"investigation": "VWP BEC - Exchange Email Trace",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"target_user_id": JR_ID,
|
||||
"jr_email_addresses": sorted(jr_emails),
|
||||
"all_unique_recipients": sorted(all_recipients),
|
||||
"victim_recipients_excluding_jr": sorted(victim_recipients),
|
||||
"total_messages_analyzed": len(all_messages),
|
||||
"approach1_count": len(msgs1),
|
||||
"approach2_count": len(msgs2),
|
||||
"approach3_count": len(msgs3),
|
||||
"messages": all_messages
|
||||
}
|
||||
|
||||
output_path = r"D:\ClaudeTools\temp\vwp_exchange_recipients.json"
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(output, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Now print summary
|
||||
print("\n" + "="*70)
|
||||
print("SUMMARY")
|
||||
print("="*70)
|
||||
print(f"Total messages found across all approaches: {len(all_messages)}")
|
||||
print(f" Approach 1 (box.com search): {len(msgs1)} messages")
|
||||
print(f" Approach 2 (notification bodies): {len(msgs2)} messages")
|
||||
print(f" Approach 3 (invitation search): {len(msgs3)} messages")
|
||||
print(f"Total unique email addresses (all): {len(all_recipients)}")
|
||||
print(f"JR's own addresses: {jr_emails}")
|
||||
print(f"Unique victim addresses (excluding JR + system): {len(victim_recipients)}")
|
||||
|
||||
print("\nAll unique victim email addresses:")
|
||||
for i, email in enumerate(sorted(victim_recipients), 1):
|
||||
print(f" {i}. {email}")
|
||||
|
||||
print(f"\n[OK] Results saved to {output_path}")
|
||||
print(f"[INFO] {len(victim_recipients)} unique victim recipient addresses identified")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user