#!/usr/bin/env python3 """ Valley Wide Plastering - BEC (Business Email Compromise) Investigation Target: jrguerrero@valleywideplastering.com Date: 2026-03-05 """ import subprocess import json import sys import os from datetime import datetime, timedelta, timezone # Configuration TENANT_ID = "5c53ae9f-7071-4248-b834-8685b646450f" CLIENT_ID = "fabb3421-8b34-484b-bc17-e46de9703418" CLIENT_SECRET = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO" GRAPH_BASE = "https://graph.microsoft.com/v1.0" GRAPH_BETA = "https://graph.microsoft.com/beta" TARGET_USER = "jrguerrero" TARGET_DOMAIN = "valleywideplastering.com" RESULTS_FILE = "D:/ClaudeTools/temp/vwp_bec_results.json" results = {} def get_token(): """Get OAuth2 token via client credentials flow.""" cmd = [ "curl", "-s", "-X", "POST", f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token", "-H", "Content-Type: application/x-www-form-urlencoded", "-d", f"client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default&grant_type=client_credentials" ] result = subprocess.run(cmd, capture_output=True, text=True) data = json.loads(result.stdout) if "access_token" not in data: print(f"[ERROR] Failed to get token: {json.dumps(data, indent=2)}") sys.exit(1) return data["access_token"] def graph_get(token, url, label=""): """Make a GET request to Microsoft Graph API.""" cmd = ["curl", "-s", "-G", "-H", f"Authorization: Bearer {token}"] # Split URL and query params to let curl handle encoding if "?" in url: base_url, query_string = url.split("?", 1) cmd.append(base_url) # Pass each query param via --data-urlencode for param in query_string.split("&"): if "=" in param: cmd.extend(["--data-urlencode", param]) else: cmd.extend(["--data-urlencode", param]) else: cmd.append(url) result = subprocess.run(cmd, capture_output=True, text=True) try: data = json.loads(result.stdout) except json.JSONDecodeError: data = {"error": "Failed to parse response", "raw": result.stdout[:500]} if "error" in data: err_msg = data['error'].get('message', data['error']) if isinstance(data['error'], dict) else data['error'] print(f" [WARNING] {label}: {err_msg}") return data def graph_get_all_pages(token, url, label=""): """Get all pages of a paginated Graph API response.""" all_values = [] current_url = url page = 0 while current_url: page += 1 data = graph_get(token, current_url, f"{label} page {page}") if "value" in data: all_values.extend(data["value"]) else: break current_url = data.get("@odata.nextLink") return all_values def print_separator(title): print(f"\n{'='*70}") print(f" {title}") print(f"{'='*70}") def main(): print("=" * 70) print(" VALLEY WIDE PLASTERING - BEC INVESTIGATION") print(f" Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}") print("=" * 70) # Get token print("\n[*] Acquiring access token...") token = get_token() print("[OK] Token acquired successfully") # ===== STEP 1: LIST ALL USERS ===== print_separator("STEP 1: ALL TENANT USERS") users_data = graph_get(token, f"{GRAPH_BASE}/users?$select=id,displayName,userPrincipalName,mail,accountEnabled,createdDateTime", "users") users = users_data.get("value", []) results["users"] = users jr_id = None jr_upn = None all_upns = [] for u in users: upn = u.get("userPrincipalName", "") all_upns.append(upn) enabled = u.get("accountEnabled", False) created = u.get("createdDateTime", "N/A") status = "[ENABLED]" if enabled else "[DISABLED]" name = u.get("displayName", "N/A") uid = u.get("id", "N/A") print(f" {status} {name} | {upn} | ID: {uid} | Created: {created}") if TARGET_USER.lower() in upn.lower(): jr_id = uid jr_upn = upn print(f" >>> TARGET USER FOUND: {jr_upn} (ID: {jr_id})") if not jr_id: print(f"\n[INFO] Exact match for '{TARGET_USER}' not found, searching by name...") # Search for JR Guerrero specifically for u in users: upn = u.get("userPrincipalName", "") name = u.get("displayName", "") # Match "JR Guerrero" by display name or j-r@ pattern if name.lower() == "jr guerrero" and TARGET_DOMAIN in upn.lower(): jr_id = u["id"] jr_upn = upn print(f" >>> TARGET USER FOUND: {jr_upn} (ID: {jr_id})") break # Fallback broader search if not jr_id: for u in users: upn = u.get("userPrincipalName", "") name = u.get("displayName", "") if ("j-r" in upn.lower() or "jr" in upn.lower()) and TARGET_DOMAIN in upn.lower(): jr_id = u["id"] jr_upn = upn print(f" >>> TARGET USER FOUND: {jr_upn} (ID: {jr_id})") break if not jr_id: print("[ERROR] Cannot proceed without target user. Exiting.") results["error"] = "Target user not found" with open(RESULTS_FILE, "w") as f: json.dump(results, f, indent=2) sys.exit(1) results["target_user"] = {"id": jr_id, "upn": jr_upn} # ===== STEP 2: SIGN-IN LOGS ===== print_separator("STEP 2: SIGN-IN LOGS (Last 14 Days)") # Try v1.0 first, then beta signin_url = f"{GRAPH_BASE}/auditLogs/signIns?$filter=userPrincipalName eq '{jr_upn}'&$top=100&$orderby=createdDateTime desc" signin_data = graph_get(token, signin_url, "sign-ins v1.0") signins = signin_data.get("value", []) if not signins and "error" in signin_data: print(" [*] Trying beta endpoint...") signin_url = f"{GRAPH_BETA}/auditLogs/signIns?$filter=userPrincipalName eq '{jr_upn}'&$top=100&$orderby=createdDateTime desc" signin_data = graph_get(token, signin_url, "sign-ins beta") signins = signin_data.get("value", []) results["signins"] = signins if signins: ips_seen = {} locations_seen = {} failed_count = 0 risky_count = 0 legacy_auth = [] timestamps_by_ip = {} for s in signins: ts = s.get("createdDateTime", "N/A") ip = s.get("ipAddress", "N/A") loc = s.get("location", {}) city = loc.get("city", "Unknown") state = loc.get("state", "Unknown") country = loc.get("countryOrRegion", "Unknown") loc_str = f"{city}, {state}, {country}" status_code = s.get("status", {}).get("errorCode", 0) status_reason = s.get("status", {}).get("failureReason", "") risk_level = s.get("riskLevelDuringSignIn", "none") risk_state = s.get("riskState", "none") app = s.get("clientAppUsed", "N/A") resource = s.get("resourceDisplayName", "N/A") app_name = s.get("appDisplayName", "N/A") # Track IPs ips_seen[ip] = ips_seen.get(ip, 0) + 1 locations_seen[loc_str] = locations_seen.get(loc_str, 0) + 1 if ip not in timestamps_by_ip: timestamps_by_ip[ip] = [] timestamps_by_ip[ip].append(ts) # Check for issues is_failed = status_code != 0 is_risky = risk_level not in ("none", "low", None) is_legacy = app in ("IMAP4", "POP3", "SMTP", "Exchange ActiveSync", "Authenticated SMTP", "Other clients", "Exchange Online PowerShell", "MAPI Over HTTP", "Offline Address Book", "Outlook Anywhere (RPC over HTTP)", "Exchange Web Services", "POP", "IMAP") if is_failed: failed_count += 1 if is_risky: risky_count += 1 if is_legacy: legacy_auth.append({"timestamp": ts, "protocol": app, "ip": ip}) # Flag suspicious entries flags = [] if is_failed: flags.append("FAILED") if is_risky: flags.append(f"RISKY:{risk_level}") if is_legacy: flags.append("LEGACY_AUTH") if country not in ("US", "Unknown", "", None): flags.append(f"FOREIGN:{country}") flag_str = f" [{'|'.join(flags)}]" if flags else "" marker = "[SUSPICIOUS]" if flags else " " print(f" {marker} {ts} | IP: {ip} | {loc_str} | App: {app_name} | Client: {app} | Status: {status_code}{flag_str}") # Summary print(f"\n --- Sign-in Summary ---") print(f" Total sign-ins: {len(signins)}") print(f" Unique IPs: {len(ips_seen)}") print(f" Failed sign-ins: {failed_count}") print(f" Risky sign-ins: {risky_count}") print(f" Legacy auth protocols: {len(legacy_auth)}") print(f"\n --- IP Address Breakdown ---") for ip, count in sorted(ips_seen.items(), key=lambda x: x[1], reverse=True): print(f" {ip}: {count} sign-ins") print(f"\n --- Location Breakdown ---") for loc, count in sorted(locations_seen.items(), key=lambda x: x[1], reverse=True): print(f" {loc}: {count} sign-ins") if legacy_auth: print(f"\n [CRITICAL] Legacy Authentication Detected!") for la in legacy_auth: print(f" {la['timestamp']} | Protocol: {la['protocol']} | IP: {la['ip']}") # Impossible travel check if len(ips_seen) > 1: print(f"\n --- Impossible Travel Check ---") print(f" Multiple IPs detected. Review timestamps above for geographic anomalies.") results["signin_summary"] = { "total": len(signins), "unique_ips": len(ips_seen), "failed": failed_count, "risky": risky_count, "legacy_auth_count": len(legacy_auth), "ips": ips_seen, "locations": locations_seen, "legacy_auth_details": legacy_auth } else: print(" No sign-in logs found (tenant may not have Azure AD P1/P2)") # ===== STEP 3: RECENT SENT MAIL ===== print_separator("STEP 3: RECENT SENT MAIL (Last 14 Days)") mail_url = f"{GRAPH_BASE}/users/{jr_id}/mailFolders/SentItems/messages?$top=100&$orderby=sentDateTime desc&$select=subject,toRecipients,ccRecipients,sentDateTime,bodyPreview,from,sender,hasAttachments" mail_data = graph_get(token, mail_url, "sent mail") sent_msgs = mail_data.get("value", []) results["sent_mail"] = sent_msgs suspicious_subjects = ["invoice", "payment", "wire", "transfer", "docusign", "urgent", "overdue", "past due", "bank", "account", "verify", "confirm", "update", "action required", "immediately", "asap", "w-2", "w2", "tax", "direct deposit", "ach", "routing", "gift card"] if sent_msgs: sus_mail_count = 0 external_recipients = set() for msg in sent_msgs: subject = msg.get("subject", "(No Subject)") sent_dt = msg.get("sentDateTime", "N/A") has_attach = msg.get("hasAttachments", False) body_preview = msg.get("bodyPreview", "")[:150] to_list = msg.get("toRecipients", []) cc_list = msg.get("ccRecipients", []) # Check recipients to_addrs = [] for r in to_list: addr = r.get("emailAddress", {}).get("address", "N/A") to_addrs.append(addr) if TARGET_DOMAIN not in addr.lower(): external_recipients.add(addr) for r in cc_list: addr = r.get("emailAddress", {}).get("address", "N/A") if TARGET_DOMAIN not in addr.lower(): external_recipients.add(addr) # Check for suspicious subjects is_suspicious = any(kw in subject.lower() for kw in suspicious_subjects) if is_suspicious: sus_mail_count += 1 attach_str = " [HAS ATTACHMENTS]" if has_attach else "" marker = "[SUSPICIOUS]" if is_suspicious else " " print(f" {marker} {sent_dt} | To: {', '.join(to_addrs[:3])} | Subject: {subject}{attach_str}") if is_suspicious: print(f" Preview: {body_preview}") print(f"\n --- Sent Mail Summary ---") print(f" Total sent messages: {len(sent_msgs)}") print(f" Suspicious subjects: {sus_mail_count}") print(f" External recipients: {len(external_recipients)}") if external_recipients: print(f" External recipient list:") for addr in sorted(external_recipients): print(f" - {addr}") results["sent_mail_summary"] = { "total": len(sent_msgs), "suspicious_count": sus_mail_count, "external_recipients": list(external_recipients) } else: print(" No sent mail found or access denied") # ===== STEP 4: INBOX RULES ===== print_separator("STEP 4: INBOX RULES (CRITICAL CHECK)") rules_url = f"{GRAPH_BASE}/users/{jr_id}/mailFolders/inbox/messageRules" rules_data = graph_get(token, rules_url, "inbox rules") rules = rules_data.get("value", []) results["inbox_rules"] = rules if rules: for rule in rules: rule_name = rule.get("displayName", "Unnamed Rule") is_enabled = rule.get("isEnabled", False) actions = rule.get("actions", {}) conditions = rule.get("conditions", {}) # Check for malicious rule patterns flags = [] forward_to = actions.get("forwardTo", []) forward_as = actions.get("forwardAsAttachmentTo", []) redirect_to = actions.get("redirectTo", []) delete = actions.get("delete", False) move_to = actions.get("moveToFolder", "") mark_read = actions.get("markAsRead", False) perm_delete = actions.get("permanentDelete", False) if forward_to: fwd_addrs = [f.get("emailAddress", {}).get("address", "N/A") for f in forward_to] flags.append(f"FORWARDS TO: {', '.join(fwd_addrs)}") if forward_as: fwd_addrs = [f.get("emailAddress", {}).get("address", "N/A") for f in forward_as] flags.append(f"FORWARDS AS ATTACHMENT TO: {', '.join(fwd_addrs)}") if redirect_to: redir_addrs = [r.get("emailAddress", {}).get("address", "N/A") for r in redirect_to] flags.append(f"REDIRECTS TO: {', '.join(redir_addrs)}") if delete: flags.append("DELETES MESSAGES") if perm_delete: flags.append("PERMANENTLY DELETES MESSAGES") if mark_read: flags.append("MARKS AS READ") status = "[ENABLED]" if is_enabled else "[DISABLED]" marker = "[CRITICAL]" if (forward_to or redirect_to or delete or perm_delete) else " " print(f" {marker} {status} Rule: '{rule_name}'") if flags: for f in flags: print(f" >>> {f}") if conditions: print(f" Conditions: {json.dumps(conditions, indent=6)}") print(f" Full actions: {json.dumps(actions, indent=6)}") else: print(" [OK] No inbox rules found") # ===== STEP 5: MAILBOX SETTINGS ===== print_separator("STEP 5: MAILBOX SETTINGS (Forwarding & Auto-Reply)") mailbox_url = f"{GRAPH_BASE}/users/{jr_id}/mailboxSettings" mailbox_data = graph_get(token, mailbox_url, "mailbox settings") results["mailbox_settings"] = mailbox_data if "error" not in mailbox_data: auto_reply = mailbox_data.get("automaticRepliesSetting", {}) ar_status = auto_reply.get("status", "disabled") ar_external = auto_reply.get("externalReplyMessage", "") ar_internal = auto_reply.get("internalReplyMessage", "") print(f" Auto-Reply Status: {ar_status}") if ar_status != "disabled": print(f" [SUSPICIOUS] Auto-replies are ENABLED!") print(f" External message: {ar_external[:200]}") print(f" Internal message: {ar_internal[:200]}") else: print(f" [OK] Auto-replies are disabled") # Check other settings print(f" Language: {mailbox_data.get('language', {}).get('locale', 'N/A')}") print(f" Timezone: {mailbox_data.get('timeZone', 'N/A')}") print(f" Date format: {mailbox_data.get('dateFormat', 'N/A')}") else: print(" Could not retrieve mailbox settings") # Also check forwarding via Exchange settings print("\n Checking SMTP forwarding...") fwd_url = f"{GRAPH_BASE}/users/{jr_id}?$select=mail,otherMails,proxyAddresses" fwd_data = graph_get(token, fwd_url, "forwarding check") if "error" not in fwd_data: proxy = fwd_data.get("proxyAddresses", []) other = fwd_data.get("otherMails", []) print(f" Proxy addresses: {proxy}") print(f" Other emails: {other}") results["forwarding_check"] = fwd_data # ===== STEP 6: AUTHENTICATION METHODS ===== print_separator("STEP 6: AUTHENTICATION METHODS") auth_url = f"{GRAPH_BASE}/users/{jr_id}/authentication/methods" auth_data = graph_get(token, auth_url, "auth methods") auth_methods = auth_data.get("value", []) results["auth_methods"] = auth_methods if auth_methods: for m in auth_methods: method_type = m.get("@odata.type", "Unknown") method_id = m.get("id", "N/A") # Clean up type name clean_type = method_type.replace("#microsoft.graph.", "") detail = "" if "phone" in method_type.lower(): detail = f" | Phone: {m.get('phoneNumber', 'N/A')} ({m.get('phoneType', 'N/A')})" elif "email" in method_type.lower(): detail = f" | Email: {m.get('emailAddress', 'N/A')}" elif "fido2" in method_type.lower(): detail = f" | Model: {m.get('model', 'N/A')} | Created: {m.get('createdDateTime', 'N/A')}" elif "microsoftAuthenticator" in method_type: detail = f" | Device: {m.get('displayName', 'N/A')} | Created: {m.get('createdDateTime', 'N/A')}" elif "softwareOath" in method_type: detail = f" | Created: {m.get('createdDateTime', 'N/A')}" print(f" [{clean_type}] ID: {method_id}{detail}") else: print(" No auth methods returned (may need different permissions)") # ===== STEP 7: OAUTH PERMISSION GRANTS ===== print_separator("STEP 7: OAUTH PERMISSION GRANTS & THIRD-PARTY APPS") oauth_url = f"{GRAPH_BASE}/users/{jr_id}/oauth2PermissionGrants" oauth_data = graph_get(token, oauth_url, "oauth grants") oauth_grants = oauth_data.get("value", []) results["oauth_grants"] = oauth_grants if oauth_grants: print(f" Found {len(oauth_grants)} OAuth permission grant(s):") for grant in oauth_grants: client_id = grant.get("clientId", "N/A") scope = grant.get("scope", "N/A") consent = grant.get("consentType", "N/A") start = grant.get("startTime", "N/A") # Flag mail-related scopes mail_scopes = ["Mail", "IMAP", "POP", "SMTP", "EWS"] has_mail = any(ms.lower() in scope.lower() for ms in mail_scopes) marker = "[SUSPICIOUS]" if has_mail else " " print(f" {marker} ClientID: {client_id}") print(f" Scope: {scope}") print(f" Consent: {consent} | Start: {start}") else: print(" [OK] No OAuth permission grants found for user") # Check third-party service principals print("\n Checking third-party service principals...") sp_url = f"{GRAPH_BASE}/servicePrincipals?$filter=appOwnerOrganizationId ne {TENANT_ID}&$select=displayName,appId,appOwnerOrganizationId&$top=50" sp_data = graph_get(token, sp_url, "service principals") sps = sp_data.get("value", []) results["third_party_apps"] = sps if sps: print(f" Third-party apps in tenant ({len(sps)}):") # Known Microsoft org IDs ms_org_ids = [ "f8cdef31-a31e-4b4a-93e4-5f571e91255a", # Microsoft "47df5bb7-e6bc-4256-afb0-dd8c8e3c1ce8", # Microsoft "72f988bf-86f1-41af-91ab-2d7cd011db47", # Microsoft Corp ] for sp in sps: org_id = sp.get("appOwnerOrganizationId", "") is_ms = org_id in ms_org_ids if not is_ms: print(f" [INFO] {sp.get('displayName', 'N/A')} | AppID: {sp.get('appId', 'N/A')} | OrgID: {org_id}") else: print(" No third-party service principals found or filter not supported") # ===== STEP 8: AUDIT LOGS ===== print_separator("STEP 8: DIRECTORY AUDIT LOGS (Recent Changes)") # Try different filter approaches audit_url = f"{GRAPH_BASE}/auditLogs/directoryAudits?$filter=targetResources/any(t:t/userPrincipalName eq '{jr_upn}')&$top=50&$orderby=activityDateTime desc" audit_data = graph_get(token, audit_url, "audit logs") audit_entries = audit_data.get("value", []) if not audit_entries and "error" in audit_data: # Try beta print(" [*] Trying beta endpoint...") audit_url = f"{GRAPH_BETA}/auditLogs/directoryAudits?$filter=targetResources/any(t:t/userPrincipalName eq '{jr_upn}')&$top=50&$orderby=activityDateTime desc" audit_data = graph_get(token, audit_url, "audit logs beta") audit_entries = audit_data.get("value", []) if not audit_entries and "error" in audit_data: # Try without filter print(" [*] Trying unfiltered audit logs (will filter manually)...") audit_url = f"{GRAPH_BASE}/auditLogs/directoryAudits?$top=100&$orderby=activityDateTime desc" audit_data = graph_get(token, audit_url, "audit logs unfiltered") all_audits = audit_data.get("value", []) # Filter manually for a in all_audits: targets = a.get("targetResources", []) for t in targets: if jr_upn.lower() in t.get("userPrincipalName", "").lower() or jr_id in t.get("id", ""): audit_entries.append(a) break results["audit_logs"] = audit_entries if audit_entries: # Key activities to watch for critical_activities = [ "Reset password", "Change password", "Reset user password", "Update user", "Add member to role", "Add app role assignment", "Consent to application", "Add OAuth2PermissionGrant", "Update authentication method", "Register security info", "Add registered owner", "Add service principal", "Set force change password", "Disable account" ] for a in audit_entries: activity = a.get("activityDisplayName", "N/A") ts = a.get("activityDateTime", "N/A") result_status = a.get("result", "N/A") initiated_by = a.get("initiatedBy", {}) user_info = initiated_by.get("user") or {} app_info = initiated_by.get("app") or {} actor_upn = user_info.get("userPrincipalName", "N/A") actor_app = app_info.get("displayName", "") is_critical = any(ca.lower() in activity.lower() for ca in critical_activities) marker = "[CRITICAL]" if is_critical else " " actor = actor_upn if actor_upn != "N/A" else actor_app print(f" {marker} {ts} | {activity} | Result: {result_status} | Actor: {actor}") if is_critical: targets = a.get("targetResources", []) for t in targets: modified = t.get("modifiedProperties", []) for mp in modified: print(f" Changed: {mp.get('displayName', 'N/A')}: {str(mp.get('oldValue', ''))[:80]} -> {str(mp.get('newValue', ''))[:80]}") else: print(" No audit log entries found for target user") # ===== STEP 9: CHECK ALL OTHER USERS FOR RISKY SIGN-INS ===== print_separator("STEP 9: LATERAL MOVEMENT CHECK (All Users Risky Sign-ins)") other_users = [u for u in users if u.get("id") != jr_id and u.get("accountEnabled", False)] results["lateral_movement"] = {} for u in other_users: upn = u.get("userPrincipalName", "") name = u.get("displayName", "") if not upn: continue # Check for risky sign-ins risk_url = f"{GRAPH_BASE}/auditLogs/signIns?$filter=userPrincipalName eq '{upn}'&$top=10&$orderby=createdDateTime desc" risk_data = graph_get(token, risk_url, f"risk check {upn}") risk_signins = risk_data.get("value", []) user_risks = [] for s in risk_signins: risk_level = s.get("riskLevelDuringSignIn", "none") risk_state = s.get("riskState", "none") status_code = s.get("status", {}).get("errorCode", 0) ip = s.get("ipAddress", "N/A") loc = s.get("location", {}) country = loc.get("countryOrRegion", "") ts = s.get("createdDateTime", "") app = s.get("clientAppUsed", "") # Flag anything risky is_risky = risk_level not in ("none", "low", None, "") is_foreign = country not in ("US", "Unknown", "", None) is_legacy = app in ("IMAP4", "POP3", "SMTP", "Authenticated SMTP", "Other clients") is_failed = status_code != 0 if is_risky or is_foreign or is_legacy: user_risks.append({ "timestamp": ts, "ip": ip, "country": country, "risk_level": risk_level, "protocol": app, "failed": is_failed }) if user_risks: print(f"\n [SUSPICIOUS] {name} ({upn}):") for r in user_risks: print(f" {r['timestamp']} | IP: {r['ip']} | Country: {r['country']} | Risk: {r['risk_level']} | Protocol: {r['protocol']}") results["lateral_movement"][upn] = user_risks else: print(f" [OK] {name} ({upn}): No risky sign-ins detected") # ===== SAVE RESULTS ===== print_separator("SAVING RESULTS") with open(RESULTS_FILE, "w") as f: json.dump(results, f, indent=2, default=str) print(f" Results saved to: {RESULTS_FILE}") # ===== INCIDENT REPORT SUMMARY ===== print_separator("INCIDENT REPORT SUMMARY") print(f""" Target: {jr_upn} (ID: {jr_id}) Investigation Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')} Tenant: Valley Wide Plastering ({TENANT_ID}) Total Users in Tenant: {len(users)} KEY FINDINGS: ============= """) # Summarize findings findings = [] # Sign-in findings signin_summary = results.get("signin_summary", {}) if signin_summary: if signin_summary.get("failed", 0) > 10: findings.append(f"[CRITICAL] {signin_summary['failed']} failed sign-ins detected - possible brute force") if signin_summary.get("risky", 0) > 0: findings.append(f"[CRITICAL] {signin_summary['risky']} risky sign-ins flagged by Microsoft") if signin_summary.get("legacy_auth_count", 0) > 0: findings.append(f"[SUSPICIOUS] {signin_summary['legacy_auth_count']} legacy auth protocol usage detected") if signin_summary.get("unique_ips", 0) > 5: findings.append(f"[SUSPICIOUS] {signin_summary['unique_ips']} unique IP addresses used") # Mail findings mail_summary = results.get("sent_mail_summary", {}) if mail_summary: if mail_summary.get("suspicious_count", 0) > 0: findings.append(f"[SUSPICIOUS] {mail_summary['suspicious_count']} emails with suspicious subjects") if len(mail_summary.get("external_recipients", [])) > 10: findings.append(f"[SUSPICIOUS] {len(mail_summary['external_recipients'])} external recipients in sent mail") # Inbox rules if rules: for rule in rules: actions = rule.get("actions", {}) if actions.get("forwardTo") or actions.get("redirectTo") or actions.get("forwardAsAttachmentTo"): findings.append(f"[CRITICAL] Inbox rule '{rule.get('displayName', 'N/A')}' forwards/redirects mail externally") if actions.get("delete") or actions.get("permanentDelete"): findings.append(f"[CRITICAL] Inbox rule '{rule.get('displayName', 'N/A')}' deletes incoming mail") # Auto-reply if results.get("mailbox_settings", {}).get("automaticRepliesSetting", {}).get("status", "disabled") != "disabled": findings.append("[SUSPICIOUS] Auto-replies are enabled - check for phishing content") # OAuth if oauth_grants: findings.append(f"[INFO] {len(oauth_grants)} OAuth grants found - review for suspicious app access") # Lateral movement lateral = results.get("lateral_movement", {}) if lateral: findings.append(f"[SUSPICIOUS] {len(lateral)} other users show suspicious sign-in activity") if findings: for f in findings: print(f" {f}") else: print(" No critical findings detected. Review detailed output above for context.") print(f""" RECOMMENDED ACTIONS: ==================== 1. Reset JR Guerrero's password immediately 2. Revoke all active sessions (Entra ID > Users > Revoke sessions) 3. Enable MFA if not already enabled 4. Remove any suspicious inbox rules 5. Disable any unauthorized OAuth app grants 6. Block legacy authentication via Conditional Access 7. Review sent items for any phishing emails sent from this account 8. Notify recipients of any suspicious emails 9. Check for data exfiltration via OneDrive/SharePoint 10. Monitor account for next 30 days Investigation script: D:/ClaudeTools/temp/vwp_bec_investigation.py Raw results: {RESULTS_FILE} """) if __name__ == "__main__": main()