When you’re in the middle of an incident and need forensic artifacts from a macOS endpoint, time matters. You’ve got a compromised machine, a CrowdStrike RTR session open, and a 10-minute timeout window ticking down. Running a full forensic collection through RTR directly isn’t realistic — Aftermath can take 30 to 60 minutes depending on the system.
So I built a pipeline that solves that. You deploy two scripts, run one command, and disconnect. The collection happens in the background, artifacts land in S3, and you get a Slack notification when it’s done. The scripts clean themselves up on exit — success or failure.
The code is on GitHub: aftermath-auto-artifact-collection-to-s3
The Problem with RTR Timeouts
CrowdStrike RTR is excellent for remote response — you can run commands, upload files, and execute scripts on endpoints in real time. But it has session timeout limits. If you try to run a full forensic collection directly through RTR, the session closes before the job finishes.
The naive fix is to just run things in the background with & and hope for the best. That works sometimes, but it’s fragile — there’s no visibility into progress, no retry logic, no cleanup, and if the process fails mid-way you’ve got partial artifacts and potentially credentials left on disk.
The proper fix is to use macOS’s own persistence mechanism: LaunchDaemons.
Architecture: Two Scripts, One Job
The solution uses two scripts with distinct responsibilities:
CrowdStrike RTR Console
↓
aftermath_rtr_trigger.sh ← runs in < 5 seconds, RTR exits
↓
LaunchDaemon ← macOS takes over
↓
aftermath_collector.sh ← runs for 30–60 minutes
↓
S3 + Slack notification
aftermath_rtr_trigger.sh
This is the RTR entry point. It does almost nothing by design — its only job is to set things up and exit before RTR times out.
What it does in under 5 seconds:
- Checks for a running duplicate (prevents concurrent executions)
- Moves
aftermath_collector.shfrom/tmp(world-writable) to/var/root(root-only) - Sets strict permissions:
chmod 700, owned byroot:wheel - Creates a LaunchDaemon plist that points to the moved script
- Loads the daemon and exits
Moving the script from /tmp to /var/root immediately after upload closes a TOCTOU window — another process can’t swap the script between upload and execution. It’s a small thing but worth doing.
aftermath_collector.sh
This is where the actual work happens, running entirely under the LaunchDaemon with no RTR dependency.
The execution flow:
- Dependency checks — installs AWS CLI v2 and Aftermath if not present, verifying both against SHA256 checksums before use
- System prep — detects the logged-in GUI user, prevents sleep with
caffeinate -d -i -m -s -u, closes browsers and Slack to avoid database locks - Collection — runs Aftermath with
--deep - Compression — archives output as
macOS-<hostname>-<timestamp>.zip - Upload — ships to S3 with AWS CLI
- Notification — sends a Slack webhook on success or failure
- Cleanup — removes everything: AWS CLI (if installed), Aftermath binary (if installed), output directory, LaunchDaemon plist, and the script itself
The cleanup happens via an EXIT trap, so it runs regardless of how the script ends. Credentials never sit on disk after execution.
Why Close Browsers First
Aftermath collects SQLite databases — browser history, cookies, downloads. If a browser has those databases open when collection runs, SQLite returns a “database is locked” error and the artifacts come back incomplete or corrupt.
The script closes browsers gracefully via osascript, falls back to pkill -9 if that fails, then waits 3 seconds before collection starts. It handles Safari, Chrome, Brave, Firefox, Edge, Arc, Opera, Vivaldi, and Chromium. Slack is also closed because its Electron-based storage uses the same SQLite pattern.
Credential Handling
AWS credentials and the Slack webhook URL are base64-encoded in the script. This isn’t encryption — it’s obfuscation. The script acknowledges this in the comments.
The trade-off is intentional: the alternative (AWS IAM Roles Anywhere, certificate-based auth) requires pre-staged certificates on endpoints and user interaction to set up. That defeats the zero-touch requirement. For a script that self-deletes after every run and lives in /var/root with root-only permissions, base64 encoding is an acceptable risk.
Credentials are decoded at runtime into environment variables, used for the upload, then immediately unset.
What Aftermath Collects
Aftermath is an open-source macOS forensic tool maintained by JAMF. With --deep, it collects:
- Persistence: LaunchAgents, LaunchDaemons, login items, BTM database, cron jobs, emond events, system extensions
- Browsers: History, cookies, downloads, extensions (Safari, Chrome, Firefox, Edge, Brave, Arc, Chromium)
- Process & network: Process tree via TrueTree, active connections, network interfaces, WiFi preferences
- User data: Shell history (bash, zsh, fish, ksh), Slack data, crash reports
- System: Configuration profiles, TCC database, SIP/Gatekeeper/FileVault/Firewall state, XBS and LSQuarantine databases
- File system: File metadata, recent files, Spotlight queries, quarantine events
- Unified Logs: Failed sudo, login events, screensharing, SSH, TCC decisions, XProtect activity
Output runs 200 MB to 5 GB compressed depending on system size and uptime.
Deployment
Step 1: Configure the collector script
# Encode your AWS credentials
echo -n "AKIAIOSFODNN7EXAMPLE" | base64
echo -n "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" | base64
# Encode your Slack webhook
echo -n "https://hooks.slack.com/services/..." | base64
Paste the encoded values into aftermath_collector.sh along with your S3 bucket name and AWS region.
Step 2: Upload to CrowdStrike
- Upload
aftermath_collector.shto RTR Put Files - Upload
aftermath_rtr_trigger.shto RTR Custom Scripts
Step 3: Execute
put aftermath_collector.sh
runscript -CloudFile="aftermath_rtr_trigger.sh"
You can disconnect immediately. The LaunchDaemon keeps it running.
Step 4: Monitor (optional)
# Check daemon is running
shell sudo launchctl list | grep aftermath
# Watch progress
shell sudo tail -f /tmp/aftermath_upload.log
When it’s done, you’ll get a Slack message with the S3 path and the script is gone from the endpoint.
Log Files
| File | What’s in it |
|---|---|
/tmp/aftermath_upload.log | Main collection and upload log (chmod 600) |
/tmp/aftermath_rtr_trigger.log | Trigger script execution |
/tmp/aftermath_launchd.out | LaunchDaemon stdout |
/tmp/aftermath_launchd.err | LaunchDaemon stderr |
The full source is at github.com/th3machin3/aftermath-auto-artifact-collection-to-s3. README covers configuration in more detail including the IAM policy you need for the S3 upload.