Created
          October 16, 2025 19:32 
        
      - 
      
- 
        Save win3zz/7fe828be189c5bb5bd8d06c9207e511d to your computer and use it in GitHub Desktop. 
    Container-Optimized OS (COS) guest audit script
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | #!/usr/bin/env bash | |
| # cos_audit.sh | |
| # Container-Optimized OS (COS) guest audit script (read-only) | |
| # Produces a PASS / FAIL / INFO style report for many guest-side hardening checks. | |
| # | |
| # Usage: | |
| # sudo ./cos_audit.sh | tee cos_audit_$(date +%F_%T).log | |
| # | |
| # Author: Generated by ChatGPT for Bipin Jitiya (auditor) | |
| set -u | |
| SCRIPT_NAME="$(basename "$0")" | |
| DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" | |
| # Helpers | |
| info() { printf "INFO: %s\n" "$*"; } | |
| ok() { printf "PASS: %s\n" "$*"; } | |
| warn() { printf "FAIL: %s\n" "$*"; } | |
| note() { printf "------ %s ------\n" "$*"; } | |
| sep() { printf "\n"; } | |
| run_cmd() { | |
| # run command; print a short snippet of output (first N lines) | |
| local cmd="$*" | |
| if ! command -v bash >/dev/null 2>&1; then | |
| # fallback - shouldn't happen on COS | |
| echo "COMMAND-UNAVAILABLE: $cmd" | |
| return 1 | |
| fi | |
| eval "$cmd" 2>/dev/null | head -n 20 | |
| } | |
| check_file_contains() { | |
| # $1 file, $2 pattern | |
| local file="$1" pattern="$2" | |
| if [ -r "$file" ]; then | |
| if grep -qi -- "$pattern" "$file"; then | |
| return 0 | |
| else | |
| return 2 | |
| fi | |
| else | |
| return 1 | |
| fi | |
| } | |
| print_header() { | |
| cat <<EOF | |
| COS GUEST AUDIT REPORT | |
| Host: $(hostname -f 2>/dev/null || hostname) | |
| Date: $DATE | |
| Script: $SCRIPT_NAME | |
| Scope: Guest (root shell) | |
| Note: Read-only checks. Capture output to keep evidence. | |
| EOF | |
| sep | |
| } | |
| # Start report | |
| print_header | |
| ################################################################ | |
| # Section: Basic system identity | |
| ################################################################ | |
| note "System Identity" | |
| echo " /etc/os-release:" | |
| run_cmd "cat /etc/os-release || true" | |
| echo | |
| echo " uname -a:" | |
| run_cmd "uname -a || true" | |
| echo | |
| echo " /proc/cmdline:" | |
| run_cmd "cat /proc/cmdline || true" | |
| sep | |
| ################################################################ | |
| # Section: Boot & Integrity Chain | |
| ################################################################ | |
| note "Boot & Integrity Chain Checks" | |
| # dm-verity evidence (dmesg & cmdline & lsblk) | |
| if dmesg | grep -i -E 'dm-verity|verity' >/dev/null 2>&1; then | |
| ok "dm-verity messages present in dmesg (verity/verity hash evidence)" | |
| echo " dmesg verity lines:" | |
| dmesg | grep -i -E 'dm-verity|verity' | head -n 10 | |
| else | |
| warn "No dm-verity messages in dmesg" | |
| fi | |
| sep | |
| # root backing device and read-only root | |
| ROOT_MOUNT_LINE="$(mount | grep ' on / ' || true)" | |
| if echo "$ROOT_MOUNT_LINE" | grep -q ' ro,'; then | |
| ok "Root filesystem mounted read-only" | |
| echo " mount line: $ROOT_MOUNT_LINE" | |
| else | |
| warn "Root filesystem NOT mounted read-only (expected ro)" | |
| echo " mount line: $ROOT_MOUNT_LINE" | |
| fi | |
| sep | |
| # check /proc/cmdline for dm-verity, module.sig_enforce, firmware_class.path, loadpin | |
| CMDLINE="$(cat /proc/cmdline 2>/dev/null || true)" | |
| if [ -n "$CMDLINE" ]; then | |
| echo " /proc/cmdline: $CMDLINE" | |
| echo | |
| # module.sig_enforce | |
| if echo "$CMDLINE" | grep -q 'module.sig_enforce=1'; then | |
| ok "module.sig_enforce=1 present (unsigned kernel modules blocked)" | |
| else | |
| warn "module.sig_enforce not set to 1 (kernel modules may be allowed unsigned)" | |
| fi | |
| # loadpin | |
| if echo "$CMDLINE" | grep -q 'modules-load=loadpin_trigger\|loadpin'; then | |
| ok "loadpin is configured in cmdline (kernel code load restriction)" | |
| else | |
| # also check sysfs | |
| if [ -r /sys/kernel/security/loadpin/enabled ] && grep -q '^1$' /sys/kernel/security/loadpin/enabled; then | |
| ok "loadpin enabled in sysfs" | |
| else | |
| warn "loadpin not enabled" | |
| fi | |
| fi | |
| # firmware_class.path | |
| if echo "$CMDLINE" | grep -q 'firmware_class.path='; then | |
| ok "firmware_class.path restricted in kernel cmdline" | |
| else | |
| warn "firmware_class.path not restricted via kernel cmdline" | |
| fi | |
| # dm-verity present check in cmdline | |
| if echo "$CMDLINE" | grep -q 'verity\|dm-mod.create'; then | |
| ok "dm-verity appears in kernel cmdline" | |
| else | |
| warn "dm-verity not found in kernel cmdline (but dmesg earlier may show verity)" | |
| fi | |
| else | |
| warn "Could not read /proc/cmdline" | |
| fi | |
| sep | |
| # Lockdown | |
| if [ -r /sys/kernel/security/lockdown ]; then | |
| LOCKDOWN="$(cat /sys/kernel/security/lockdown 2>/dev/null || true)" | |
| if [ -n "$LOCKDOWN" ]; then | |
| ok "Kernel lockdown: $LOCKDOWN" | |
| else | |
| warn "Kernel lockdown present but empty" | |
| fi | |
| else | |
| warn "/sys/kernel/security/lockdown not present" | |
| fi | |
| sep | |
| # kexec disabled | |
| if [ -r /proc/sys/kernel/kexec_load_disabled ]; then | |
| KEXEC="$(cat /proc/sys/kernel/kexec_load_disabled 2>/dev/null || true)" | |
| if [ "$KEXEC" = "1" ]; then | |
| ok "kexec_load_disabled = 1 (kexec disabled)" | |
| else | |
| warn "kexec_load_disabled != 1 (kexec may be allowed)" | |
| fi | |
| else | |
| warn "/proc/sys/kernel/kexec_load_disabled not present" | |
| fi | |
| sep | |
| ################################################################ | |
| # Section: Kernel & runtime hardening | |
| ################################################################ | |
| note "Kernel & Runtime Hardening" | |
| # AppArmor | |
| if [ -r /sys/module/apparmor/parameters/enabled ]; then | |
| AA="$(cat /sys/module/apparmor/parameters/enabled 2>/dev/null || true)" | |
| if echo "$AA" | grep -qi 'Y'; then | |
| ok "AppArmor enabled" | |
| else | |
| warn "AppArmor not enabled" | |
| fi | |
| else | |
| info "AppArmor sysfs path not present (aa-status may still not be installed)" | |
| if command -v aa-status >/dev/null 2>&1; then | |
| aa-status --enabled 2>/dev/null && ok "AppArmor reports enabled" || warn "AppArmor reports disabled" | |
| fi | |
| fi | |
| sep | |
| # Seccomp: check /proc/1/status Seccomp | |
| if grep -q '^Seccomp:' /proc/1/status 2>/dev/null; then | |
| SC="$(awk '/^Seccomp:/ {print $2}' /proc/1/status 2>/dev/null || true)" | |
| if [ "$SC" = "2" ]; then | |
| ok "PID 1 seccomp status = 2 (strict seccomp)" | |
| else | |
| warn "PID 1 seccomp status = $SC (expected 2 for enforced seccomp)" | |
| fi | |
| else | |
| info "No Seccomp info for PID 1" | |
| fi | |
| sep | |
| # unprivileged_bpf_disabled | |
| if [ -r /proc/sys/kernel/unprivileged_bpf_disabled ]; then | |
| UBPF="$(cat /proc/sys/kernel/unprivileged_bpf_disabled 2>/dev/null || true)" | |
| if [ "$UBPF" = "1" ]; then | |
| ok "unprivileged_bpf_disabled = 1 (unprivileged eBPF disabled)" | |
| else | |
| warn "unprivileged_bpf_disabled != 1" | |
| fi | |
| else | |
| info "/proc/sys/kernel/unprivileged_bpf_disabled not present" | |
| fi | |
| sep | |
| # ASLR | |
| if [ -r /proc/sys/kernel/randomize_va_space ]; then | |
| ASLR="$(cat /proc/sys/kernel/randomize_va_space 2>/dev/null || true)" | |
| if [ "$ASLR" = "2" ]; then | |
| ok "ASLR randomize_va_space = 2 (full ASLR)" | |
| else | |
| warn "ASLR randomize_va_space = $ASLR (expected 2)" | |
| fi | |
| else | |
| warn "/proc/sys/kernel/randomize_va_space not readable" | |
| fi | |
| sep | |
| # CPU vulnerabilities | |
| if [ -d /sys/devices/system/cpu/vulnerabilities ]; then | |
| ok "CPU vulnerability files present; listing (first 20):" | |
| for f in /sys/devices/system/cpu/vulnerabilities/*; do | |
| printf " %s: " "$(basename "$f")" | |
| awk '{print substr($0,1,200)}' "$f" 2>/dev/null | sed -n '1p' | |
| done | sed -n '1,20p' | |
| else | |
| info "No CPU vulnerability directory at /sys/devices/system/cpu/vulnerabilities" | |
| fi | |
| sep | |
| # unprivileged_userns_clone | |
| if [ -r /proc/sys/kernel/unprivileged_userns_clone ]; then | |
| UUSERNS="$(cat /proc/sys/kernel/unprivileged_userns_clone 2>/dev/null || true)" | |
| if [ "$UUSERNS" = "0" ]; then | |
| ok "unprivileged_userns_clone = 0 (userns disabled)" | |
| else | |
| warn "unprivileged_userns_clone = $UUSERNS (recommended 0 to mitigate escapes)" | |
| fi | |
| else | |
| info "/proc/sys/kernel/unprivileged_userns_clone not present" | |
| fi | |
| sep | |
| ################################################################ | |
| # Section: Filesystem & storage controls | |
| ################################################################ | |
| note "Filesystem & Storage Checks" | |
| # Find writable in /usr /bin /sbin | |
| WIE="$(find /usr /bin /sbin -xdev -type f -writable 2>/dev/null | head -n 5 || true)" | |
| if [ -z "$WIE" ]; then | |
| ok "No writable files found under /usr, /bin, /sbin (first 5 checked)" | |
| else | |
| warn "Writable files found under system paths (first 5 shown):" | |
| echo "$WIE" | |
| fi | |
| sep | |
| # /tmp mount flags | |
| TMPLINE="$(mount | grep ' on /tmp ' || true)" | |
| if [ -n "$TMPLINE" ]; then | |
| if echo "$TMPLINE" | grep -qE 'noexec|nodev|nosuid'; then | |
| ok "/tmp mounted with recommended flags: $TMPLINE" | |
| else | |
| warn "/tmp mount lacks recommended flags (noexec,nodev,nosuid): $TMPLINE" | |
| fi | |
| else | |
| info "/tmp not separately mounted (check if tmpfs or not present)" | |
| fi | |
| sep | |
| # /var/log | |
| VLOG="$(mount | grep ' on /var/log ' || true)" | |
| if [ -n "$VLOG" ]; then | |
| ok "/var/log mount line: $VLOG" | |
| else | |
| info "/var/log not separately mounted; writable as expected for logging" | |
| fi | |
| sep | |
| # overlay mounts (containers) | |
| if mount | grep -i overlay >/dev/null 2>&1; then | |
| ok "Overlay mounts present (container runtime overlays detected). Example lines:" | |
| mount | grep -i overlay | head -n 5 | |
| else | |
| info "No overlay mounts detected" | |
| fi | |
| sep | |
| ################################################################ | |
| # Section: Identity, privilege & access | |
| ################################################################ | |
| note "Identity & Access Checks" | |
| # SSH presence and sshd_config (may be absent on COS) | |
| if command -v ss >/dev/null 2>&1; then | |
| echo " Open listening sockets (ss -tulpn):" | |
| ss -tulpn | sed -n '1,40p' | |
| else | |
| info "ss not present; skipping listening socket enumeration" | |
| fi | |
| sep | |
| if [ -r /etc/ssh/sshd_config ]; then | |
| echo " /etc/ssh/sshd_config (selected relevant lines):" | |
| grep -Ei 'PermitRootLogin|PasswordAuthentication|ChallengeResponseAuthentication|PubkeyAuthentication' /etc/ssh/sshd_config || true | |
| if grep -qEi 'PermitRootLogin\s+yes' /etc/ssh/sshd_config; then | |
| warn "PermitRootLogin is set to yes in sshd_config" | |
| else | |
| ok "SSH root login not enabled in sshd_config (or config absent/unreadable)" | |
| fi | |
| else | |
| info "No /etc/ssh/sshd_config found or unreadable (COS often doesn't run sshd)" | |
| fi | |
| sep | |
| # Sudoers | |
| if [ -r /etc/sudoers ] || [ -d /etc/sudoers.d ]; then | |
| echo " /etc/sudoers and /etc/sudoers.d (sanity list):" | |
| ( [ -r /etc/sudoers ] && sed -n '1,80p' /etc/sudoers ) || true | |
| ls -l /etc/sudoers.d 2>/dev/null || true | |
| else | |
| info "No sudoers present or not readable" | |
| fi | |
| sep | |
| # Root .ssh | |
| if [ -d /root/.ssh ]; then | |
| echo " root authorized_keys:" | |
| ls -l /root/.ssh || true | |
| [ -r /root/.ssh/authorized_keys ] && sed -n '1,20p' /root/.ssh/authorized_keys || true | |
| else | |
| info "/root/.ssh not present" | |
| fi | |
| sep | |
| ################################################################ | |
| # Section: Container runtime & image controls | |
| ################################################################ | |
| note "Container runtime (containerd) Checks" | |
| # Check for containerd binary / version | |
| if command -v containerd >/dev/null 2>&1; then | |
| CV="$(containerd --version 2>/dev/null | head -n1 || true)" | |
| ok "containerd present: $CV" | |
| else | |
| # try ctr | |
| if command -v ctr >/dev/null 2>&1; then | |
| CV="$(ctr version 2>/dev/null | head -n1 || true)" | |
| ok "containerd 'ctr' present: $CV" | |
| else | |
| warn "containerd/ctr not found" | |
| fi | |
| fi | |
| sep | |
| # list images (if ctr available) | |
| if command -v ctr >/dev/null 2>&1; then | |
| echo " ctr images ls (first 20):" | |
| ctr images ls | sed -n '1,20p' || true | |
| else | |
| info "ctr not available; cannot list container images locally" | |
| fi | |
| sep | |
| # containerd journal logs | |
| if command -v journalctl >/dev/null 2>&1; then | |
| echo " containerd journalctl (last 200 lines):" | |
| journalctl -u containerd --no-pager -n 200 2>/dev/null | sed -n '1,200p' || true | |
| else | |
| info "journalctl not available" | |
| fi | |
| sep | |
| # seccomp profile existence for containerd config | |
| if [ -r /etc/containerd/config.toml ]; then | |
| echo " /etc/containerd/config.toml (snippet):" | |
| sed -n '1,160p' /etc/containerd/config.toml || true | |
| else | |
| info "/etc/containerd/config.toml not present/readable" | |
| fi | |
| sep | |
| ################################################################ | |
| # Section: Logging & auditing | |
| ################################################################ | |
| note "Logging & Auditing Checks" | |
| # auditd presence | |
| if systemctl list-unit-files | grep -q '^auditd' 2>/dev/null; then | |
| if systemctl is-enabled auditd >/dev/null 2>&1; then | |
| ok "auditd service present and enabled" | |
| else | |
| warn "auditd present but not enabled" | |
| fi | |
| else | |
| info "auditd service not present (COS often relies on journald and cloud logging)" | |
| fi | |
| sep | |
| # journalctl - recent kernel errors | |
| if command -v journalctl >/dev/null 2>&1; then | |
| echo " Recent kernel log (dmesg tail):" | |
| dmesg | tail -n 40 | |
| else | |
| info "journalctl/dmesg may be unavailable" | |
| fi | |
| sep | |
| ################################################################ | |
| # Section: Virtualization & device isolation | |
| ################################################################ | |
| note "Virtualization & Device Isolation Checks" | |
| # hypervisor detection | |
| if dmesg | grep -i -E 'Hypervisor detected|hypervisor|kvm-clock|kvm' >/dev/null 2>&1; then | |
| echo " dmesg hypervisor lines:" | |
| dmesg | grep -i -E 'Hypervisor detected|kvm-clock|kvm|hypervisor' | head -n 20 | |
| ok "Hypervisor-related messages found" | |
| else | |
| info "No obvious hypervisor messages in dmesg (unexpected for a guest)" | |
| fi | |
| sep | |
| # /dev/kvm | |
| if [ -e /dev/kvm ]; then | |
| warn "/dev/kvm exists inside guest (nested virtualization or misconfiguration); device details:" | |
| ls -l /dev/kvm || true | |
| else | |
| ok "/dev/kvm absent (normal for guest without nested virtualization)" | |
| fi | |
| sep | |
| # lspci virtio devices | |
| if command -v lspci >/dev/null 2>&1; then | |
| if lspci | grep -Ei 'virtio|virtual|vmware|qemu|Red Hat, Inc.' >/dev/null 2>&1; then | |
| ok "Paravirtualized devices reported by lspci (virtio/Red Hat virtio)" | |
| lspci | grep -Ei 'virtio|virtual|vmware|qemu|Red Hat, Inc.' | sed -n '1,20p' | |
| else | |
| info "No virtio/vm vendor strings in lspci output" | |
| fi | |
| else | |
| info "lspci not available" | |
| fi | |
| sep | |
| # /proc/interrupts virtio lines | |
| if grep -E 'virtio|xen|vmw|hv_' /proc/interrupts >/dev/null 2>&1; then | |
| ok "VirtIO interrupts present (paravirtualized devices present)" | |
| grep -E 'virtio|xen|vmw|hv_' /proc/interrupts | sed -n '1,40p' | |
| else | |
| info "No virtio/xen/vmw/hv_ interrupts present" | |
| fi | |
| sep | |
| # check for passthrough devices (PCI with vendor that is not virtio) | |
| if command -v lspci >/dev/null 2>&1; then | |
| echo " Full lspci (first 40 lines):" | |
| lspci | sed -n '1,40p' | |
| else | |
| info "lspci not present" | |
| fi | |
| sep | |
| ################################################################ | |
| # Section: Optional & host isolation | |
| ################################################################ | |
| note "Optional: EFI vars, efivarfs, and host mounts" | |
| # efivars | |
| if mount | grep -q efivarfs; then | |
| ok "efivarfs mounted" | |
| mount | grep efivarfs | |
| else | |
| info "efivarfs not mounted" | |
| fi | |
| sep | |
| # /proc/xen and /sys/hypervisor | |
| [ -d /proc/xen ] && warn "/proc/xen exists (xen guest?)" || ok "/proc/xen absent" | |
| if [ -d /sys/hypervisor ]; then | |
| ok "/sys/hypervisor exists (virtualization hypervisor info available)" | |
| else | |
| info "/sys/hypervisor absent" | |
| fi | |
| sep | |
| ################################################################ | |
| # Section: Final summary hint lines | |
| ################################################################ | |
| note "Summary Hints (quick reading)" | |
| echo " - If many PASS entries in Boot & Integrity and Kernel Lockdown sections => guest is strongly protected against host-impacting tampering." | |
| echo " - If /dev/kvm present, or module.sig_enforce not set, or root is writable => escalate findings (host-risk)." | |
| echo " - Collect logs produced by this script as evidence and attach relevant dmesg/journal snippets." | |
| sep | |
| echo "Audit completed. Save the log file for reporting." | |
| # End | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment