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