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:

  1. Checks for a running duplicate (prevents concurrent executions)
  2. Moves aftermath_collector.sh from /tmp (world-writable) to /var/root (root-only)
  3. Sets strict permissions: chmod 700, owned by root:wheel
  4. Creates a LaunchDaemon plist that points to the moved script
  5. 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:

  1. Dependency checks — installs AWS CLI v2 and Aftermath if not present, verifying both against SHA256 checksums before use
  2. 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
  3. Collection — runs Aftermath with --deep
  4. Compression — archives output as macOS-<hostname>-<timestamp>.zip
  5. Upload — ships to S3 with AWS CLI
  6. Notification — sends a Slack webhook on success or failure
  7. 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.sh to RTR Put Files
  • Upload aftermath_rtr_trigger.sh to 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

FileWhat’s in it
/tmp/aftermath_upload.logMain collection and upload log (chmod 600)
/tmp/aftermath_rtr_trigger.logTrigger script execution
/tmp/aftermath_launchd.outLaunchDaemon stdout
/tmp/aftermath_launchd.errLaunchDaemon 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.