diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b535386..27beb42 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,3 +50,14 @@ jobs: - uses: actions/checkout@v4 - name: Test run: ./tests/test-linux.sh + + macos: + strategy: + fail-fast: false + matrix: + os: [macos-14, macos-15] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Test + run: sudo ./tests/test-macos.sh diff --git a/README.md b/README.md index a109587..44cab28 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ WinLogs turns on the telemetry you want **before** you detonate malware, validat Built for disposable VMs: malware analysis sandboxes, DFIR and threat-hunting practice, Sigma rule testing, purple-team and blue-team labs. **Do not run it on production or work machines.** It turns on very verbose logging. +**Scope:** logging only. WinLogs configures the logs and the components that produce and keep them (audit policy, Sysmon, `auditd`, `eslogger`, retention). Other tooling, such as network capture, endpoint query tools or evidence collectors, belongs in a separate setup stage that can read the logs this one produces. + ## Quick start **Windows 10 / 11 / Server 2022 / 2025** (elevated PowerShell 5.1 or 7): @@ -22,18 +24,26 @@ sudo ./linux/install.sh --dry-run # preview sudo ./linux/install.sh --revert # undo ``` +**macOS 13+** (as root; nothing to download): +```bash +sudo ./macos/install.sh # asks for confirmation, then applies everything +sudo ./macos/install.sh --dry-run # preview +sudo ./macos/install.sh --revert # undo +``` +`eslogger` needs Full Disk Access, which macOS only lets a person grant by hand; the installer tells you exactly where. See [docs/logged-macos.md](docs/logged-macos.md). + ## What you get -| | Windows | Linux | -|---|---|---| -| Audit policy | Advanced audit policy via `auditpol` (logon, account and group management, process creation 4688 with command line, object access, policy change, DPAPI, SAM, ...) plus a Server profile (Kerberos, Directory Service) | `auditd` with the [Neo23x0 ruleset](https://github.com/Neo23x0/auditd): exec, file, network, privilege and persistence rules | -| Scripting visibility | PowerShell Script Block (4104), Module (4103) and Transcription logging for Windows PowerShell and PowerShell 7 | Interactive bash commands logged to syslog | -| Process / network / DNS / file telemetry | **Sysmon** with the [sysmon-modular](https://github.com/olafhartong/sysmon-modular) config (on by default) | `auditd` execve rules; **Sysmon for Linux** is opt-in with `--with-sysmon` | -| Retention | Event logs raised to 1 GB for Security, Sysmon and PowerShell; Task Scheduler, DNS-Client and more channels enabled | `auditd` keeps 10 x 200 MB with synchronous writes; `journald` persistent with rate limiting off | -| Object auditing | Per-object audit rules (SACLs) on persistence and tamper-prone registry keys and folders | n/a (covered by `auditd` watches) | -| Undo | `-Revert` | `--revert` | +| | Windows | Linux | macOS | +|---|---|---|---| +| Audit policy | Advanced audit policy via `auditpol` (logon, account and group management, process creation 4688 with command line, object access, policy change, DPAPI, SAM, ...) plus a Server profile (Kerberos, Directory Service) | `auditd` with the [Neo23x0 ruleset](https://github.com/Neo23x0/auditd): exec, file, network, privilege and persistence rules | Apple's `eslogger` (Endpoint Security) as a LaunchDaemon: exec, file create/delete/rename, persistence items, XProtect, logins, sudo/su, profile and TCC changes | +| Scripting visibility | PowerShell Script Block (4104), Module (4103) and Transcription logging for Windows PowerShell and PowerShell 7 | Interactive bash commands logged to syslog | Interactive zsh commands logged to the unified log | +| Process / network / DNS / file telemetry | **Sysmon** with the [sysmon-modular](https://github.com/olafhartong/sysmon-modular) config (on by default) | `auditd` execve rules; **Sysmon for Linux** is opt-in with `--with-sysmon` | `eslogger` JSON events (no Sysmon equivalent; no network connect events) | +| Retention | Event logs raised to 1 GB for Security, Sysmon and PowerShell; Task Scheduler, DNS-Client and more channels enabled | `auditd` keeps 10 x 200 MB with synchronous writes; `journald` persistent with rate limiting off | `/var/log/winlogs/eslogger.ndjson` rotated at 100 MB x 10; optional daily unified-log archives | +| Object auditing | Per-object audit rules (SACLs) on persistence and tamper-prone registry keys and folders | n/a (covered by `auditd` watches) | n/a (covered by `eslogger` file events) | +| Undo | `-Revert` | `--revert` | `--revert` | -Details and event IDs: [docs/what-gets-logged.md](docs/what-gets-logged.md). +Per-OS details, event IDs and limits: [Windows](docs/logged-windows.md) · [Linux](docs/logged-linux.md) · [macOS](docs/logged-macos.md) · [side-by-side overview](docs/what-gets-logged.md). ## Air-gapped labs @@ -47,12 +57,13 @@ Sysmon is Microsoft software that this repo does not redistribute, and lab machi sudo ./linux/prepare-offline.sh ./winlogs-offline # run on a machine matching the lab image sudo ./linux/install.sh --offline-dir ./winlogs-offline # verified against SHA256SUMS ``` +macOS needs no offline step: it only uses tools that ship with the OS. ## Tested -CI runs the full install, verify-events and revert cycle on Windows Server 2022 and 2025 (Windows PowerShell 5.1 and PowerShell 7), Windows 11 on ARM64, and Ubuntu 22.04 and 24.04. The Fedora job runs in a container, so it covers package install, config and revert but not live auditing. +CI runs the full install, verify-events and revert cycle on Windows Server 2022 and 2025 (Windows PowerShell 5.1 and PowerShell 7), Windows 11 on ARM64, Ubuntu 22.04 and 24.04, and macOS 14 and 15 (Apple silicon). The Fedora job runs in a container, so it covers package install, config and revert but not live auditing. -Not yet tested: Windows 10 and x64 Windows 11 client builds, a real domain controller, RHEL-family live auditing (Rocky/Alma), Debian, Kali, openSUSE, Arch, and Sysmon for Linux. Treat the first run as a test inside a snapshot-able VM. macOS is planned. +Not yet tested: Windows 10 and x64 Windows 11 client builds, a real domain controller, RHEL-family live auditing (Rocky/Alma), Debian, Kali, openSUSE, Arch, and Sysmon for Linux, and on macOS: Intel Macs, macOS 13, and a Mac where Full Disk Access has not been granted. Treat the first run as a test inside a snapshot-able VM. ## Related projects @@ -60,6 +71,7 @@ Maintained logging guidance and tools worth knowing, and what inspired this one: - [Yamato-Security/EnableWindowsLogSettings](https://github.com/Yamato-Security/EnableWindowsLogSettings): Windows log settings aimed at Sigma coverage - [olafhartong/sysmon-modular](https://github.com/olafhartong/sysmon-modular): modular Sysmon configs - [Neo23x0/auditd](https://github.com/Neo23x0/auditd): best-practice auditd rules +- [Cybereason: Blue teaming on macOS with eslogger](https://www.cybereason.com/blog/blue-teaming-on-macos-with-eslogger): how Apple's `eslogger` is used for detection and analysis - [Malware Archaeology logging cheat sheets](https://www.malwarearchaeology.com/cheat-sheets) and [Log-MD](https://www.log-md.com/): the original inspiration (the core Windows sheet dates from 2019) - [Microsoft Sysmon documentation](https://learn.microsoft.com/windows-server/administration/windows-commands/sysmon) and [advanced security auditing](https://learn.microsoft.com/windows/security/threat-protection/auditing/advanced-security-auditing) diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md index 84763aa..ffa6e67 100644 --- a/THIRD_PARTY.md +++ b/THIRD_PARTY.md @@ -8,4 +8,6 @@ Rule for this repo: only bundle what the license clearly allows, and keep each license text next to the file. Anything else is fetched by the user, with a signature or SHA-256 check. +The Linux ruleset is bundled as `linux/audit/neo23x0-audit.rules` with its Apache-2.0 license next to it. Sysmon for Linux is installed from Microsoft's package repo and never bundled. macOS bundles nothing: it uses `eslogger` and other tools that ship with the OS. + The WinLogs scripts and docs themselves are MIT licensed; see `LICENSE`. diff --git a/docs/logged-linux.md b/docs/logged-linux.md new file mode 100644 index 0000000..b720952 --- /dev/null +++ b/docs/logged-linux.md @@ -0,0 +1,42 @@ +# Linux: what gets logged + +See also: [Windows](logged-windows.md) · [macOS](logged-macos.md) · [overview](what-gets-logged.md) + +`linux/install.sh` (run with `sudo`) supports Debian/Ubuntu/Kali (`apt`), RHEL/Fedora/Rocky/Alma (`dnf`/`yum`), openSUSE (`zypper`) and Arch (`pacman`). CI exercises Ubuntu 22.04 and 24.04 on real VMs and Fedora in a container. + +## What it turns on + +| Piece | How | Where to look | +|---|---|---| +| Audit rules | `auditd` plus the [Neo23x0 ruleset](https://github.com/Neo23x0/auditd) (Apache-2.0), installed as `/etc/audit/rules.d/90-winlogs.rules`; `UID_MIN` is read from `/etc/login.defs` | `ausearch -k `, `/var/log/audit/audit.log` | +| Audit retention | `max_log_file = 200` (MB), `num_logs = 10`, rotate, `flush = SYNC` so events hit disk immediately | `/etc/audit/auditd.conf` | +| journald | Persistent storage (`/var/log/journal`), `SystemMaxUse` 2G by default (`--journal-size`), rate limiting off so bursts are not dropped | `journalctl`, `/etc/systemd/journald.conf.d/90-winlogs.conf` | +| Shell commands | Interactive bash commands logged to syslog, facility `local6`, tag `winlogs-cmd` (`--no-shell-logging` skips it) | `journalctl -t winlogs-cmd` or `/var/log/syslog` | +| Sysmon for Linux (opt-in) | `--with-sysmon` installs Microsoft's package and a small config (process create/terminate, network connect, file create/delete); experimental, not bundled | syslog (`/var/log/syslog` or the journal) | +| Auditing switched on | The last step runs `auditctl -e 1` (restarting journald on Ubuntu 22.04 turns kernel auditing back off) | `auditctl -s` shows `enabled 1` | + +## What the audit rules cover + +Rule keys in the Neo23x0 set, grouped. Use them with `ausearch -k `. + +- **Process execution:** `process_creation` (every `execve`/`execveat`; the highest-volume rule), `32bit_abi`, `process_vm`, `tracing`, `userfaultfd` +- **Persistence and startup:** `cron`, `systemd`, `init`, `udev`, `shell_profiles`, `systemwide_preloads`, `libpath`, `boot_writes`, `sysconfig`, `svc_defaults` +- **Accounts and authentication:** `etcpasswd`, `etcgroup`, `rootkey`, `sshd`, `pam`, `login`, `session` +- **Files and permissions:** `file_creation`, `file_modification`, `file_access`, `perm_mod`, `delete`, `specialfiles`, `bin_writes`, `usr_writes`, `unauthedfileaccess`, `T1070_006_timestomp` +- **Kernel and tampering:** `modules`, `modprobe`, `KEXEC`, `bpf`, `socket_bpf_filter_attached`, `io_uring`, `sysctl`, `mac_policy`, `namespaces`, `auditlog`, `auditconfig` +- **Network:** `network_socket_created`, `raw_network_socket_created`, `network_modifications`, `firewall`, `network` +- **Containers:** `docker`, `containers` +- **Package and software changes:** `software_mgmt` + +The ruleset is broad on purpose. If volume is a problem, comment out `process_creation` or the `file_*` rules in `/etc/audit/rules.d/90-winlogs.rules` and run `sudo augenrules --load`. + +## Good to know + +- **Kernel and container limits:** auditing needs a real kernel audit subsystem. In a container the install warns instead of failing, and no audit events are produced. +- **Shell logging** covers interactive bash only and is easy for an attacker to bypass; treat it as a convenience, not evidence. +- **Reboots:** the rules persist through `rules.d`, and journald and auditd start at boot. +- **Offline:** `linux/prepare-offline.sh` downloads the packages with a `SHA256SUMS` file; `install.sh --offline-dir DIR` verifies it first. Run it on a machine that matches the lab image. +- **Revert:** `sudo ./linux/install.sh --revert` restores edited files from backups and removes the ones it created. Installed packages are left in place. + +## Not yet verified +Rocky/Alma (RHEL-family live auditing), Debian, Kali, openSUSE, Arch, and `--with-sysmon`. diff --git a/docs/logged-macos.md b/docs/logged-macos.md new file mode 100644 index 0000000..92bb025 --- /dev/null +++ b/docs/logged-macos.md @@ -0,0 +1,37 @@ +# macOS: what gets logged + +See also: [Windows](logged-windows.md) · [Linux](logged-linux.md) · [overview](what-gets-logged.md) + +`macos/install.sh` (macOS 13 Ventura or newer, run with `sudo`) uses only tools that ship with macOS, so there is nothing to download and no offline bundle to prepare. + +## What it turns on + +| Piece | How | Where to look | +|---|---|---| +| Endpoint Security events | Apple's `eslogger` runs as a LaunchDaemon (`com.winlogs.eslogger`) writing newline-delimited JSON | `/var/log/winlogs/eslogger.ndjson` (rotated at 100 MB, 10 kept, gzip, by `newsyslog`) | +| zsh command history | A marker block in `/etc/zshrc` logs each interactive command through `logger` | `log show --predicate 'eventMessage contains "winlogs-cmd"'` | +| Application firewall logging | `socketfilterfw --setloggingmode on` (previous mode restored on revert) | unified log / `/var/log/appfirewall.log` on older releases | +| Unified log snapshots (opt-in, `--with-log-archive`) | Daily `log collect --last 1d`, pruned after `--keep-days` (default 7) | `/var/log/winlogs/unified-YYYY-MM-DD.logarchive` | + +Event types recorded (those this macOS supports; the list is checked against `eslogger --list-events` at install time): process exec, file create/unlink/rename, mount, kext load, background-item (persistence) add/remove, XProtect malware detected/remediated, logins (local, ssh, screen sharing), sudo/su, profile install/remove, Gatekeeper overrides, TCC changes, remote thread creation, code-signature invalidation, and local account/group changes. + +## One manual step: Full Disk Access + +`eslogger` is an Endpoint Security client, and macOS only lets a person (or an MDM) grant it Full Disk Access: + +System Settings > Privacy & Security > Full Disk Access > `+` > press Cmd+Shift+G > `/usr/bin/eslogger` + +Then restart it: `sudo launchctl kickstart -k system/com.winlogs.eslogger`. +The installer checks the daemon's error output and prints this reminder when access is missing. CI runners come with permissions already granted, so a green CI run does not prove a fresh Mac is configured. + +## Limits worth knowing + +- **No Sysmon equivalent and no network connection events.** Endpoint Security has no TCP/UDP connect event, so this project cannot log connections on macOS. Collecting network telemetry is out of scope here; if you need it, handle it in a separate setup stage. +- **OpenBSM (`audit`) is deliberately not used.** It is deprecated; `eslogger` is Apple's replacement. +- **Unified log redaction.** Many messages show `` values. Turning private data on needs a configuration profile or reduced SIP, which this script does not do. +- **Log rotation gap.** After `newsyslog` rotates the file, `eslogger` is restarted by launchd, so a few seconds of events can be missed at rotation time. +- **Shell logging covers zsh only** (the default shell) and is easy for an attacker to bypass. +- `/etc/zshrc` is edited between marker comments; `--revert` restores the original file exactly. + +## Not yet verified +Intel Macs, macOS 13, and behaviour on a Mac where Full Disk Access has not been granted. CI covers macOS 14 and 15 on Apple silicon. diff --git a/docs/logged-windows.md b/docs/logged-windows.md new file mode 100644 index 0000000..3dc6210 --- /dev/null +++ b/docs/logged-windows.md @@ -0,0 +1,41 @@ +# Windows: what gets logged + +See also: [Linux](logged-linux.md) · [macOS](logged-macos.md) · [overview](what-gets-logged.md) + +| Area | Setting | Where to look | +|---|---|---| +| Audit policy | `windows/audit.csv` via `auditpol /restore` (logon, account/group mgmt, process creation/termination, object access, policy change, sensitive privilege use, ...) | Security log | +| Command lines | `ProcessCreationIncludeCmdLine_Enabled=1` | Security 4688 | +| Legacy override | `SCENoApplyLegacyAuditPolicy=1` so advanced audit policy wins | n/a | +| PowerShell | Script Block (incl. invocation start/stop), Module (`*`), Transcription to `C:\ProgramData\WinLogs\transcripts`; set for Windows PowerShell and PowerShell 7 | Microsoft-Windows-PowerShell/Operational 4103/4104, transcript files | +| Sysmon | sysmon-modular balanced config (process, network, DNS, image load, registry, WMI, pipes, ...) | Microsoft-Windows-Sysmon/Operational | +| Channels enabled / enlarged | Task Scheduler, DNS-Client, DriverFrameworks-UserMode (usually off); Defender, WMI-Activity, BITS-Client, Firewall, TerminalServices, CodeIntegrity, NTLM, Security-Mitigations, PrintService, SmbClient Security, AppLocker (4 logs), Diagnosis-Scripted, PowerShellCore, System, Application | each channel | +| Sizes | Security, Sysmon, PowerShell: `-LogSizeMB` (default 1024). Others: up to 256 MB. Logs are never shrunk. | n/a | + +## Client vs Server + +`Install-WinLogs.ps1` picks an audit profile automatically (`-AuditProfile Auto|Workstation|Server`): + +| | Windows 10 / 11 | Windows Server (incl. domain controllers) | +|---|---|---| +| Base policy (`audit.csv`) | yes | yes | +| Kerberos Authentication Service and Service Ticket Operations (4768/4769/4771...), Directory Service Access, Directory Service Replication (`audit-server.csv`) | no (these only generate events on a DC) | yes | +| Extra channels | n/a | Directory Service, DNS Server, DNS Server Audit, DFS Replication (only if the role is installed) | + +## Audit policy changes from the original 2016 baseline +Added: PNP Activity (USB / device insertion, 6416), Group Membership (4627), and Other Object Access Events set to Success and Failure (scheduled task creation 4698/4702). +Turned on: SAM (4661 handle requests to the SAM), Other Policy Change Events, MPSSVC Rule-Level Policy Change (firewall rule changes, 4946-4948), DPAPI Activity (4692/4693). +Still off on purpose because they are very noisy: Handle Manipulation, Non Sensitive Privilege Use, Other Privilege Use, Filtering Platform Packet Drop, Detailed Directory Service Replication. + +## Per-object auditing (SACLs) +The File System and Registry audit categories only log access to objects that carry an audit rule. `windows/sacls.psd1` lists what gets one (success audit for Everyone) and is meant to be edited: +- Registry: Run / RunOnce (native and WOW6432Node), Policies\Explorer\Run, Active Setup, Winlogon, Windows (AppInit_DLLs), Services, Session Manager, Image File Execution Options, SilentProcessExit, Lsa, SecurityProviders (WDigest), and the Defender policy key. Events: 4657 (value change), 4656/4663. +- Files: the all-users StartUp folder, `System32\Tasks`, `System32\drivers`, and the hosts file. Events: 4663 (and 4656). +- Per-user hives (HKCU Run keys) are not covered, since each user's hive is only loaded while that user is signed in. +- Tamper-protected keys (for example Defender) may refuse the rule; that is reported as a warning and the run continues. +- `-SkipObjectAuditing` leaves these out; `-Revert` removes exactly the rules it added. + +Notes +- Filtering Platform Connection (5156/5158) is very noisy and overlaps Sysmon event 3. Remove that row from `audit.csv` if logs roll too quickly. +- Transcripts are plain files that malware running as admin can delete. Fine for a lab, not evidence-grade. +- Everything is recorded in `C:\ProgramData\WinLogs\state.json`; `-Revert` restores it. diff --git a/docs/what-gets-logged.md b/docs/what-gets-logged.md index f31b6b1..356c171 100644 --- a/docs/what-gets-logged.md +++ b/docs/what-gets-logged.md @@ -1,39 +1,18 @@ -# What the Windows script turns on +# What gets logged -| Area | Setting | Where to look | -|---|---|---| -| Audit policy | `windows/audit.csv` via `auditpol /restore` (logon, account/group mgmt, process creation/termination, object access, policy change, sensitive privilege use, ...) | Security log | -| Command lines | `ProcessCreationIncludeCmdLine_Enabled=1` | Security 4688 | -| Legacy override | `SCENoApplyLegacyAuditPolicy=1` so advanced audit policy wins | n/a | -| PowerShell | Script Block (incl. invocation start/stop), Module (`*`), Transcription to `C:\ProgramData\WinLogs\transcripts`; set for Windows PowerShell and PowerShell 7 | Microsoft-Windows-PowerShell/Operational 4103/4104, transcript files | -| Sysmon | sysmon-modular balanced config (process, network, DNS, image load, registry, WMI, pipes, ...) | Microsoft-Windows-Sysmon/Operational | -| Channels enabled / enlarged | Task Scheduler, DNS-Client, DriverFrameworks-UserMode (usually off); Defender, WMI-Activity, BITS-Client, Firewall, TerminalServices, CodeIntegrity, NTLM, Security-Mitigations, PrintService, SmbClient Security, AppLocker (4 logs), Diagnosis-Scripted, PowerShellCore, System, Application | each channel | -| Sizes | Security, Sysmon, PowerShell: `-LogSizeMB` (default 1024). Others: up to 256 MB. Logs are never shrunk. | n/a | +Each OS has its own page with the exact settings, where to find the events, and the limits. -## Client vs Server - -`Install-WinLogs.ps1` picks an audit profile automatically (`-AuditProfile Auto|Workstation|Server`): - -| | Windows 10 / 11 | Windows Server (incl. domain controllers) | -|---|---|---| -| Base policy (`audit.csv`) | yes | yes | -| Kerberos Authentication Service and Service Ticket Operations (4768/4769/4771...), Directory Service Access, Directory Service Replication (`audit-server.csv`) | no (these only generate events on a DC) | yes | -| Extra channels | n/a | Directory Service, DNS Server, DNS Server Audit, DFS Replication (only if the role is installed) | - -## Audit policy changes from the original 2016 baseline -Added: PNP Activity (USB / device insertion, 6416), Group Membership (4627), and Other Object Access Events set to Success and Failure (scheduled task creation 4698/4702). -Turned on: SAM (4661 handle requests to the SAM), Other Policy Change Events, MPSSVC Rule-Level Policy Change (firewall rule changes, 4946-4948), DPAPI Activity (4692/4693). -Still off on purpose because they are very noisy: Handle Manipulation, Non Sensitive Privilege Use, Other Privilege Use, Filtering Platform Packet Drop, Detailed Directory Service Replication. - -## Per-object auditing (SACLs) -The File System and Registry audit categories only log access to objects that carry an audit rule. `windows/sacls.psd1` lists what gets one (success audit for Everyone) and is meant to be edited: -- Registry: Run / RunOnce (native and WOW6432Node), Policies\Explorer\Run, Active Setup, Winlogon, Windows (AppInit_DLLs), Services, Session Manager, Image File Execution Options, SilentProcessExit, Lsa, SecurityProviders (WDigest), and the Defender policy key. Events: 4657 (value change), 4656/4663. -- Files: the all-users StartUp folder, `System32\Tasks`, `System32\drivers`, and the hosts file. Events: 4663 (and 4656). -- Per-user hives (HKCU Run keys) are not covered, since each user's hive is only loaded while that user is signed in. -- Tamper-protected keys (for example Defender) may refuse the rule; that is reported as a warning and the run continues. -- `-SkipObjectAuditing` leaves these out; `-Revert` removes exactly the rules it added. - -Notes -- Filtering Platform Connection (5156/5158) is very noisy and overlaps Sysmon event 3. Remove that row from `audit.csv` if logs roll too quickly. -- Transcripts are plain files that malware running as admin can delete. Fine for a lab, not evidence-grade. -- Everything is recorded in `C:\ProgramData\WinLogs\state.json`; `-Revert` restores it. +| | [Windows](logged-windows.md) | [Linux](logged-linux.md) | [macOS](logged-macos.md) | +|---|---|---|---| +| Script | `windows/Install-WinLogs.ps1` | `linux/install.sh` | `macos/install.sh` | +| Process creation | Security 4688 with command line, Sysmon 1 | `auditd` `process_creation`, optional Sysmon for Linux | `eslogger` `exec` | +| Authentication and accounts | Audit policy (logon, account/group management, Kerberos on Server) | `auditd` (`pam`, `login`, `etcpasswd`, ...) | `eslogger` login, sudo, su, ssh, account changes | +| Scripting visibility | PowerShell Script Block, Module and Transcription | Interactive bash to syslog | Interactive zsh to the unified log | +| File / registry changes | Per-object audit rules (SACLs) on persistence keys and folders | `auditd` file watches and syscall rules | `eslogger` create/unlink/rename | +| Network connections | Sysmon 3, Filtering Platform 5156 | `auditd` socket rules, Sysmon for Linux | not available from Endpoint Security | +| Persistence | Run keys, services, scheduled tasks, StartUp | `cron`, `systemd`, profile and preload watches | Background Task Management items | +| Malware detections | Defender operational log | n/a | XProtect events via `eslogger` | +| Where it is stored | Event Log channels (sized up to 1 GB) | `/var/log/audit`, persistent journal | `/var/log/winlogs/eslogger.ndjson`, unified log | +| Undo | `-Revert` | `--revert` | `--revert` | +| Offline install | `Prepare-Offline.ps1` | `prepare-offline.sh` | nothing to download | +| CI | Server 2022/2025, Windows 11 ARM64 | Ubuntu 22.04/24.04, Fedora (container) | macOS 14/15 | diff --git a/macos/install.sh b/macos/install.sh new file mode 100755 index 0000000..76048bf --- /dev/null +++ b/macos/install.sh @@ -0,0 +1,282 @@ +#!/usr/bin/env bash +# WinLogs for macOS: forensic-grade logging for disposable malware / detection labs. +# +# sudo ./install.sh apply (asks for confirmation) +# sudo ./install.sh --dry-run show what would change +# sudo ./install.sh --revert undo +# +# Turns on: Apple's eslogger (Endpoint Security events as JSON, run as a LaunchDaemon with rotation), +# zsh command logging to the unified log, application firewall logging, and (opt-in) daily unified-log +# archives. macOS 13 (Ventura) or newer. Everything used ships with macOS: nothing to download. +# +# MANUAL STEP: eslogger needs Full Disk Access. macOS only allows that by hand (or MDM): +# System Settings > Privacy & Security > Full Disk Access > "+" > press Cmd+Shift+G > /usr/bin/eslogger +# This script detects when it is missing and says so. +# +# Written for the system bash (3.2): no associative arrays, no mapfile, no ${var,,}. +set -euo pipefail + +STATE_DIR=/var/db/winlogs +MANIFEST="$STATE_DIR/manifest" # lines: "created " | "backup " | "fw " | "label " +BACKUP_DIR="$STATE_DIR/backup" +LOG_DIR=/var/log/winlogs +LIBEXEC=/usr/local/libexec/winlogs +ES_LABEL=com.winlogs.eslogger +ES_PLIST=/Library/LaunchDaemons/$ES_LABEL.plist +ES_WRAPPER=$LIBEXEC/eslogger-run.sh +ES_LOG=$LOG_DIR/eslogger.ndjson +ES_PID=/var/run/winlogs-eslogger.pid +NEWSYSLOG=/etc/newsyslog.d/winlogs.conf +COLLECT_LABEL=com.winlogs.logcollect +COLLECT_PLIST=/Library/LaunchDaemons/$COLLECT_LABEL.plist +COLLECT_SCRIPT=$LIBEXEC/logcollect.sh +ZSHRC=/etc/zshrc +FWCTL=/usr/libexec/ApplicationFirewall/socketfilterfw +MARK_BEGIN="# >>> winlogs >>>" +MARK_END="# <<< winlogs <<<" + +YES=0 DRY=0 REVERT=0 SHELLLOG=1 ARCHIVE=0 KEEP_DAYS=7 ES_MAX_MB=100 ES_KEEP=10 + +# Endpoint Security events worth keeping. Anything this macOS does not support is dropped at install time. +WANTED_EVENTS="exec create unlink rename mount kextload btm_launch_item_add btm_launch_item_remove \ +xp_malware_detected xp_malware_remediated login_login login_logout lw_session_login lw_session_logout \ +sudo su openssh_login openssh_logout screensharing_attach screensharing_detach profile_add profile_remove \ +authentication gatekeeper_user_override tcc_modify remote_thread_create cs_invalidated \ +od_create_user od_delete_user od_modify_password od_group_add od_group_remove od_group_set" + +usage() { sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; echo "Options: --yes --dry-run --revert --no-shell-logging --with-log-archive --keep-days N --es-max-mb N"; } + +while [ $# -gt 0 ]; do + case "$1" in + --yes|-y) YES=1 ;; + --dry-run) DRY=1 ;; + --revert) REVERT=1 ;; + --no-shell-logging) SHELLLOG=0 ;; + --with-log-archive) ARCHIVE=1 ;; + --keep-days) KEEP_DAYS="${2:?--keep-days needs a number}"; shift ;; + --es-max-mb) ES_MAX_MB="${2:?--es-max-mb needs a number}"; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +say() { printf '[*] %s\n' "$*"; } +warn() { printf '[!] %s\n' "$*" >&2; } +die() { printf '[x] %s\n' "$*" >&2; exit 1; } +run() { if [ "$DRY" = 1 ]; then printf ' (dry-run) %s\n' "$*"; else "$@"; fi; } + +[ "$(uname -s)" = Darwin ] || die "This script is for macOS." +[ "$(id -u)" = 0 ] || die "Run as root (sudo)." + +OS_MAJOR="$(sw_vers -productVersion | cut -d. -f1)" +[ "$OS_MAJOR" -ge 13 ] || die "macOS 13 (Ventura) or newer is required (this is $(sw_vers -productVersion))." + +confirm() { + if [ "$YES" = 1 ] || [ "$DRY" = 1 ]; then return 0; fi + warn "This turns on very verbose logging. Use it on disposable lab machines only." + read -r -p "Type YES to continue: " a + [ "$a" = YES ] || die "Aborted." +} + +record() { if [ "$DRY" != 1 ]; then mkdir -p "$STATE_DIR"; echo "$*" >> "$MANIFEST"; fi; } +already_recorded() { [ -f "$MANIFEST" ] && grep -qxF "$*" "$MANIFEST"; } + +# Back up a file the first time we touch it; remember files we create. +claim() { + local f="$1" + if [ "$DRY" = 1 ]; then return 0; fi + mkdir -p "$BACKUP_DIR" + if already_recorded "created $f"; then return 0; fi + if [ -e "$f" ]; then + if ! already_recorded "backup $f"; then cp -p "$f" "$BACKUP_DIR/$(echo "$f" | tr / _)"; record "backup $f"; fi + else + record "created $f" + fi +} + +supported_events() { + # eslogger --list-events prints one event per line; keep only events we want and this macOS knows. + local avail ev out="" + avail="$(/usr/bin/eslogger --list-events 2>/dev/null || true)" + if [ -z "$avail" ]; then echo "exec create unlink rename"; return; fi + for ev in $WANTED_EVENTS; do + if echo "$avail" | grep -qx "$ev"; then out="$out $ev"; fi + done + echo "${out# }" +} + +install_eslogger() { + [ -x /usr/bin/eslogger ] || { warn "/usr/bin/eslogger not found; skipping Endpoint Security logging."; return; } + local events; events="$(supported_events)" + say "Installing eslogger LaunchDaemon ($(echo "$events" | wc -w | tr -d ' ') event types)" + if [ "$DRY" = 1 ]; then echo " (dry-run) write $ES_WRAPPER, $ES_PLIST, $NEWSYSLOG; launchctl bootstrap"; return; fi + + mkdir -p "$LIBEXEC" "$LOG_DIR"; chmod 700 "$LOG_DIR" + claim "$ES_WRAPPER" + cat > "$ES_WRAPPER" < $ES_PID +exec /usr/bin/eslogger $events +EOF + chmod 755 "$ES_WRAPPER" + + claim "$ES_PLIST" + cat > "$ES_PLIST" < + + + + Label$ES_LABEL + ProgramArguments$ES_WRAPPER + RunAtLoad + KeepAlive + ThrottleInterval5 + StandardOutPath$ES_LOG + StandardErrorPath$LOG_DIR/eslogger.err + + +EOF + chown root:wheel "$ES_PLIST"; chmod 644 "$ES_PLIST" + + mkdir -p /etc/newsyslog.d + claim "$NEWSYSLOG" + # logfile mode count size(KB) when flags(Z=gzip B=binary) pid_file signal(1=HUP) + printf '%s\t640\t%s\t%s\t*\tZB\t%s\t1\n' "$ES_LOG" "$ES_KEEP" "$((ES_MAX_MB * 1024))" "$ES_PID" > "$NEWSYSLOG" + + launchctl bootout "system/$ES_LABEL" >/dev/null 2>&1 || true + launchctl bootstrap system "$ES_PLIST" || warn "launchctl bootstrap failed for $ES_LABEL." + record "label $ES_LABEL" +} + +install_shell_logging() { + [ "$SHELLLOG" = 1 ] || return 0 + say "Logging interactive zsh commands to the unified log" + if [ "$DRY" = 1 ]; then echo " (dry-run) append marker block to $ZSHRC"; return; fi + claim "$ZSHRC" + if grep -qF "$MARK_BEGIN" "$ZSHRC" 2>/dev/null; then return; fi + cat >> "$ZSHRC" </dev/null && add-zsh-hook precmd __winlogs_cmd +$MARK_END +EOF +} + +install_firewall_logging() { + [ -x "$FWCTL" ] || return 0 + local cur; cur="$("$FWCTL" --getloggingmode 2>/dev/null | grep -io 'on\|off' | head -1 | tr '[:upper:]' '[:lower:]' || true)" + say "Turning on application firewall logging (was: ${cur:-unknown})" + if [ "$DRY" = 1 ]; then echo " (dry-run) $FWCTL --setloggingmode on"; return; fi + if [ -n "$cur" ] && ! already_recorded "fw on" && ! already_recorded "fw off"; then record "fw $cur"; fi + "$FWCTL" --setloggingmode on >/dev/null 2>&1 || warn "Could not change firewall logging mode." +} + +install_log_archive() { + [ "$ARCHIVE" = 1 ] || return 0 + say "Scheduling a daily unified-log archive (keeping $KEEP_DAYS days)" + if [ "$DRY" = 1 ]; then echo " (dry-run) write $COLLECT_SCRIPT, $COLLECT_PLIST"; return; fi + mkdir -p "$LIBEXEC" "$LOG_DIR" + claim "$COLLECT_SCRIPT" + cat > "$COLLECT_SCRIPT" </dev/null 2>&1 +find "$LOG_DIR" -maxdepth 1 -name 'unified-*.logarchive' -mtime +$KEEP_DAYS -exec rm -rf {} + +EOF + chmod 755 "$COLLECT_SCRIPT" + claim "$COLLECT_PLIST" + cat > "$COLLECT_PLIST" < + + + + Label$COLLECT_LABEL + ProgramArguments$COLLECT_SCRIPT + StartCalendarIntervalHour0Minute5 + + +EOF + chown root:wheel "$COLLECT_PLIST"; chmod 644 "$COLLECT_PLIST" + launchctl bootout "system/$COLLECT_LABEL" >/dev/null 2>&1 || true + launchctl bootstrap system "$COLLECT_PLIST" || warn "launchctl bootstrap failed for $COLLECT_LABEL." + record "label $COLLECT_LABEL" +} + +check_eslogger() { + [ "$DRY" = 1 ] && return 0 + [ -f "$ES_PLIST" ] || return 0 + sleep 4 + local err=""; [ -f "$LOG_DIR/eslogger.err" ] && err="$(tail -c 600 "$LOG_DIR/eslogger.err")" + if echo "$err" | grep -qi -E 'not permitted|NOT_PERMITTED|full disk|privilege'; then + warn "eslogger is blocked: grant Full Disk Access to /usr/bin/eslogger, then restart it with:" + warn " sudo launchctl kickstart -k system/$ES_LABEL" + warn " (System Settings > Privacy & Security > Full Disk Access > + > Cmd+Shift+G > /usr/bin/eslogger)" + elif launchctl print "system/$ES_LABEL" >/dev/null 2>&1; then + say "eslogger is running; output: $ES_LOG" + else + warn "eslogger is not loaded; check: sudo launchctl print system/$ES_LABEL" + fi +} + +do_revert() { + [ -f "$MANIFEST" ] || die "No manifest at $MANIFEST; nothing to revert." + confirm + say "Reverting" + while read -r kind path; do + case "$kind" in + label) run launchctl bootout "system/$path" >/dev/null 2>&1 || true ;; + esac + done < "$MANIFEST" + while read -r kind path; do + case "$kind" in + created) + if [ "$path" = "$ZSHRC" ]; then continue; fi + run rm -f "$path" ;; + backup) + if [ "$path" = "$ZSHRC" ]; then continue; fi + run cp -p "$BACKUP_DIR/$(echo "$path" | tr / _)" "$path" ;; + fw) + if [ -x "$FWCTL" ]; then run "$FWCTL" --setloggingmode "$path" >/dev/null 2>&1 || true; fi ;; + esac + done < "$MANIFEST" + # /etc/zshrc: strip our marker block (keeps anything else that changed since); delete it if we created it. + if [ -f "$ZSHRC" ] && grep -qF "$MARK_BEGIN" "$ZSHRC"; then + if [ "$DRY" != 1 ]; then + awk -v b="$MARK_BEGIN" -v e="$MARK_END" '$0==b{skip=1} !skip{print} $0==e{skip=0}' "$ZSHRC" > "$ZSHRC.winlogs.tmp" + if already_recorded "backup $ZSHRC"; then + # Restore the exact original (also drops the blank line we added before the block). + cp -p "$BACKUP_DIR/$(echo "$ZSHRC" | tr / _)" "$ZSHRC"; rm -f "$ZSHRC.winlogs.tmp" + else + cat "$ZSHRC.winlogs.tmp" > "$ZSHRC"; rm -f "$ZSHRC.winlogs.tmp" + fi + fi + fi + if already_recorded "created $ZSHRC" && [ "$DRY" != 1 ] && [ ! -s "$ZSHRC" ]; then rm -f "$ZSHRC"; fi + run rmdir "$LIBEXEC" >/dev/null 2>&1 || true + run rm -f "$ES_PID" + if [ "$DRY" != 1 ]; then rm -rf "$STATE_DIR"; fi + say "Reverted. Collected logs were left in $LOG_DIR (delete them when you are done)." +} + +if [ "$REVERT" = 1 ]; then do_revert; exit 0; fi + +say "WinLogs on macOS $(sw_vers -productVersion) ($(uname -m))" +confirm +install_eslogger +install_shell_logging +install_firewall_logging +install_log_archive +check_eslogger + +echo +say "Done. Look at: tail -f $ES_LOG ; log show --last 5m --predicate 'eventMessage contains \"winlogs-cmd\"'" +say "Undo: sudo $0 --revert" diff --git a/tests/test-macos.sh b/tests/test-macos.sh new file mode 100755 index 0000000..7c9b8bd --- /dev/null +++ b/tests/test-macos.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# End-to-end test for the macOS script. Needs root on a DISPOSABLE macOS 13+ machine (e.g. a CI runner). +# Whether eslogger actually captures events depends on Full Disk Access, which a script cannot grant, +# so that one check is reported but never fails the run. +set -uo pipefail +cd "$(dirname "$0")/.." || exit 1 +fail=0 +check() { if "${@:2}"; then echo " PASS $1"; else echo " FAIL $1"; echo "::error::FAIL $1"; fail=$((fail+1)); fi; } +runq() { + local out + if ! out="$("$@" 2>&1)"; then + echo "$out" | tail -25 + echo "::error::$* failed: $(echo "$out" | tail -12 | tr '\n' '|')" + return 1 + fi + echo "$out" | tail -6 +} +sum() { shasum -a 256 "$1" | cut -d' ' -f1; } +fwmode() { /usr/libexec/ApplicationFirewall/socketfilterfw --getloggingmode 2>&1 | grep -io 'on\|off' | head -1 | tr '[:upper:]' '[:lower:]'; } + +PLIST=/Library/LaunchDaemons/com.winlogs.eslogger.plist +ZSHRC_BEFORE="$(sum /etc/zshrc)" +FW_BEFORE="$(fwmode)" + +echo "== dry-run changes nothing" +runq ./macos/install.sh --dry-run >/dev/null +check "dry-run left no LaunchDaemon" test ! -e "$PLIST" +check "dry-run left no state dir" test ! -d /var/db/winlogs +check "dry-run left /etc/zshrc alone" test "$(sum /etc/zshrc)" = "$ZSHRC_BEFORE" + +echo "== install" +runq ./macos/install.sh --yes --with-log-archive +check "eslogger LaunchDaemon plist written" test -s "$PLIST" +check "eslogger wrapper written" grep -q 'exec /usr/bin/eslogger exec' /usr/local/libexec/winlogs/eslogger-run.sh +check "eslogger job loaded in launchd" launchctl print system/com.winlogs.eslogger +check "newsyslog rotation config written" test -s /etc/newsyslog.d/winlogs.conf +check "log-archive job written" test -s /Library/LaunchDaemons/com.winlogs.logcollect.plist +check "/etc/zshrc still parses" zsh -n /etc/zshrc +check "zsh command hook defined" bash -c "zsh -ic 'whence -w __winlogs_cmd' 2>/dev/null | grep -q function" +check "firewall logging is on" test "$(fwmode)" = on + +echo "== events (informational)" +marker="winlogs-marker-$RANDOM$RANDOM" +/usr/bin/true "$marker" +sleep 4 +n=$(grep -c "$marker" /var/log/winlogs/eslogger.ndjson 2>/dev/null || true) +echo "::notice::eslogger captured marker lines: ${n:-0} | size: $(stat -f %z /var/log/winlogs/eslogger.ndjson 2>&1) | err: $(tail -c 300 /var/log/winlogs/eslogger.err 2>&1 | tr '\n' '|')" + +echo "== re-run is idempotent" +runq ./macos/install.sh --yes +check "exactly one zshrc block after re-run" test "$(grep -c '>>> winlogs >>>' /etc/zshrc)" = 1 + +echo "== revert" +runq ./macos/install.sh --yes --revert +check "LaunchDaemon plist removed" test ! -e "$PLIST" +check "log-archive plist removed" test ! -e /Library/LaunchDaemons/com.winlogs.logcollect.plist +check "newsyslog config removed" test ! -e /etc/newsyslog.d/winlogs.conf +check "job unloaded from launchd" bash -c '! launchctl print system/com.winlogs.eslogger >/dev/null 2>&1' +check "/etc/zshrc restored exactly" test "$(sum /etc/zshrc)" = "$ZSHRC_BEFORE" +check "firewall logging mode restored" test "$(fwmode)" = "$FW_BEFORE" +check "state dir removed" test ! -d /var/db/winlogs + +if [ "$fail" = 0 ]; then echo "All checks passed."; else echo "$fail check(s) failed."; exit 1; fi