Files
claudetools/temp/vwp_exchange_trace.py
Mike Swanson fa15b03180 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>
2026-03-10 19:59:08 -07:00

444 lines
17 KiB
Python

"""
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()