diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5ea5229 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,50 @@ +name: test +on: + push: + pull_request: + workflow_dispatch: + +jobs: + windows: + strategy: + fail-fast: false + matrix: + os: [windows-2022, windows-2025] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Test with Windows PowerShell 5.1 + shell: powershell + run: .\tests\Test-WinLogs.ps1 + - name: Test with PowerShell 7 + shell: pwsh + run: .\tests\Test-WinLogs.ps1 + + linux: + strategy: + fail-fast: false + matrix: + os: [ubuntu-22.04, ubuntu-24.04] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: ShellCheck + run: | + out="$(shellcheck -f gcc -e SC1091 linux/install.sh linux/prepare-offline.sh tests/test-linux.sh)" || { + echo "$out" | sed 's/^/::error::/' + exit 1 + } + - name: Test + run: sudo ./tests/test-linux.sh + + fedora: + runs-on: ubuntu-latest + # Container, so no systemd/kernel audit: this validates the dnf install, config and revert paths only. + container: + image: fedora:latest + steps: + - name: Tools for checkout + run: dnf install -y git tar gzip findutils which diffutils + - uses: actions/checkout@v4 + - name: Test + run: ./tests/test-linux.sh diff --git a/.github/workflows/windows-test.yml b/.github/workflows/windows-test.yml deleted file mode 100644 index 1c5196b..0000000 --- a/.github/workflows/windows-test.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: windows-test -on: - push: - pull_request: - workflow_dispatch: - -jobs: - windows: - strategy: - fail-fast: false - matrix: - os: [windows-2022, windows-2025] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: Test with Windows PowerShell 5.1 - shell: powershell - run: .\tests\Test-WinLogs.ps1 - - name: Test with PowerShell 7 - shell: pwsh - run: .\tests\Test-WinLogs.ps1 diff --git a/linux/audit/LICENSE-Neo23x0-auditd b/linux/audit/LICENSE-Neo23x0-auditd new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/linux/audit/LICENSE-Neo23x0-auditd @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/linux/audit/neo23x0-audit.rules b/linux/audit/neo23x0-audit.rules new file mode 100644 index 0000000..677bc7f --- /dev/null +++ b/linux/audit/neo23x0-audit.rules @@ -0,0 +1,488 @@ +# ___ ___ __ __ +# / | __ ______/ (_) /_____/ / +# / /| |/ / / / __ / / __/ __ / +# / ___ / /_/ / /_/ / / /_/ /_/ / +# /_/ |_\__,_/\__,_/_/\__/\__,_/ +# +# Linux Audit Daemon - Best Practice Configuration +# /etc/audit/audit.rules +# +# Maintained by Nextron Systems (https://www.nextron-systems.com) +# +# These rules provide broad, high-fidelity telemetry for Linux hosts. +# Detection intelligence (specific tool signatures, behavioral patterns, +# threat-actor TTPs) belongs in Sigma rules evaluated by your SIEM or +# a host-level agent, not in the audit ruleset itself. +# +# UID_MIN convention +# Rules that target human-user activity use `auid>=1000 -F auid!=unset`. +# 1000 is the default UID_MIN on Debian/Ubuntu and RHEL 7+ / Fedora. +# If your host uses a different UID_MIN (e.g. 500 on RHEL/CentOS 6), +# replace 1000 everywhere with `awk '/^UID_MIN/{print $2}' /etc/login.defs`. +# auditd rules do not support variables, so this substitution must be +# done at deploy time (Ansible/Jinja2, sed, envsubst, etc.). +# +# Remove any existing rules +-D + +# Buffer Size +## Feel free to increase this if the machine panic's +-b 8192 + +# Failure Mode +## Possible values: 0 (silent), 1 (printk, print a failure message), 2 (panic, halt the system) +-f 1 + +# Ignore errors +## Keep optional distro-specific paths from aborting the whole load. +## For strict validation, test a copy of this file with this line removed. +-i + +# Self Auditing --------------------------------------------------------------- + +## Audit the audit logs +### Successful and unsuccessful attempts to read information from the audit records +-w /var/log/audit/ -p wra -k auditlog +-w /var/audit/ -p wra -k auditlog + +## Auditd configuration +### Modifications to audit configuration that occur while the audit collection functions are operating +-w /etc/audit/ -p wa -k auditconfig +-w /etc/libaudit.conf -p wa -k auditconfig +-w /etc/audisp/ -p wa -k audispconfig + +# Filters --------------------------------------------------------------------- + +### We put these early because audit is a first match wins system. + +## Ignore current working directory records +## -a always,exclude -F msgtype=CWD + +## Cron jobs fill the logs with stuff we normally don't want (works with SELinux) +## NOTE: subj_type= requires SELinux. On non-SELinux systems (Debian/Ubuntu) +## these rules are silently ignored (due to -i above). Replace with UID-based +## filters if not running SELinux. +-a never,user -F subj_type=crond_t +-a never,exit -F subj_type=crond_t + +## Optional, distro-specific chrony/ntp suppression (commented out by default): +## Account names differ across distros (chrony vs _chrony on Ubuntu 24.04) so +## uncomment and adjust uid= to your host's time-daemon account if needed. +#-a never,exit -F arch=b64 -S adjtimex -F auid=unset -F uid=chrony -F subj_type=chronyd_t + +## This is not very interesting and wastes a lot of space if the server is public facing +-a always,exclude -F msgtype=CRYPTO_KEY_USER + +## Open VM Tools +-a never,exit -F arch=b64 -S all -F exe=/usr/bin/vmtoolsd +-a never,exit -F arch=b32 -S all -F exe=/usr/bin/vmtoolsd + +## High Volume Event Filter (especially on Linux Workstations) +-a never,exit -F arch=b32 -F dir=/dev/shm/ -F key=sharedmemaccess +-a never,exit -F arch=b64 -F dir=/dev/shm/ -F key=sharedmemaccess + +-a never,exit -F arch=b32 -F dir=/var/lock/lvm/ -F key=locklvm +-a never,exit -F arch=b64 -F dir=/var/lock/lvm/ -F key=locklvm + +## Filebeat +### https://www.elastic.co/guide/en/beats/filebeat/current/directory-layout.html + +-a always,exit -F arch=b32 -F dir=/etc/filebeat/ -F perm=wa -F key=filebeat +-a always,exit -F arch=b64 -F dir=/etc/filebeat/ -F perm=wa -F key=filebeat + +-a always,exit -F arch=b32 -F dir=/usr/share/filebeat/ -F perm=wa -F key=filebeat +-a always,exit -F arch=b64 -F dir=/usr/share/filebeat/ -F perm=wa -F key=filebeat + +## More information on how to filter events +### https://access.redhat.com/solutions/2482221 + +# Rules ----------------------------------------------------------------------- + +## Kernel parameters +-w /etc/sysctl.conf -p wa -k sysctl +-w /etc/sysctl.d -p wa -k sysctl + +## Kernel module loading and unloading +-a always,exit -F arch=b64 -S finit_module -S init_module -S delete_module -F auid!=unset -k modules +-a always,exit -F arch=b32 -S finit_module -S init_module -S delete_module -F auid!=unset -k modules + +## Modprobe configuration +-w /etc/modprobe.conf -p wa -k modprobe +-w /etc/modprobe.d -p wa -k modprobe +-w /etc/modules-load.d/ -p wa -k modprobe + +## KExec usage (all actions) +## NOTE: kexec_file_load is x86_64-only; i386/b32 kexec is deprecated +## and omitted here to keep the ruleset portable across libaudit versions. +-a always,exit -F arch=b64 -S kexec_file_load -k KEXEC + +## Special files +-a always,exit -F arch=b64 -S mknod -S mknodat -k specialfiles +-a always,exit -F arch=b32 -S mknod -S mknodat -k specialfiles + +## Mount operations (only attributable) +-a always,exit -F arch=b64 -S mount -S umount2 -S move_mount -S open_tree -S fsopen -S fsconfig -S fsmount -F auid!=unset -k mount +-a always,exit -F arch=b32 -S mount -S umount2 -S move_mount -S open_tree -S fsopen -S fsconfig -S fsmount -F auid!=unset -k mount + +## Change swap (only attributable) +-a always,exit -F arch=b64 -S swapon -S swapoff -F auid!=unset -k swap +-a always,exit -F arch=b32 -S swapon -S swapoff -F auid!=unset -k swap + +## Time +## Account names differ across distros (ntp/chrony/systemd-timesync) so no +## uid!= filter is applied here. Add a distro-specific never,exit overlay +## above if your time daemon generates excessive noise. +-a always,exit -F arch=b64 -S adjtimex -S settimeofday -S clock_settime -k time +-a always,exit -F arch=b32 -S adjtimex -S settimeofday -S clock_settime -k time +### Local time zone +-w /etc/localtime -p wa -k localtime + +## Cron configuration & scheduled jobs +-w /etc/cron.allow -p wa -k cron +-w /etc/cron.deny -p wa -k cron +-w /etc/cron.d/ -p wa -k cron +-w /etc/cron.daily/ -p wa -k cron +-w /etc/cron.hourly/ -p wa -k cron +-w /etc/cron.monthly/ -p wa -k cron +-w /etc/cron.weekly/ -p wa -k cron +-w /etc/crontab -p wa -k cron +-w /etc/anacrontab -p wa -k cron +-w /etc/at.allow -p wa -k cron +-w /etc/at.deny -p wa -k cron +-w /var/spool/cron/ -p wa -k cron + +## User, group, password databases +-w /etc/group -p wa -k etcgroup +-w /etc/passwd -p wa -k etcpasswd +-w /etc/gshadow -p wa -k etcgroup +-w /etc/shadow -p wa -k etcpasswd +-w /etc/nsswitch.conf -p wa -k etcpasswd +-w /etc/sssd/ -p wa -k etcpasswd +-w /etc/openldap/ -p wa -k etcpasswd +-w /etc/krb5.conf -p wa -k etcpasswd +-w /etc/krb5.conf.d/ -p wa -k etcpasswd +-w /etc/subuid -p wa -k etcpasswd +-w /etc/subgid -p wa -k etcpasswd + +## PAM & security configuration +-w /etc/pam.d/ -p wa -k pam +-w /etc/security/ -p wa -k pam +-w /etc/polkit-1/ -p wa -k pam + +## Sudoers file changes +-w /etc/sudoers -p wa -k actions +-w /etc/sudoers.d/ -p wa -k actions + +## Login configuration and information +-w /etc/login.defs -p wa -k login +-w /etc/securetty -p wa -k login +-w /var/log/faillog -p wa -k login +-w /var/log/lastlog -p wa -k login +-w /var/log/tallylog -p wa -k login + +## Network Environment +### Changes to hostname +-a always,exit -F arch=b64 -S sethostname -S setdomainname -k network_modifications +-a always,exit -F arch=b32 -S sethostname -S setdomainname -k network_modifications + +### Successful IPv4 Connections +-a always,exit -F arch=b64 -S connect -F a2=16 -F success=1 -F key=network_connect_4 +-a always,exit -F arch=b32 -S connect -F a2=16 -F success=1 -F key=network_connect_4 + +### Successful IPv6 Connections +-a always,exit -F arch=b64 -S connect -F a2=28 -F success=1 -F key=network_connect_6 +-a always,exit -F arch=b32 -S connect -F a2=28 -F success=1 -F key=network_connect_6 + +### Changes to other files +-w /etc/hosts -p wa -k network_modifications +-w /etc/resolv.conf -p wa -k network_modifications +-w /etc/hostname -p wa -k network_modifications +-w /etc/sysconfig/ -p wa -k sysconfig +-w /etc/network/ -p wa -k network +-a always,exit -F dir=/etc/NetworkManager/ -F perm=wa -k network_modifications + +### Firewall configuration +-w /etc/nftables.conf -p wa -k firewall +-w /etc/iptables/ -p wa -k firewall + +### Changes to issue +-w /etc/issue -p wa -k etcissue +-w /etc/issue.net -p wa -k etcissue + +## Service defaults +-w /etc/default/ -p wa -k svc_defaults + +## Filesystem table +-w /etc/fstab -p wa -k fstab + +## udev rules +-w /etc/udev/rules.d/ -p wa -k udev + +## System startup scripts +-w /etc/inittab -p wa -k init +-w /etc/init.d/ -p wa -k init +-w /etc/init/ -p wa -k init +-w /etc/rc.local -p wa -k init + +## System binary and boot path changes +## NOTE: On modern distros /bin -> /usr/bin; bin_writes and usr_writes may +## double-fire for the same path. Keep both for older layouts. +-a always,exit -F arch=b32 -F dir=/bin -F perm=wa -k bin_writes +-a always,exit -F arch=b64 -F dir=/bin -F perm=wa -k bin_writes +-a always,exit -F arch=b32 -F dir=/usr -F perm=wa -k usr_writes +-a always,exit -F arch=b64 -F dir=/usr -F perm=wa -k usr_writes +-a always,exit -F arch=b32 -F dir=/boot -F perm=wa -k boot_writes +-a always,exit -F arch=b64 -F dir=/boot -F perm=wa -k boot_writes + +## Library search paths +-w /etc/ld.so.conf -p wa -k libpath +-w /etc/ld.so.conf.d -p wa -k libpath + +## Systemwide library preloads (LD_PRELOAD) +-w /etc/ld.so.preload -p wa -k systemwide_preloads + +## System-wide environment variables +-w /etc/environment -p wa -k environment + +## Mail configuration +-w /etc/aliases -p wa -k mail +-w /etc/postfix/ -p wa -k mail +-w /etc/exim4/ -p wa -k mail + +## SSH configuration +-w /etc/ssh/ -p wa -k sshd +-w /etc/dropbear/ -p wa -k sshd + +## root ssh key tampering +-w /root/.ssh -p wa -k rootkey + +# Systemd +-w /etc/systemd/ -p wa -k systemd +-w /usr/lib/systemd -p wa -k systemd +-w /lib/systemd/ -p wa -k systemd +-w /usr/local/lib/systemd/ -p wa -k systemd + +## Mandatory Access Controls (MAC) policy +-w /etc/selinux/ -p wa -k mac_policy +-w /etc/apparmor/ -p wa -k mac_policy +-w /etc/apparmor.d/ -p wa -k mac_policy + +## Shell/profile configurations +-w /etc/profile.d/ -p wa -k shell_profiles +-w /etc/profile -p wa -k shell_profiles +-w /etc/shells -p wa -k shell_profiles +-w /etc/bashrc -p wa -k shell_profiles +-w /etc/csh.cshrc -p wa -k shell_profiles +-w /etc/csh.login -p wa -k shell_profiles +-w /etc/fish/ -p wa -k shell_profiles +-w /etc/zsh/ -p wa -k shell_profiles + +## Critical elements access failures +-a always,exit -F arch=b64 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/etc -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b32 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/etc -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b64 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/bin -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b32 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/bin -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b64 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/sbin -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b32 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/sbin -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b64 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/usr/bin -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b32 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/usr/bin -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b64 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/usr/sbin -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b32 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/usr/sbin -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b64 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/var -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b32 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/var -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b64 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/home -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b32 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/home -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b64 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/srv -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess +-a always,exit -F arch=b32 -S open -S openat -S openat2 -S open_by_handle_at -F dir=/srv -F success=0 -F auid>=1000 -F auid!=unset -k unauthedfileaccess + +### Permission-denied opens from system accounts/daemons (no login session). +### Narrower than success=0 above to avoid the file-not-found noise that +### dominates daemon activity — EACCES/EPERM are the high-signal cases. +-a always,exit -F arch=b64 -S open -S openat -S openat2 -S open_by_handle_at -F exit=-EACCES -F auid=unset -k unauthedfileaccess_system +-a always,exit -F arch=b64 -S open -S openat -S openat2 -S open_by_handle_at -F exit=-EPERM -F auid=unset -k unauthedfileaccess_system +-a always,exit -F arch=b32 -S open -S openat -S openat2 -S open_by_handle_at -F exit=-EACCES -F auid=unset -k unauthedfileaccess_system +-a always,exit -F arch=b32 -S open -S openat -S openat2 -S open_by_handle_at -F exit=-EPERM -F auid=unset -k unauthedfileaccess_system + +## Session initiation information +-w /var/run/utmp -p wa -k session +-w /var/log/btmp -p wa -k session +-w /var/log/wtmp -p wa -k session + +## Discretionary Access Control (DAC) modifications +## NOTE: Rules that use `auid>=1000` assume the common Linux `UID_MIN=1000`. +## If your host uses a different UID_MIN in `/etc/login.defs`, replace `1000` +## accordingly before deployment. `auid!=unset` excludes daemon/system sessions. +-a always,exit -F arch=b64 -S chmod -S fchmod -S fchmodat -F auid>=1000 -F auid!=unset -k perm_mod +-a always,exit -F arch=b32 -S chmod -S fchmod -S fchmodat -F auid>=1000 -F auid!=unset -k perm_mod +-a always,exit -F arch=b64 -S chown -S fchown -S fchownat -S lchown -F auid>=1000 -F auid!=unset -k perm_mod +-a always,exit -F arch=b32 -S chown -S fchown -S fchownat -S lchown -F auid>=1000 -F auid!=unset -k perm_mod +-a always,exit -F arch=b64 -S setxattr -S lsetxattr -S fsetxattr -S removexattr -S lremovexattr -S fremovexattr -F auid>=1000 -F auid!=unset -k perm_mod +-a always,exit -F arch=b32 -S setxattr -S lsetxattr -S fsetxattr -S removexattr -S lremovexattr -S fremovexattr -F auid>=1000 -F auid!=unset -k perm_mod + +# Special Rules --------------------------------------------------------------- + +## Ptrace (injection, debugging, tracing) +### Logs all ptrace calls; Sigma differentiates by a0 value +### (0x4=PTRACE_POKETEXT, 0x5=PTRACE_POKEDATA, 0x6=PTRACE_POKEUSR, etc.) +-a always,exit -F arch=b64 -S ptrace -k tracing +-a always,exit -F arch=b32 -S ptrace -k tracing + +## Anonymous File Creation +### "memfd_create" creates anonymous file and returns a file descriptor to access it +### When combined with "fexecve" can be used to stealthily run binaries in memory without touching disk +-a always,exit -F arch=b64 -S memfd_create -F key=anon_file_create +-a always,exit -F arch=b32 -S memfd_create -F key=anon_file_create + +## Timestomping (T1070.006) +-a always,exit -F arch=b64 -S utimensat -S utimes -S futimesat -F auid>=1000 -F auid!=unset -k T1070_006_timestomp +-a always,exit -F arch=b32 -S utimensat -S utimes -S futimesat -F auid>=1000 -F auid!=unset -k T1070_006_timestomp + +## eBPF program loading +-a always,exit -F arch=b64 -S bpf -k bpf +-a always,exit -F arch=b32 -S bpf -k bpf + +## Namespace manipulation +-a always,exit -F arch=b64 -S unshare -S setns -S pivot_root -k namespaces +-a always,exit -F arch=b32 -S unshare -S setns -S pivot_root -k namespaces + +## Cross-process memory access +-a always,exit -F arch=b64 -S process_vm_readv -S process_vm_writev -k process_vm +-a always,exit -F arch=b32 -S process_vm_readv -S process_vm_writev -k process_vm + +## io_uring (known audit blind spot — operations bypass syscall auditing) +-a always,exit -F arch=b64 -S io_uring_setup -k io_uring +-a always,exit -F arch=b32 -S io_uring_setup -k io_uring + +## Exploitation primitives +-a always,exit -F arch=b64 -S userfaultfd -k userfaultfd +-a always,exit -F arch=b32 -S userfaultfd -k userfaultfd + +## System reboot/shutdown +-a always,exit -F arch=b64 -S reboot -k reboot +-a always,exit -F arch=b32 -S reboot -k reboot + +## Process accounting control +-a always,exit -F arch=b64 -S acct -k process_accounting +-a always,exit -F arch=b32 -S acct -k process_accounting + +## Kernel crypto sockets (AF_ALG) +### Low-noise on most hosts. AF_ALG usage typically requires socket creation, +### SOL_ALG configuration, and bind() with a SOCKADDR record that downstream +### tooling can decode to recover salg_type / salg_name. Audit cannot match +### those strings in-kernel, so keep the collection generic here. +-a always,exit -F arch=b64 -S socket -F a0=38 -F success=1 -F auid>=1000 -F auid!=unset -k af_alg +-a always,exit -F arch=b32 -S socket -F a0=38 -F success=1 -F auid>=1000 -F auid!=unset -k af_alg +-a always,exit -F arch=b64 -S bind -F a2=88 -F success=1 -F auid>=1000 -F auid!=unset -k af_alg +-a always,exit -F arch=b32 -S bind -F a2=88 -F success=1 -F auid>=1000 -F auid!=unset -k af_alg +-a always,exit -F arch=b64 -S setsockopt -F a1=279 -F success=1 -F auid>=1000 -F auid!=unset -k af_alg +-a always,exit -F arch=b32 -S setsockopt -F a1=279 -F success=1 -F auid>=1000 -F auid!=unset -k af_alg + +## Privilege Abuse +### The purpose of this rule is to detect when an admin may be abusing power by looking in user's home dir. +-a always,exit -F dir=/home -F uid=0 -F auid>=1000 -F auid!=unset -C auid!=obj_uid -k power_abuse + +## Raw sockets +### Logs all raw sockets +-a always,exit -F arch=b32 -S socket -F a0=17 -F a1=3 -k raw_network_socket_created +-a always,exit -F arch=b64 -S socket -F a0=17 -F a1=3 -k raw_network_socket_created +### Keep watch for when BPF filters are attached +-a always,exit -F arch=b32 -S setsockopt -F a1=1 -F a2=26 -k socket_bpf_filter_attached +-a always,exit -F arch=b64 -S setsockopt -F a1=1 -F a2=26 -k socket_bpf_filter_attached + +# Socket Creations +# will catch both IPv4 and IPv6 + +-a always,exit -F arch=b32 -S socket -F a0=2 -k network_socket_created +-a always,exit -F arch=b64 -S socket -F a0=2 -k network_socket_created + +-a always,exit -F arch=b32 -S socket -F a0=10 -k network_socket_created +-a always,exit -F arch=b64 -S socket -F a0=10 -k network_socket_created + +## Optional overlay: enable only if splice/vmsplice are rare in your estate. +## Correlate short bursts of these events with recent af_alg activity from the +## same pid/auid when investigating unusual kernel crypto socket usage. +#-a always,exit -F arch=b64 -S splice -S vmsplice -F success=1 -F auid>=1000 -F auid!=unset -k splice_user +#-a always,exit -F arch=b32 -S splice -S vmsplice -F success=1 -F auid>=1000 -F auid!=unset -k splice_user + +# Software Management --------------------------------------------------------- + +## Package manager configuration +-w /etc/apt/ -p wa -k software_mgmt +-w /etc/dnf/ -p wa -k software_mgmt +-w /etc/yum.repos.d/ -p wa -k software_mgmt + +## Container configuration +-w /var/lib/docker -p wa -k docker +-w /etc/docker -p wa -k docker +-w /etc/containers/ -p wa -k containers + +# CrowdStrike Falcon +-a always,exit -F arch=b32 -F dir=/etc/crowdstrike/ -F perm=wa -F key=falcon_sensor +-a always,exit -F arch=b64 -F dir=/etc/crowdstrike/ -F perm=wa -F key=falcon_sensor + +-a always,exit -F arch=b32 -F dir=/usr/lib/crowdstrike/ -F perm=wa -F key=falcon_sensor +-a always,exit -F arch=b64 -F dir=/usr/lib/crowdstrike/ -F perm=wa -F key=falcon_sensor + +-a always,exit -F arch=b32 -F dir=/opt/CrowdStrike/ -F perm=wa -F key=falcon_sensor +-a always,exit -F arch=b64 -F dir=/opt/CrowdStrike/ -F perm=wa -F key=falcon_sensor + +-a always,exit -F arch=b32 -F dir=/var/log/crowdstrike/ -F perm=wa -F key=falcon_sensor +-a always,exit -F arch=b64 -F dir=/var/log/crowdstrike/ -F perm=wa -F key=falcon_sensor + +-a always,exit -F arch=b32 -S connect -F exe=/opt/CrowdStrike/falcon-sensor -F key=crowdstrike_network +-a always,exit -F arch=b64 -S connect -F exe=/opt/CrowdStrike/falcon-sensor -F key=crowdstrike_network + +# ipc system call +# /usr/include/linux/ipc.h + +## NOTE: glibc's semop() is implemented via semtimedop() on modern systems; +## the standalone semop name was dropped from some libaudit tables. +-a always,exit -F arch=b64 -S msgctl -S msgget -S semctl -S semget -S shmctl -S shmget -k Inter-Process_Communication +-a always,exit -F arch=b32 -S msgctl -S msgget -S semctl -S semget -S shmctl -S shmget -k Inter-Process_Communication + +# High Volume Events ---------------------------------------------------------- + +## Disable these rules if they create too many events in your environment + +## Process creation +### Collect generic execution telemetry and derive tool- and context-specific +### detections in downstream tooling such as Sigma / SIEM content. +-a always,exit -F arch=b32 -S execve -S execveat -k process_creation +-a always,exit -F arch=b64 -S execve -S execveat -k process_creation + +## File Deletion Events by User +-a always,exit -F arch=b64 -S rmdir -S unlink -S unlinkat -S rename -S renameat -S renameat2 -F auid>=1000 -F auid!=unset -k delete +-a always,exit -F arch=b32 -S rmdir -S unlink -S unlinkat -S rename -S renameat -S renameat2 -F auid>=1000 -F auid!=unset -k delete + +## File Access +### Unauthorized Access (unsuccessful) +-a always,exit -F arch=b64 -S creat -S open -S openat -S openat2 -S open_by_handle_at -S truncate -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -k file_access +-a always,exit -F arch=b64 -S creat -S open -S openat -S openat2 -S open_by_handle_at -S truncate -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -k file_access +-a always,exit -F arch=b32 -S creat -S open -S openat -S openat2 -S open_by_handle_at -S truncate -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=unset -k file_access +-a always,exit -F arch=b32 -S creat -S open -S openat -S openat2 -S open_by_handle_at -S truncate -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=unset -k file_access + +### Unsuccessful Creation +-a always,exit -F arch=b64 -S mkdir -S mkdirat -S creat -S link -S linkat -S symlink -S symlinkat -S mknod -S mknodat -F exit=-EACCES -k file_creation +-a always,exit -F arch=b64 -S mkdir -S mkdirat -S creat -S link -S linkat -S symlink -S symlinkat -S mknod -S mknodat -F exit=-EPERM -k file_creation +-a always,exit -F arch=b32 -S mkdir -S mkdirat -S creat -S link -S linkat -S symlink -S symlinkat -S mknod -S mknodat -F exit=-EACCES -k file_creation +-a always,exit -F arch=b32 -S mkdir -S mkdirat -S creat -S link -S linkat -S symlink -S symlinkat -S mknod -S mknodat -F exit=-EPERM -k file_creation + +### Unsuccessful Modification +-a always,exit -F arch=b64 -S rename -S renameat -S renameat2 -S truncate -S chmod -S setxattr -S lsetxattr -S removexattr -S lremovexattr -F exit=-EACCES -k file_modification +-a always,exit -F arch=b64 -S rename -S renameat -S renameat2 -S truncate -S chmod -S setxattr -S lsetxattr -S removexattr -S lremovexattr -F exit=-EPERM -k file_modification +-a always,exit -F arch=b32 -S rename -S renameat -S renameat2 -S truncate -S chmod -S setxattr -S lsetxattr -S removexattr -S lremovexattr -F exit=-EACCES -k file_modification +-a always,exit -F arch=b32 -S rename -S renameat -S renameat2 -S truncate -S chmod -S setxattr -S lsetxattr -S removexattr -S lremovexattr -F exit=-EPERM -k file_modification + +## 32bit ABI Exploitation +### https://github.com/linux-audit/audit-userspace/blob/c014eec64b3a16c004f4a75e5792a4ac2fcc0df2/rules/21-no32bit.rules +### If you are on a 64 bit platform, everything _should_ be running +### in 64 bit mode. This rule will detect any use of the 32 bit syscalls +### because this might be a sign of someone exploiting a hole in the 32 +### bit ABI. +### NOTE: Explicit b32 rules above provide specific keys for SIEM correlation; +### this catch-all additionally tags all 32-bit activity under a single key. +-a always,exit -F arch=b32 -S all -k 32bit_abi + +# Make The Configuration Immutable -------------------------------------------- + +##-e 2 diff --git a/linux/install.sh b/linux/install.sh new file mode 100755 index 0000000..b76760d --- /dev/null +++ b/linux/install.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +# WinLogs for Linux: quick 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 +# sudo ./install.sh --offline-dir D install packages from a folder made by prepare-offline.sh +# +# Turns on: auditd + the Neo23x0 ruleset, persistent journald with no rate limiting, larger audit +# log retention, and bash command logging to syslog. Sysmon for Linux is opt-in (--with-sysmon). +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +STATE_DIR=/var/lib/winlogs +BACKUP_DIR="$STATE_DIR/backup" +MANIFEST="$STATE_DIR/manifest" # lines: "created " | "backup " | "pkg " +RULES_DST=/etc/audit/rules.d/90-winlogs.rules +JOURNAL_DROPIN=/etc/systemd/journald.conf.d/90-winlogs.conf +PROFILE_DST=/etc/profile.d/winlogs-cmdlog.sh +AUDITD_CONF=/etc/audit/auditd.conf + +YES=0 DRY=0 REVERT=0 SYSMON=0 SHELLLOG=1 OFFLINE="" JOURNAL_MAX=2G + +usage() { sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; echo "Options: --yes --dry-run --revert --offline-dir DIR --with-sysmon --no-shell-logging --journal-size SIZE"; } + +while [ $# -gt 0 ]; do + case "$1" in + --yes|-y) YES=1 ;; + --dry-run) DRY=1 ;; + --revert) REVERT=1 ;; + --with-sysmon) SYSMON=1 ;; + --no-shell-logging) SHELLLOG=0 ;; + --offline-dir) OFFLINE="${2:?--offline-dir needs a folder}"; shift ;; + --journal-size) JOURNAL_MAX="${2:?--journal-size needs a value like 2G}"; 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; } + +[ "$(id -u)" = 0 ] || die "Run as root (sudo)." +[ "$(uname -s)" = Linux ] || die "This script is for Linux." + +confirm() { + [ "$YES" = 1 ] || [ "$DRY" = 1 ] && return 0 + 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." +} + +detect_pm() { + if command -v apt-get >/dev/null; then PM=apt; AUDIT_PKG=auditd + elif command -v dnf >/dev/null; then PM=dnf; AUDIT_PKG=audit + elif command -v yum >/dev/null; then PM=yum; AUDIT_PKG=audit + elif command -v zypper >/dev/null; then PM=zypper; AUDIT_PKG=audit + elif command -v pacman >/dev/null; then PM=pacman; AUDIT_PKG=audit + else die "No supported package manager (apt, dnf, yum, zypper, pacman)."; fi +} + +record() { [ "$DRY" = 1 ] || { mkdir -p "$STATE_DIR"; echo "$*" >> "$MANIFEST"; }; } +already_recorded() { [ -f "$MANIFEST" ] && grep -qxF "$*" "$MANIFEST"; } + +# Back up a file the first time we touch it (revert restores it); record files we create. +claim() { + local f="$1" + [ "$DRY" = 1 ] && return 0 + mkdir -p "$BACKUP_DIR" + # A file we created on an earlier run is ours, not something to back up and restore. + if already_recorded "created $f"; then return 0; fi + if [ -e "$f" ]; then + already_recorded "backup $f" || { cp -a "$f" "$BACKUP_DIR/$(echo "$f" | tr / _)"; record "backup $f"; } + else + already_recorded "created $f" || record "created $f" + fi +} + +pkg_installed() { + case "$PM" in + apt) dpkg -s "$1" >/dev/null 2>&1 ;; + dnf|yum|zypper) rpm -q "$1" >/dev/null 2>&1 ;; + pacman) pacman -Qi "$1" >/dev/null 2>&1 ;; + esac +} + +install_packages() { + if pkg_installed "$AUDIT_PKG"; then say "$AUDIT_PKG already installed"; return; fi + say "Installing $AUDIT_PKG" + if [ -n "$OFFLINE" ]; then + [ -d "$OFFLINE" ] || die "Offline folder not found: $OFFLINE" + ( cd "$OFFLINE" && { [ ! -f SHA256SUMS ] || sha256sum -c --quiet SHA256SUMS; } ) \ + || die "Offline bundle failed its SHA256SUMS check." + case "$PM" in + apt) run dpkg -i "$OFFLINE"/*.deb ;; + dnf|yum) run "$PM" install -y --disablerepo='*' "$OFFLINE"/*.rpm ;; + zypper) run zypper --non-interactive install --allow-unsigned-rpm "$OFFLINE"/*.rpm ;; + pacman) run pacman -U --noconfirm "$OFFLINE"/*.pkg.tar.* ;; + esac + else + case "$PM" in + apt) run env DEBIAN_FRONTEND=noninteractive apt-get install -y "$AUDIT_PKG" ;; + dnf|yum) run "$PM" install -y "$AUDIT_PKG" ;; + zypper) run zypper --non-interactive install "$AUDIT_PKG" ;; + pacman) run pacman -S --noconfirm "$AUDIT_PKG" ;; + esac + fi + record "pkg $AUDIT_PKG" +} + +install_audit_rules() { + say "Installing audit rules" + local uid_min + uid_min="$(awk '/^UID_MIN/{print $2}' /etc/login.defs 2>/dev/null || true)" + uid_min="${uid_min:-1000}" + [ -d "$(dirname "$RULES_DST")" ] || run mkdir -p "$(dirname "$RULES_DST")" + claim "$RULES_DST" + if [ "$DRY" = 1 ]; then echo " (dry-run) write $RULES_DST (UID_MIN=$uid_min)"; return; fi + sed "s/auid>=1000/auid>=$uid_min/g" "$HERE/audit/neo23x0-audit.rules" > "$RULES_DST" + # Some kernels boot with audit disabled; make sure it is switched on after the rules load. + printf '\n# WinLogs: make sure auditing is enabled\n-e 1\n' >> "$RULES_DST" + chmod 640 "$RULES_DST" + + # Keep more audit history than the distro default. + if [ -f "$AUDITD_CONF" ]; then + claim "$AUDITD_CONF" + sed -i -E 's/^max_log_file *=.*/max_log_file = 200/; s/^num_logs *=.*/num_logs = 10/; s/^max_log_file_action *=.*/max_log_file_action = ROTATE/; s/^flush *=.*/flush = SYNC/' "$AUDITD_CONF" + fi + + if command -v systemctl >/dev/null && systemctl list-unit-files auditd.service >/dev/null 2>&1; then + systemctl enable auditd >/dev/null 2>&1 || true + fi + # RHEL-family refuses "systemctl restart auditd"; "service" works everywhere. + service auditd restart >/dev/null 2>&1 || systemctl restart auditd >/dev/null 2>&1 \ + || warn "Could not restart auditd (container or no kernel audit support?)." + if command -v augenrules >/dev/null; then augenrules --load >/dev/null 2>&1 || warn "augenrules --load failed." + else auditctl -R "$RULES_DST" >/dev/null 2>&1 || warn "auditctl -R failed."; fi + # Belt and braces: on some kernels (seen on Ubuntu 22.04) auditing is still off after the rules load. + auditctl -e 1 >/dev/null 2>&1 || warn "Could not enable auditing (container or no kernel audit support?)." +} + +configure_journald() { + say "Making journald persistent (max $JOURNAL_MAX, no rate limiting)" + [ "$DRY" = 1 ] && { echo " (dry-run) write $JOURNAL_DROPIN"; return; } + command -v journalctl >/dev/null || { warn "No systemd-journald here; skipping."; return; } + mkdir -p /etc/systemd/journald.conf.d /var/log/journal + claim "$JOURNAL_DROPIN" + cat > "$JOURNAL_DROPIN" </dev/null 2>&1 || warn "Could not restart journald." +} + +configure_shell_logging() { + [ "$SHELLLOG" = 1 ] || return 0 + say "Logging interactive bash commands to syslog (local6)" + [ "$DRY" = 1 ] && { echo " (dry-run) write $PROFILE_DST"; return; } + claim "$PROFILE_DST" + cat > "$PROFILE_DST" <<'EOF' +# WinLogs: log each interactive bash command to syslog (user is easy to bypass; not tamper-proof). +if [ -n "${BASH_VERSION:-}" ] && [ -n "${PS1:-}" ]; then + __winlogs_cmd() { + local c; c="$(HISTTIMEFORMAT= history 1 | sed 's/^ *[0-9]* *//')" + [ -n "$c" ] && logger -p local6.info -t winlogs-cmd "user=$(id -un) uid=$(id -u) pwd=$PWD cmd=$c" + } + case ":${PROMPT_COMMAND:-}:" in *:__winlogs_cmd:*) ;; *) PROMPT_COMMAND="__winlogs_cmd${PROMPT_COMMAND:+;$PROMPT_COMMAND}" ;; esac +fi +EOF +} + +install_sysmon() { + [ "$SYSMON" = 1 ] || return 0 + [ -z "$OFFLINE" ] || die "--with-sysmon needs internet (Microsoft package repo); offline Sysmon is not supported yet." + say "Installing Sysmon for Linux from Microsoft's package repo (experimental)" + # shellcheck source=/dev/null + . /etc/os-release + local tmp; tmp="$(mktemp -d)" + case "$PM" in + apt) + run curl -fsSL -o "$tmp/ms.deb" "https://packages.microsoft.com/config/$ID/$VERSION_ID/packages-microsoft-prod.deb" + run dpkg -i "$tmp/ms.deb"; run apt-get update -y + run env DEBIAN_FRONTEND=noninteractive apt-get install -y sysmonforlinux ;; + dnf|yum) + run rpm -Uvh "https://packages.microsoft.com/config/$ID/${VERSION_ID%%.*}/packages-microsoft-prod.rpm" + run "$PM" install -y sysmonforlinux ;; + *) die "Sysmon for Linux packages are only wired up for apt and dnf/yum here." ;; + esac + record "pkg sysmonforlinux" + run sysmon -accepteula -i "$HERE/sysmon/sysmonconfig-linux.xml" + rm -rf "$tmp" +} + +do_revert() { + [ -f "$MANIFEST" ] || die "No manifest at $MANIFEST; nothing to revert." + confirm + say "Reverting" + while read -r kind path; do + case "$kind" in + created) run rm -f "$path" ;; + backup) run cp -a "$BACKUP_DIR/$(echo "$path" | tr / _)" "$path" ;; + esac + done < "$MANIFEST" + if command -v augenrules >/dev/null; then run augenrules --load >/dev/null 2>&1 || true; fi + run service auditd restart >/dev/null 2>&1 || true + run systemctl restart systemd-journald >/dev/null 2>&1 || true + if grep -qxF "pkg sysmonforlinux" "$MANIFEST"; then run sysmon -u force >/dev/null 2>&1 || true; fi + [ "$DRY" = 1 ] || rm -rf "$STATE_DIR" + say "Reverted. Packages we installed were left in place (remove them with your package manager if you want)." +} + +if [ "$REVERT" = 1 ]; then do_revert; exit 0; fi + +detect_pm +say "WinLogs on $(grep -m1 ^PRETTY_NAME= /etc/os-release | cut -d= -f2- | tr -d '"') using $PM" +confirm +install_packages +install_audit_rules +configure_journald +configure_shell_logging +install_sysmon + +# Last step on purpose: on Ubuntu 22.04 restarting journald switches kernel auditing back off. +if [ "$DRY" != 1 ] && command -v auditctl >/dev/null; then + auditctl -e 1 >/dev/null 2>&1 || warn "Could not enable auditing (container or no kernel audit support?)." +fi + +echo +say "Done. Check: auditctl -l | wc -l ; ausearch --input-logs -k process_creation -ts recent ; journalctl --disk-usage" +say "Undo: sudo $0 --revert" diff --git a/linux/prepare-offline.sh b/linux/prepare-offline.sh new file mode 100755 index 0000000..55d0b87 --- /dev/null +++ b/linux/prepare-offline.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Download the packages WinLogs needs so an air-gapped Linux lab can install without internet. +# +# ./prepare-offline.sh [OUT_DIR] (run as root, on a machine with internet) +# +# Run it on a machine that matches the lab image (same distro and release, ideally a fresh copy of the +# lab VM template) so every dependency the lab is missing gets downloaded. Then copy OUT_DIR to the lab: +# sudo ./install.sh --offline-dir OUT_DIR +set -euo pipefail + +OUT="${1:-./winlogs-offline-linux}" +[ "$(id -u)" = 0 ] || { echo "Run as root (sudo)." >&2; exit 1; } +mkdir -p "$OUT" +OUT="$(cd "$OUT" && pwd)" +rm -f "$OUT"/*.deb "$OUT"/*.rpm "$OUT"/*.pkg.tar.* "$OUT/SHA256SUMS" + +if command -v apt-get >/dev/null; then + apt-get update -y + # --reinstall so the package is fetched even if it is already installed here. + apt-get install -y --reinstall --download-only -o "Dir::Cache::archives=$OUT" auditd + rm -f "$OUT/lock"; rm -rf "$OUT/partial" +elif command -v dnf >/dev/null; then + # dnf4 takes --downloaddir on install; dnf5 (Fedora 41+) uses the download command instead. + dnf install -y --downloadonly --downloaddir="$OUT" audit 2>/dev/null \ + || dnf download --resolve --destdir="$OUT" audit +elif command -v yum >/dev/null; then + yum install -y --downloadonly --downloaddir="$OUT" audit +elif command -v zypper >/dev/null; then + zypper --non-interactive --pkg-cache-dir "$OUT" install --download-only audit + find "$OUT" -name '*.rpm' -exec mv -t "$OUT" {} + 2>/dev/null || true +elif command -v pacman >/dev/null; then + pacman -Sw --noconfirm --cachedir "$OUT" audit +else + echo "No supported package manager." >&2; exit 1 +fi + +cd "$OUT" +find . -maxdepth 1 -type f \( -name '*.deb' -o -name '*.rpm' -o -name '*.pkg.tar.*' \) -printf '%P\n' | sort > .pkgs +[ -s .pkgs ] || { echo "Nothing was downloaded." >&2; exit 1; } +xargs -d '\n' sha256sum < .pkgs > SHA256SUMS +rm -f .pkgs +echo "Offline bundle ready: $OUT ($(wc -l < SHA256SUMS) packages)" +echo "Copy it to the lab, then: sudo ./install.sh --offline-dir $OUT" diff --git a/linux/sysmon/sysmonconfig-linux.xml b/linux/sysmon/sysmonconfig-linux.xml new file mode 100644 index 0000000..fab292f --- /dev/null +++ b/linux/sysmon/sysmonconfig-linux.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tests/test-linux.sh b/tests/test-linux.sh new file mode 100755 index 0000000..60edf8d --- /dev/null +++ b/tests/test-linux.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# End-to-end test for the Linux script. Needs root on a DISPOSABLE Debian/Ubuntu-style machine +# with internet (e.g. a CI runner). Exercises dry-run, offline prep, offline install, events, re-run, revert. +set -uo pipefail +cd "$(dirname "$0")/.." || exit 1 +fail=0 +# Without systemd (e.g. a container) auditd and journald can't run, so skip the live-kernel checks. +LIVE=1; [ -d /run/systemd/system ] || LIVE=0 +skip() { echo " SKIP $1 (no systemd)"; } +check() { if "${@:2}"; then echo " PASS $1"; else echo " FAIL $1"; echo "::error::FAIL $1"; fail=$((fail+1)); fi; } +# Run a script; on failure surface its output as a CI annotation. +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 -5 +} +rules_loaded() { [ "$(auditctl -l 2>/dev/null | grep -c .)" -gt 50 ]; } + +echo "== dry-run changes nothing" +runq ./linux/install.sh --dry-run >/dev/null +check "dry-run left no rules file" test ! -e /etc/audit/rules.d/90-winlogs.rules +check "dry-run left no state dir" test ! -d /var/lib/winlogs + +echo "== prepare-offline" +runq ./linux/prepare-offline.sh /tmp/winlogs-offline +check "SHA256SUMS written" test -s /tmp/winlogs-offline/SHA256SUMS + +echo "== install (offline)" +runq ./linux/install.sh --yes --offline-dir /tmp/winlogs-offline +check "audit rules file installed" test -s /etc/audit/rules.d/90-winlogs.rules +if [ "$LIVE" = 1 ]; then check "audit rules loaded in kernel" rules_loaded; else skip "audit rules loaded in kernel"; fi +check "auditd.conf retention raised" grep -q '^max_log_file = 200' /etc/audit/auditd.conf +check "journald drop-in present" test -s /etc/systemd/journald.conf.d/90-winlogs.conf +if [ "$LIVE" = 1 ]; then check "journald is persistent" test -d /var/log/journal; else skip "journald is persistent"; fi +check "bash logging hook present" test -s /etc/profile.d/winlogs-cmdlog.sh + +if [ "$LIVE" = 1 ]; then +echo "== events actually appear" +marker="winlogs-marker-$RANDOM$RANDOM" +# Launch via systemd: on some kernels (seen on Ubuntu 22.04) processes that were already running when +# auditing was first switched on are not audited, but freshly started services are. +systemd-run --quiet --wait --collect /bin/echo "$marker" >/dev/null 2>&1 || /bin/echo "$marker" >/dev/null +sleep 2 +# auditd flushes to disk asynchronously, so retry for a while. +found_marker() { for _ in $(seq 10); do ausearch --input-logs -i -k process_creation -ts recent 2>/dev/null | grep -q "$marker" && return 0; sleep 2; done; return 1; } +check "execve audited (process_creation)" found_marker +else skip "events actually appear"; fi + +echo "== re-run is idempotent" +runq ./linux/install.sh --yes +check "second run kept one backup of auditd.conf" test "$(grep -c '^backup /etc/audit/auditd.conf' /var/lib/winlogs/manifest)" -le 1 + +echo "== revert" +runq ./linux/install.sh --yes --revert +check "rules file removed" test ! -e /etc/audit/rules.d/90-winlogs.rules +check "journald drop-in gone" test ! -e /etc/systemd/journald.conf.d/90-winlogs.conf +check "profile hook gone" test ! -e /etc/profile.d/winlogs-cmdlog.sh +check "state dir removed" test ! -d /var/lib/winlogs + +if [ "$fail" = 0 ]; then echo "All checks passed."; else echo "$fail check(s) failed."; exit 1; fi