#!/bin/bash ################################################################################ # Script Name: openscap-audit.sh # Description: OpenSCAP / CIS Benchmark security audit script with HTML and # terminal reporting on Ubuntu/Debian and RHEL/Rocky/Alma/Fedora # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # Version: 1.0 # # Usage: # sudo ./openscap-audit.sh # sudo ./openscap-audit.sh --profile cis_server_l1 # sudo ./openscap-audit.sh --report /tmp/audit-report.html # sudo ./openscap-audit.sh --dry-run # ################################################################################ set -euo pipefail # ============================================================================ # DEFAULTS # ============================================================================ PROFILE="" REPORT_DIR="/var/lib/openscap/reports" REPORT_FILE="" RESULTS_FILE="" TAILORING_FILE="" REMEDIATE=false DRY_RUN=false LIST_PROFILES=false # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' # ============================================================================ # HELPER FUNCTIONS # ============================================================================ log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } log_step() { echo -e "${CYAN}[STEP]${NC} $*"; } show_usage() { cat </dev/null; then log_info "OpenSCAP already installed: $(oscap --version | head -1)" return 0 fi log_step "Installing OpenSCAP..." case "$OS_FAMILY" in debian) apt-get update -qq apt-get install -y -qq libopenscap8 openscap-utils ssg-debian ssg-base 2>/dev/null || \ apt-get install -y -qq libopenscap8 openscap-utils scap-security-guide 2>/dev/null || \ apt-get install -y -qq openscap-scanner openscap-utils scap-security-guide ;; rhel) dnf install -y -q openscap-scanner openscap-utils scap-security-guide ;; esac log_info "OpenSCAP installed: $(oscap --version | head -1)" } # ============================================================================ # CONTENT DETECTION # ============================================================================ find_content() { log_step "Finding SCAP content for $PRETTY_NAME..." local content_paths=( "/usr/share/xml/scap/ssg/content" "/usr/share/scap-security-guide" "/usr/share/openscap" ) SCAP_CONTENT="" # Build expected filename patterns based on OS local os_patterns=() case "$OS_ID" in ubuntu) os_patterns=("ssg-ubuntu${OS_VERSION}04-ds.xml" "ssg-ubuntu${OS_VERSION}-ds.xml" "ssg-ubuntu-ds.xml") ;; debian) os_patterns=("ssg-debian${OS_VERSION}-ds.xml" "ssg-debian-ds.xml") ;; rhel) os_patterns=("ssg-rhel${OS_VERSION}-ds.xml" "ssg-rhel-ds.xml") ;; centos) os_patterns=("ssg-centos${OS_VERSION}-ds.xml" "ssg-rhel${OS_VERSION}-ds.xml") ;; rocky) os_patterns=("ssg-rl${OS_VERSION}-ds.xml" "ssg-rhel${OS_VERSION}-ds.xml") ;; almalinux) os_patterns=("ssg-almalinux${OS_VERSION}-ds.xml" "ssg-rhel${OS_VERSION}-ds.xml") ;; fedora) os_patterns=("ssg-fedora-ds.xml") ;; esac for dir in "${content_paths[@]}"; do if [[ -d "$dir" ]]; then for pattern in "${os_patterns[@]}"; do if [[ -f "$dir/$pattern" ]]; then SCAP_CONTENT="$dir/$pattern" log_info "Found SCAP content: $SCAP_CONTENT" return 0 fi done fi done # Fallback: find any matching content SCAP_CONTENT=$(find /usr/share -name "ssg-*-ds.xml" -type f 2>/dev/null | head -1) if [[ -z "$SCAP_CONTENT" ]]; then log_error "No SCAP content found for $PRETTY_NAME" log_error "Install scap-security-guide package" exit 1 fi log_info "Using SCAP content: $SCAP_CONTENT" } list_available_profiles() { log_step "Available profiles for $PRETTY_NAME:" echo "" oscap info --profiles "$SCAP_CONTENT" 2>/dev/null | while IFS=: read -r id title; do printf " %-50s %s\n" "$id" "$title" done echo "" } select_profile() { if [[ -n "$PROFILE" ]]; then # Match partial profile names local full_profile full_profile=$(oscap info --profiles "$SCAP_CONTENT" 2>/dev/null | \ grep -i "$PROFILE" | head -1 | cut -d: -f1) if [[ -n "$full_profile" ]]; then PROFILE="$full_profile" log_info "Selected profile: $PROFILE" else log_error "Profile '$PROFILE' not found" log_info "Available profiles:" list_available_profiles exit 1 fi else # Auto-select: prefer CIS Level 1 Server, then STIG, then standard for pattern in "cis.*server.*l1\|cis_server_l1" "stig\b" "standard"; do PROFILE=$(oscap info --profiles "$SCAP_CONTENT" 2>/dev/null | \ grep -i "$pattern" | head -1 | cut -d: -f1) || true if [[ -n "$PROFILE" ]]; then break fi done if [[ -z "$PROFILE" ]]; then # Use first available profile PROFILE=$(oscap info --profiles "$SCAP_CONTENT" 2>/dev/null | head -1 | cut -d: -f1) fi log_info "Auto-selected profile: $PROFILE" fi } # ============================================================================ # AUDIT EXECUTION # ============================================================================ run_audit() { local timestamp timestamp=$(date +%Y%m%d_%H%M%S) mkdir -p "$REPORT_DIR" if [[ -z "$REPORT_FILE" ]]; then REPORT_FILE="$REPORT_DIR/openscap-report-${timestamp}.html" fi if [[ -z "$RESULTS_FILE" ]]; then RESULTS_FILE="$REPORT_DIR/openscap-results-${timestamp}.xml" fi log_step "Running OpenSCAP audit..." log_info "Profile: $PROFILE" log_info "Content: $SCAP_CONTENT" log_info "Report: $REPORT_FILE" log_info "Results: $RESULTS_FILE" echo "" local oscap_cmd=( oscap xccdf eval --profile "$PROFILE" --report "$REPORT_FILE" --results "$RESULTS_FILE" ) if [[ -n "$TAILORING_FILE" ]] && [[ -f "$TAILORING_FILE" ]]; then oscap_cmd+=(--tailoring-file "$TAILORING_FILE") fi if [[ "$REMEDIATE" == true ]]; then oscap_cmd+=(--remediate) log_warn "REMEDIATION ENABLED — changes will be applied!" fi oscap_cmd+=("$SCAP_CONTENT") # oscap returns exit code 2 for "some checks failed" — that's normal local exit_code=0 "${oscap_cmd[@]}" 2>&1 || exit_code=$? if [[ $exit_code -eq 0 ]]; then log_info "All checks passed" elif [[ $exit_code -eq 2 ]]; then log_warn "Some checks failed (this is normal for a first audit)" else log_error "OpenSCAP encountered an error (exit code: $exit_code)" fi } # ============================================================================ # RESULTS SUMMARY # ============================================================================ show_summary() { log_step "Audit Summary" echo "" if [[ -f "$RESULTS_FILE" ]]; then local pass fail error notapplicable notchecked total score pass=$(grep -c 'result="pass"' "$RESULTS_FILE" 2>/dev/null) || pass=0 fail=$(grep -c 'result="fail"' "$RESULTS_FILE" 2>/dev/null) || fail=0 error=$(grep -c 'result="error"' "$RESULTS_FILE" 2>/dev/null) || error=0 notapplicable=$(grep -c 'result="notapplicable"' "$RESULTS_FILE" 2>/dev/null) || notapplicable=0 notchecked=$(grep -c 'result="notchecked"' "$RESULTS_FILE" 2>/dev/null) || notchecked=0 total=$((pass + fail)) if [[ $total -gt 0 ]]; then score=$(echo "scale=1; $pass * 100 / $total" | bc) else score=0 fi echo -e " ${GREEN}Pass:${NC} $pass" echo -e " ${RED}Fail:${NC} $fail" echo -e " ${YELLOW}Error:${NC} $error" echo -e " Not Applicable: $notapplicable" echo -e " Not Checked: $notchecked" echo "" echo -e " ${CYAN}Compliance Score:${NC} ${score}%" echo "" # Show top failures if [[ $fail -gt 0 ]]; then log_info "Top failed rules (first 10):" grep -B2 'result="fail"' "$RESULTS_FILE" 2>/dev/null | \ grep 'idref=' | sed 's/.*idref="\([^"]*\)".*/ - \1/' | head -10 echo "" fi fi log_info "HTML report: $REPORT_FILE" log_info "XML results: $RESULTS_FILE" if [[ $fail -gt 0 ]]; then echo "" log_info "To auto-remediate failed checks (USE WITH CAUTION):" echo " $0 --profile '$PROFILE' --remediate" fi } # ============================================================================ # DRY RUN # ============================================================================ dry_run() { echo "" log_info "===== DRY RUN — No changes will be made =====" echo "" log_info "OS: $PRETTY_NAME" log_info "Content: $SCAP_CONTENT" log_info "Profile: $PROFILE" log_info "Report dir: $REPORT_DIR" log_info "Remediate: $REMEDIATE" echo "" log_info "Actions that would be performed:" echo " 1. Install OpenSCAP (if not present)" echo " 2. Find SCAP content for $OS_ID $OS_VERSION" echo " 3. Evaluate profile: $PROFILE" echo " 4. Generate HTML report in $REPORT_DIR/" echo " 5. Generate XCCDF results XML" if [[ "$REMEDIATE" == true ]]; then echo " 6. APPLY REMEDIATION for failed checks" fi echo "" } # ============================================================================ # MAIN # ============================================================================ main() { parse_args "$@" check_root detect_os install_openscap find_content if [[ "$LIST_PROFILES" == true ]]; then list_available_profiles exit 0 fi select_profile if [[ "$DRY_RUN" == true ]]; then dry_run exit 0 fi echo "" log_info "===== OpenSCAP Security Audit =====" echo "" run_audit show_summary echo "" log_info "===== Audit Complete =====" echo "" } main "$@"