Investigate business email compromise with audit logs and sign-ins
For developers and responders handling a suspected business email compromise, this walks through collecting mailbox audit logs, inbox/forwarding rules, and sign-in history without guessing through vendor-specific UI differences. You’ll end with a timestamped evidence set that shows whether the mailbox was accessed, what changed, and from where.
TL;DR — For a suspected business email compromise, pull three things first: mailbox audit events, inbox/forwarding rules, and sign-in history for the affected account over the same time window. The most common finding is a malicious inbox rule or forwarding setting added after a successful sign-in from an unusual IP; preserve evidence before you disable or delete anything. Reading time: ~5 min
Goal
When you finish, you will have a local evidence folder containing mailbox audit events, current inbox/forwarding rules, and sign-in history for the affected user over a defined time range, plus a short timeline you can use to decide whether the mailbox was accessed and what attacker persistence exists.
Prerequisites
- Admin access to your mail/identity provider with permission to view audit logs, mailbox settings, and sign-in history
- The affected mailbox address, for example
alice@example.com - A time window in UTC, for example
2026-10-24T00:00:00Zto2026-10-30T23:59:59Z - A workstation with
bash,curl,jq >= 1.6— check withjq --version python >= 3.11for quick CSV/JSON handling — check withpython3 --version- If your provider exposes an API: an API token or OAuth access token with audit/read scopes
- If your provider is UI-only for some data: browser access to the admin dashboard and the affected user object
- A case folder path, for example
/tmp/bec-alice-20261030
Steps
Step 1: Create an evidence workspace and lock the incident window
export CASE_DIR="/tmp/bec-alice-20261030"
export USER_EMAIL="alice@example.com"
export START_UTC="2026-10-24T00:00:00Z"
export END_UTC="2026-10-30T23:59:59Z"
mkdir -p "$CASE_DIR"/{raw,notes}
printf "user=%s\nstart=%s\nend=%s\ncollected_at=%s\n" \
"$USER_EMAIL" "$START_UTC" "$END_UTC" "$(date -u +%FT%TZ)" \
| tee "$CASE_DIR/notes/context.txt"
Success looks like a context.txt file with the mailbox and UTC window you will use everywhere else.
Step 2: Export sign-in history for the affected user
Use your provider’s admin dashboard path for sign-ins/history for the user object. Typical path: Admin dashboard → Users → alice@example.com → Sign-in logs and filter exactly to START_UTC through END_UTC, then export CSV or JSON to $CASE_DIR/raw/signins.csv or $CASE_DIR/raw/signins.json.
If your provider has an HTTP API, call it directly with your token and save the raw response:
export TOKEN="REDACTED"
curl -fsS \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
"https://api.your-provider.example/v1/signins?user=$(python3 - <<'PY'
import urllib.parse, os
print(urllib.parse.quote(os.environ['USER_EMAIL']))
PY
)&start=$START_UTC&end=$END_UTC" \
-o "$CASE_DIR/raw/signins.json"
jq '.[0] // .items[0]' "$CASE_DIR/raw/signins.json"
Success looks like records with timestamp, source IP, client/app, result, and location fields.
Step 3: Export mailbox audit events for the same window
Use your provider’s admin dashboard path for mailbox audit/search. Typical path: Admin dashboard → Compliance/Audit → Audit search, set User = alice@example.com, set the exact UTC range, and include mailbox operations such as sign-in/session, message access, send, rule changes, forwarding changes, and mailbox settings changes. Export to $CASE_DIR/raw/mailbox-audit.csv or .json.
If your provider has an API:
curl -fsS \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
"https://api.your-provider.example/v1/audit/mailbox?user=$(python3 - <<'PY'
import urllib.parse, os
print(urllib.parse.quote(os.environ['USER_EMAIL']))
PY
)&start=$START_UTC&end=$END_UTC" \
-o "$CASE_DIR/raw/mailbox-audit.json"
jq '.[0] // .items[0]' "$CASE_DIR/raw/mailbox-audit.json"
Success looks like events showing operation names, actor, client IP, and timestamps.
⚠️ Do not remove inbox rules or disable forwarding before you export them. Deleting attacker persistence first destroys evidence and makes timeline reconstruction harder.
Step 4: Capture current inbox rules and forwarding settings
Use your provider’s admin dashboard path for the mailbox object. Typical paths: Admin dashboard → Users → alice@example.com → Mail → Inbox rules and ... → Forwarding or ... → Mail flow. Export if the UI supports it; otherwise capture screenshots and transcribe exact values into a text file.
If your provider exposes mailbox settings via API:
curl -fsS \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
"https://api.your-provider.example/v1/mailboxes/$(python3 - <<'PY'
import urllib.parse, os
print(urllib.parse.quote(os.environ['USER_EMAIL']))
PY
)/rules" \
-o "$CASE_DIR/raw/inbox-rules.json"
curl -fsS \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
"https://api.your-provider.example/v1/mailboxes/$(python3 - <<'PY'
import urllib.parse, os
print(urllib.parse.quote(os.environ['USER_EMAIL']))
PY
)/forwarding" \
-o "$CASE_DIR/raw/forwarding.json"
jq '.' "$CASE_DIR/raw/inbox-rules.json" | sed -n '1,80p'
jq '.' "$CASE_DIR/raw/forwarding.json"
Success looks like explicit rule conditions/actions and any forwarding target addresses.
Step 5: Build a quick timeline and flag suspicious events
python3 - <<'PY' > "$CASE_DIR/notes/timeline.txt"
import json, os, glob
case=os.environ['CASE_DIR']
for path in sorted(glob.glob(case + '/raw/*')):
print(f'## {os.path.basename(path)}')
try:
data=json.load(open(path))
except Exception:
print('non-json export; inspect manually')
continue
items=data if isinstance(data,list) else data.get('items',[])
for x in items[:200]:
ts=x.get('timestamp') or x.get('createdAt') or x.get('time')
op=x.get('operation') or x.get('eventType') or x.get('action')
ip=x.get('ip') or x.get('ipAddress') or x.get('sourceIp')
result=x.get('result') or x.get('status')
print(f'{ts}\t{op}\t{ip}\t{result}')
PY
sed -n '1,120p' "$CASE_DIR/notes/timeline.txt"
Success looks like a merged, readable list where you can line up successful sign-ins with rule/forwarding changes or unusual message access.
Step 6: Correlate sign-ins with mailbox changes
Use these exact checks against the exported data:
jq -r '.[]? // .items[]? | [.timestamp // .createdAt // .time, .ip // .ipAddress // .sourceIp, .result // .status, .clientApp // .app // .userAgent] | @tsv' \
"$CASE_DIR/raw/signins.json" 2>/dev/null | sort > "$CASE_DIR/notes/signins.tsv"
jq -r '.[]? // .items[]? | [.timestamp // .createdAt // .time, .operation // .eventType // .action, .ip // .ipAddress // .sourceIp, .actor // .user // .mailbox] | @tsv' \
"$CASE_DIR/raw/mailbox-audit.json" 2>/dev/null | sort > "$CASE_DIR/notes/audit.tsv"
printf "=== sign-ins ===\n"; sed -n '1,40p' "$CASE_DIR/notes/signins.tsv"
printf "\n=== mailbox changes ===\n"; grep -Ei 'rule|forward|delegate|mailbox|login|access|send' "$CASE_DIR/notes/audit.tsv" | sed -n '1,80p'
Success looks like one or more successful sign-ins preceding rule, forward, delegate, or mailbox-setting changes from the same or nearby IPs.
Step 7: Preserve and then contain
After exports are complete, use your provider’s admin dashboard to contain the account: Admin dashboard → Users → alice@example.com → Sessions → Revoke/Sign out all sessions, then ... → Authentication → Reset password, then remove malicious rules/forwarding entries by exact name/value.
Record the exact actions in a note:
cat >> "$CASE_DIR/notes/actions.txt" <<'EOF'
2026-10-30T12:00:00Z revoked all sessions
2026-10-30T12:02:00Z reset password
2026-10-30T12:05:00Z removed rule "RSS" forwarding to attacker@example.net
2026-10-30T12:06:00Z disabled mailbox forwarding to attacker@example.net
EOF
Success looks like no active forwarding, no suspicious rules, and all sessions invalidated after evidence collection.
Verify it works
Run these checks after containment:
test -s "$CASE_DIR/raw/signins.json" -o -s "$CASE_DIR/raw/signins.csv" && echo "have sign-in export"
test -s "$CASE_DIR/raw/mailbox-audit.json" -o -s "$CASE_DIR/raw/mailbox-audit.csv" && echo "have audit export"
test -s "$CASE_DIR/raw/inbox-rules.json" -o -s "$CASE_DIR/raw/forwarding.json" && echo "have rules/forwarding export"
sed -n '1,40p' "$CASE_DIR/notes/actions.txt"
Expected result: all three evidence categories exist on disk, and your notes show post-preservation containment actions. In the provider UI, the mailbox should show zero suspicious inbox rules, forwarding disabled or set only to approved addresses, and no active sessions except new ones after the password reset.
Common pitfalls
Exporting different time zones
Mistake: sign-ins are exported in local time while audit logs are exported in UTC.
Symptom: events appear out of order and the sign-in seems to happen after the rule change.
Fix: rerun both exports with explicit UTC start/end values like 2026-10-24T00:00:00Z.
Deleting the malicious rule before exporting it
Mistake: removing the attacker’s inbox rule immediately.
Symptom: you can contain the incident, but you lose the exact target address, conditions, and creation timestamp.
Fix: export rules/forwarding first, then remove them.
Looking only at failed sign-ins
Mistake: filtering sign-in history to failures because the mailbox owner reported password prompts.
Symptom: you miss the one successful sign-in from a residential proxy or foreign ASN that actually matters.
Fix: include both success and failure results, then sort by timestamp and source IP.
Ignoring non-rule persistence
Mistake: checking inbox rules only.
Symptom: no suspicious rules found, but mail is still being exfiltrated.
Fix: also export mailbox forwarding, delegates/shared access, and mailbox settings changes from audit logs.
Overwriting raw exports during re-runs
Mistake: saving a second export to the same filename after containment.
Symptom: your “before” evidence is replaced with a cleaned-up “after” state.
Fix: keep the first export in $CASE_DIR/raw/ and save later pulls with suffixes like -post-containment.json.
Trusting UI summaries instead of raw fields
Mistake: relying on a dashboard card that says “unusual sign-in blocked” or “no risky events.”
Symptom: the summary hides the successful legacy/protocol sign-in or the exact rule action.
Fix: export raw CSV/JSON and inspect fields like result, clientApp, sourceIp, operation, and forwarding targets directly.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI