MOVED => https://github.com/MVladislav/SCA
-
-
Save MVladislav/b186d7dc6f151301cdd7b3943993d47c to your computer and use it in GitHub Desktop.
A small Wazuh SCA script for run the yaml files with bash
Quick install for wazuh-regex
yq
and get an example yml for ubuntu 22-04
wget https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-manager/wazuh-manager_4.9.1-1_amd64.deb
mkdir wazuh-manager && dpkg-deb -R wazuh-manager_4.9.1-1_amd64.deb wazuh-manager
cp ./wazuh-manager/var/ossec/bin/wazuh-regex .
mkdir wazuh-lib && cp -r ./wazuh-manager/var/ossec/lib/* ./wazuh-lib
rm wazuh-manager* -rf
chmod u+x wazuh-regex
apt install yq jq
wget -O ./cis_ubuntu22-04.yml \
https://raw.githubusercontent.com/wazuh/wazuh/refs/heads/master/ruleset/sca/ubuntu/cis_ubuntu22-04.yml
# Run all rules in the file './cis_ubuntu22-04.yml'
./wazuh-sca.sh -pdc
# Get help info
./wazuh-sca.sh -h
# RUN rule by ID in './cis_ubuntu22-04.yml'
./wazuh-sca.sh -pdc -i <ID>
This script will possible not handle everything 100% correct, manually checks are needed and improves for script are welcome
This file contains 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 | |
# Color definitions | |
NC='\033[0m' # No Color - ${NC} | |
BRED='\033[1;31m' # Red - ${BRED} | |
BGREEN='\033[1;32m' # Green - ${BGREEN} | |
BYELLOW='\033[1;33m' # Yellow - ${BYELLOW} | |
BPURPLE='\033[1;35m' # Purple - ${BPURPLE} | |
BCYAN='\033[1;36m' # Cyan - ${BCYAN} | |
BBLUE='\033[1;34m' # Blue | |
# Define the path to wazuh-regex | |
WAZUH_REGEX_PATH="./wazuh-regex" | |
# If you have a local manual copy you need to define where the .so files are located | |
LD_LIBRARY_PATH=./wazuh-lib/:$LD_LIBRARY_PATH | |
# Path to the YAML file | |
YAML_FILE="./cis_ubuntu22-04.yml" | |
# Initialize pass, fail, and skip counters | |
total_count=0 | |
pass_count=0 | |
fail_count=0 | |
skip_count=0 | |
CIS_RULE_ID="" | |
LOG_PRINT_ACTUAL_OUTPUT=0 | |
LOG_PRINT_DETAIL_CHECK=0 | |
SKIP_OS_CHECK=0 | |
main() { | |
if [[ "$SKIP_OS_CHECK" == 0 ]]; then | |
# Validate each OS requirement | |
echo -e "${BYELLOW}Validating OS requirements...${NC}" | |
echo -e "${BYELLOW}#####################################################${NC}" | |
# shellcheck disable=SC2155 | |
local requirements=$(yq '.requirements.rules' "$YAML_FILE") | |
# shellcheck disable=SC2155 | |
local requirements_count=$(echo "$requirements" | jq 'length') | |
for ((i = 0; i < requirements_count; i++)); do | |
rule=$(echo "$requirements" | jq ".[${i}]") | |
id="OS Requirement $((i + 1))" | |
title="System Validation Requirement $((i + 1))" | |
echo -e "\n${BYELLOW}💡 Check #${id}: ${title}${NC}" | |
if run_check "$rule" "$id" "$title"; then | |
echo -e "${BGREEN}✔ Check passed for #${id}: ${title}${NC}" | |
((pass_count--)) | |
else | |
echo -e "${BRED}✖ Check failed for #${id}: ${title}${NC}" | |
((fail_count--)) | |
fi | |
((total_count--)) | |
done | |
fi | |
# Check if a specific ID was provided as an argument | |
if [[ -n "$CIS_RULE_ID" ]]; then | |
echo -e "\n${BYELLOW}Running check for ID: ${CIS_RULE_ID}${NC}" | |
echo -e "${BYELLOW}#####################################################${NC}" | |
# Find the index of the specific check with the given ID | |
# shellcheck disable=SC2155 | |
local index=$(echo "$all_checks" | jq "map(.id == $CIS_RULE_ID) | index(true)") | |
if [[ -z "$index" || "$index" == "null" ]]; then | |
echo -e "${BRED}\n🕵️ No rule found for ID: ${CIS_RULE_ID}${NC}" | |
exit 1 | |
fi | |
# Extract and run all rules for the specific check | |
extract_from_yaml "$index" | |
else | |
echo -e "\n${BYELLOW}Starting all configuration checks...${NC}" | |
echo -e "${BYELLOW}#####################################################${NC}" | |
# Loop through each check in the YAML file | |
# shellcheck disable=SC2155 | |
local check_count=$(echo "$all_checks" | jq 'length') | |
for ((index = 0; index < check_count; index++)); do | |
extract_from_yaml "$index" | |
done | |
fi | |
# Print final results | |
echo -e "\n${BYELLOW}All checks complete.${NC}" | |
echo -e "${BCYAN} ∞ Total Count Checks: $total_count${NC}" | |
echo -e "${BGREEN} ✔ Total Passed Checks: $pass_count${NC}" | |
echo -e "${BRED} ✖ Total Failed Checks: $fail_count${NC}" | |
echo -e "${BPURPLE} ↷ Total Skipped Checks: $skip_count${NC}" | |
} | |
# Function to parse and run a command, then check output against a regex | |
run_check() { | |
local rule="$1" | |
local id="$2" | |
local title="$3" | |
# Remove the leading and trailing double quotes | |
rule=${rule#\"} # Removes the first double quote | |
rule=${rule%\"} # Removes the last double quote | |
# Split rule into command and regex parts using '->' as the delimiter | |
local type_part_o="$rule" regex_part="" | |
if [[ "$rule" == *" -> "* ]]; then | |
type_part_o="${rule%% -> *}" | |
regex_part="${rule#* -> }" | |
fi | |
# Negate if rule starts with "not" | |
local negate=false | |
local type_part="$type_part_o" | |
if [[ "$type_part" == not* ]]; then | |
type_part=${type_part#not } # Remove "not " | |
negate=true | |
fi | |
# Define the command based on prefix | |
local output | |
case "$type_part" in | |
f:*) | |
if [[ -z "$regex_part" ]]; then | |
if [[ -e "${type_part#f:}" ]]; then | |
print_message success "$id" "$title" "$regex_part" "$type_part_o" "File does exist!" "$negate" | |
return $? | |
else | |
print_message failed "$id" "$title" "$regex_part" "$type_part_o" "File does not exist!" "$negate" | |
return $? | |
fi | |
fi | |
output=$(eval "cat ${type_part#f:}" 2>&1) | |
;; | |
c:*) | |
output=$(eval "${type_part#c:}" 2>&1) | |
;; | |
p:*) | |
# NOTE: not tested | |
local process_name="${type_part#p:}" | |
if pgrep -x "$process_name" >/dev/null; then | |
print_message success "$id" "$title" "$regex_part" "$type_part_o" "Process running" "$negate" | |
else | |
print_message failed "$id" "$title" "$regex_part" "$type_part_o" "Process not found" "$negate" | |
fi | |
print_message error "$id" "$title" "$regex_part" "$type_part_o" "Process type is current not handled" | |
return 2 | |
;; | |
d:*) | |
local dir_path="${type_part#d:}" | |
if [[ ! -d "$dir_path" ]]; then | |
print_message failed "$id" "$title" "$regex_part" "$type_part_o" "Directory not found" "$negate" | |
return $? | |
fi | |
if [[ -z "$regex_part" ]]; then | |
print_message success "$id" "$title" "$regex_part" "$type_part_o" "Directory exists" "$negate" | |
return $? | |
else | |
regex_part="${regex_part//\\\\/\\}" | |
# Check if regex_part contains an additional '->' for nested checks | |
if [[ "$regex_part" != *" -> "* ]]; then | |
# Single regex: Find files matching pattern in directory | |
local matching_files | |
matching_files=$(find "$dir_path" -type f | LD_LIBRARY_PATH="$LD_LIBRARY_PATH" "$WAZUH_REGEX_PATH" "${regex_part#r:}" 2>&1) | |
if [[ -n "$matching_files" ]]; then | |
print_message success "$id" "$title" "$regex_part" "$type_part_o" "Matching files found: $matching_files" "$negate" | |
return $? | |
else | |
print_message failed "$id" "$title" "$regex_part" "$type_part_o" "No matching files found in directory" "$negate" | |
return $? | |
fi | |
else | |
# Nested regex: Find files matching first pattern, then check contents | |
local file_regex_search="${regex_part%% -> *}" | |
local file_content_regex_search="${regex_part#* -> }" | |
matching_files=$(find "$dir_path" -type f | LD_LIBRARY_PATH="$LD_LIBRARY_PATH" "$WAZUH_REGEX_PATH" "${file_regex_search#r:}" 2>&1) | |
if [[ ${#matching_files[@]} -eq 0 ]]; then | |
print_message failed "$id" "$title" "$regex_part" "$type_part_o" "No matching files found for pattern '$file_regex_search'" "$negate" | |
return $? | |
fi | |
cleaned_files=$(echo "$matching_files" | awk '/^\+OSRegex_Execute:/ {print $2}' | sort -u 2>&1) | |
IFS=$'\n' read -rd '' -a cleaned_files_array <<<"$cleaned_files" | |
local success_file_search=0 | |
for file in "${cleaned_files_array[@]}"; do | |
local file_output | |
file_output=$(cat "$file" 2>&1) | |
if process_file_check "$file_content_regex_search" "$file_output"; then | |
success_file_search=1 | |
fi | |
done | |
if [[ $success_file_search == 1 ]]; then | |
print_message success "$id" "$title" "$file_content_regex_search" "$type_part_o" "File '$file' passed content checks" "$negate" | |
return $? | |
else | |
print_message failed "$id" "$title" "$file_content_regex_search" "$type_part_o" "File '$file' failed content checks" "$negate" | |
return $? | |
fi | |
fi | |
fi | |
;; | |
*) | |
print_message error "$id" "$title" "$regex_part" "$type_part_o" "Unknown command type in rule" | |
return $? | |
;; | |
esac | |
process_file_check "$regex_part" "$output" | |
process_file_check_status=$? | |
if [[ $process_file_check_status == 0 ]]; then | |
print_message success "$id" "$title" "$regex_part" "$type_part_o" "$output" "$negate" | |
return $? | |
elif [[ $process_file_check_status == 1 ]]; then | |
print_message failed "$id" "$title" "$regex_part" "$type_part_o" "$output" "$negate" | |
return $? | |
fi | |
return $process_file_check_status | |
} | |
process_file_check() { | |
local regex_part="$1" | |
local output="$2" | |
local success="${3:0}" | |
local output_result="$output" | |
# Check if command execution not fall into not found | |
if [[ -n "$output" && "$output" == *"command not found"* ]]; then | |
print_message error "$id" "$title" "$regex_part" "$type_part_o" "$output" | |
return $? | |
fi | |
# Handle regex patterns split by '&&' | |
local regex_array | |
IFS='&&' read -r -a regex_array <<<"$regex_part" | |
# Process each regex pattern | |
for regex in "${regex_array[@]}"; do | |
[[ -z "$regex" ]] && continue # Skip empty regex entries | |
regex="${regex#"${regex%%[![:space:]]*}"}" # Remove leading whitespace | |
regex="${regex%"${regex##*[![:space:]]}"}" # Remove trailing whitespace | |
regex="${regex//\\\\/\\}" # Clean up any escaped backslashes | |
success=0 | |
match_output="" | |
compare="" | |
verify="" | |
case "$regex" in | |
r:* | \!r:*) | |
regex="${regex//r:/}" # Remove 'r:' prefix | |
;; | |
n:* | \!n:*) | |
regex="${regex//n:/}" # Remove 'n:' prefix | |
compare=$(echo "$regex" | awk -F ' compare ' '{print $2}' | awk '{print $1}') | |
verify=$(echo "$regex" | awk -F ' compare ' '{print $2}' | awk '{print $2}') | |
regex=$(echo "$regex" | awk -F ' compare ' '{print $1}') | |
;; | |
*:*) | |
print_message error "$id" "$title" "$regex_part" "$type_part_o" "Unknown pattern in 'Expected pattern'" | |
return $? | |
;; | |
*) | |
print_message error "$id" "$title" "$regex_part" "$type_part_o" "Missing pattern in 'Expected pattern'" | |
return $? | |
;; | |
esac | |
match_output=$(echo "$output_result" | LD_LIBRARY_PATH="$LD_LIBRARY_PATH" "$WAZUH_REGEX_PATH" "$regex" 2>&1) | |
if [[ -n "$match_output" ]]; then | |
if [[ -n "$compare" && -n "$verify" ]]; then | |
if [[ "$match_output" =~ \-Substring:\ ([0-9]+) ]]; then | |
local captured_value="${BASH_REMATCH[1]}" # Extract captured number | |
if compare_values "$captured_value" "$compare" "$verify"; then | |
success=1 | |
fi | |
fi | |
else | |
output_result=$match_output | |
success=1 | |
fi | |
else | |
break | |
fi | |
done | |
# Output results | |
if [[ "$success" -eq 1 ]]; then | |
return 0 | |
else | |
return 1 | |
fi | |
} | |
extract_from_yaml() { | |
local index="$1" | |
local id cis_compliance title rules rules_count condition | |
# Extract multiple fields at once for efficiency | |
local check_data | |
check_data=$(echo "$all_checks" | jq ".[$index] | {id, cis_compliance: (.compliance[] | select(.cis) | .cis[0]), title, rules, rules_count: (.rules | length), condition}") | |
# Parse extracted data | |
id=$(echo "$check_data" | jq -r '.id') | |
cis_compliance=$(echo "$check_data" | jq -r '.cis_compliance') | |
title="${cis_compliance} :: $(echo "$check_data" | jq -r '.title')" | |
rules=$(echo "$check_data" | jq -r '.rules') | |
rules_count=$(echo "$rules" | jq -r 'length') | |
condition=$(echo "$check_data" | jq -r '.condition') | |
# Print message if there are no rules defined | |
if ((rules_count == 0)); then | |
echo -e "${BPURPLE}\n👎 No rule checks defined for ID: #${id}, Title: ${title}${NC}" | |
((skip_count++)) | |
return | |
fi | |
[[ "$LOG_PRINT_DETAIL_CHECK" == 1 ]] && echo -e "\n${BYELLOW}💡 Check #${id}: ${title}${NC}" | |
local rules_processed=0 rules_failed=0 rules_skipped=0 | |
for ((j = 0; j < rules_count; j++)); do | |
# shellcheck disable=SC2155 | |
local rule=$(echo "$rules" | jq ".[${j}]") | |
run_check "$rule" "$id" "$title" | |
case $? in | |
1) ((rules_failed++)) ;; | |
2) | |
((rules_skipped++)) | |
((rules_failed++)) | |
;; | |
esac | |
((rules_processed++)) | |
done | |
# Process condition outcomes | |
((total_count++)) | |
case "$condition" in | |
"all") | |
if ((rules_skipped > 0)); then | |
echo -e "${BPURPLE}↷ Check skipped for #${id}: ${title} (Condition: ${condition})${NC}" | |
((skip_count++)) | |
fi | |
if ((rules_failed > 0)); then | |
echo -e "${BRED}✖ Check failed for #${id}: ${title} (Condition: ${condition})${NC}" | |
((fail_count++)) | |
elif ((rules_failed == 0 && rules_processed > 0)); then | |
echo -e "${BGREEN}✔ Check passed for #${id}: ${title} (Condition: ${condition})${NC}" | |
((pass_count++)) | |
else | |
echo -e "${BPURPLE}↷ Check skipped for #${id}: ${title} (Condition: ${condition})${NC}" | |
((skip_count++)) | |
fi | |
;; | |
*) | |
if ((rules_skipped > 0)); then | |
echo -e "${BPURPLE}↷ Check skipped for #${id}: ${title} (Condition: ${condition})${NC}" | |
((skip_count++)) | |
fi | |
if ((rules_processed == rules_failed && rules_processed > 0)); then | |
echo -e "${BRED}✖ Check failed for #${id}: ${title} (Condition: ${condition})${NC}" | |
((fail_count++)) | |
elif ((rules_processed > rules_failed && rules_processed > 0)); then | |
echo -e "${BGREEN}✔ Check passed for #${id}: ${title} (Condition: ${condition})${NC}" | |
((pass_count++)) | |
else | |
echo -e "${BPURPLE}↷ Check skipped for #${id}: ${title} (Condition: ${condition})${NC}" | |
((skip_count++)) | |
fi | |
;; | |
esac | |
} | |
# Simplified comparison function | |
compare_values() { | |
local value="$1" | |
local operator="$2" | |
local target="$3" | |
case "$operator" in | |
">=") [[ "$value" -ge "$target" ]] ;; | |
"<=") [[ "$value" -le "$target" ]] ;; | |
">") [[ "$value" -gt "$target" ]] ;; | |
"<") [[ "$value" -lt "$target" ]] ;; | |
"==") [[ "$value" -eq "$target" ]] ;; | |
*) return 1 ;; | |
esac | |
} | |
# Helper function to print messages | |
print_message() { | |
local status="$1" | |
local id="$2" | |
local title="$3" | |
local expected="$4" | |
local command="$5" | |
local output="$6" | |
local negate="$7" | |
if [[ "$negate" == true && "$status" == "success" ]]; then | |
status="failed" | |
elif [[ "$negate" == true && "$status" == "failed" ]]; then | |
status="success" | |
fi | |
case "$status" in | |
success) | |
[[ $LOG_PRINT_DETAIL_CHECK -eq 1 ]] && echo -e "${BGREEN} ✔ Check passed for #${id}: ${title}${NC}" | |
[[ $LOG_PRINT_DETAIL_CHECK -eq 1 ]] && echo -e "${BYELLOW} - Expected pattern: '${expected}'${NC}" | |
[[ $LOG_PRINT_DETAIL_CHECK -eq 1 ]] && echo -e "${BYELLOW} - Command used : '${command}'${NC}" | |
[[ $LOG_PRINT_ACTUAL_OUTPUT -eq 1 && $LOG_PRINT_DETAIL_CHECK -eq 1 ]] && echo -e "${BYELLOW} - Actual output : '${output}'${NC}" | |
return 0 | |
;; | |
failed) | |
[[ $LOG_PRINT_DETAIL_CHECK -eq 1 ]] && echo -e "${BRED} ✖ Check failed for #${id}: ${title}${NC}" | |
[[ $LOG_PRINT_DETAIL_CHECK -eq 1 ]] && echo -e "${BYELLOW} - Expected pattern: '${expected}'${NC}" | |
[[ $LOG_PRINT_DETAIL_CHECK -eq 1 ]] && echo -e "${BYELLOW} - Command used : '${command}'${NC}" | |
[[ $LOG_PRINT_ACTUAL_OUTPUT -eq 1 && $LOG_PRINT_DETAIL_CHECK -eq 1 ]] && echo -e "${BYELLOW} - Actual output : '${output}'${NC}" | |
return 1 | |
;; | |
error) | |
echo -e "${BBLUE} 🍌 ERROR: for #${id}: ${title}${NC}" | |
echo -e "${BBLUE} - Expected pattern: '${expected}'${NC}" | |
echo -e "${BBLUE} - Command used : '${command}'${NC}" | |
echo -e "${BBLUE} - Actual output : '${output}'${NC}" | |
return 2 | |
;; | |
esac | |
} | |
# Function to show usage information | |
usage() { | |
echo "Usage: $0 [options]" | |
echo "Options:" | |
echo " -h, --help Show this help message and exit" | |
echo " -i, --id Define an ID to only check one specific rule" | |
echo " -f, --file ..." | |
echo " -wr, --wazuh-regex ..." | |
echo " -wl, --wazuh-libs ..." | |
echo " -pdc, --print-detail-check ..." | |
echo " -pao, --print-actual-output ..." | |
} | |
# Function to parse command-line arguments | |
parse_args() { | |
while [[ $# -gt 0 ]]; do | |
key="$1" | |
case $key in | |
-h | --help) | |
usage | |
exit 0 | |
;; | |
-i | --id) | |
CIS_RULE_ID="$2" | |
shift | |
;; | |
-f | --file) | |
YAML_FILE="$2" | |
shift | |
;; | |
-wr | --wazuh-regex) | |
WAZUH_REGEX_PATH="$2" | |
shift | |
;; | |
-wl | --wazuh-libs) | |
LD_LIBRARY_PATH="$2:$LD_LIBRARY_PATH" | |
shift | |
;; | |
-soc | --skip-os-check) | |
SKIP_OS_CHECK=1 | |
;; | |
-pdc | --print-detail-check) | |
LOG_PRINT_DETAIL_CHECK=1 | |
;; | |
-pao | --print-actual-output) | |
LOG_PRINT_ACTUAL_OUTPUT=1 | |
;; | |
*) | |
echo "Unknown option: $key" >&2 | |
usage | |
exit 1 | |
;; | |
esac | |
shift | |
done | |
} | |
echo "Starting script $0 ..." | |
# Parse command-line arguments | |
parse_args "$@" | |
# Check if YAML_FILE exists | |
if [[ ! -f "$YAML_FILE" ]]; then | |
echo -e "${BRED}YAML file not found at ${YAML_FILE}. Please verify the file path.${NC}" | |
exit 1 | |
fi | |
# Ensure yq is installed and executable | |
if ! command -v yq &>/dev/null; then | |
echo -e "${BRED}yq could not be found. Please install it to parse YAML files.${NC}" | |
echo -e "${BYELLOW}Install with 'sudo apt install yq'${NC}" | |
exit 1 | |
fi | |
# Ensure jq is installed and executable | |
if ! command -v jq &>/dev/null; then | |
echo -e "${BRED}jq could not be found. Please install it to parse JSON files.${NC}" | |
echo -e "${BYELLOW}Install with 'sudo apt install jq'${NC}" | |
exit 1 | |
fi | |
# Ensure wazuh-regex is installed and executable | |
if ! command -v "$WAZUH_REGEX_PATH" &>/dev/null; then | |
echo -e "${BRED}wazuh-regex could not be found at ${WAZUH_REGEX_PATH}. Please ensure it is accessible or update WAZUH_REGEX_PATH.${NC}" | |
echo -e "${BYELLOW}Example install manually as follow:${NC}" | |
echo -e "${BYELLOW} - 'wget https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-manager/wazuh-manager_4.9.1-1_amd64.deb'${NC}" | |
echo -e "${BYELLOW} - 'mkdir wazuh-manager && dpkg-deb -R wazuh-manager_4.9.1-1_amd64.deb wazuh-manager'${NC}" | |
echo -e "${BYELLOW} - 'cp ./wazuh-manager/var/ossec/bin/wazuh-regex .'${NC}" | |
echo -e "${BYELLOW} - 'mkdir wazuh-lib && cp -r ./wazuh-manager/var/ossec/lib/* ./wazuh-lib'${NC}" | |
echo -e "${BYELLOW} - 'rm wazuh-manager* -rf'${NC}" | |
echo -e "${BYELLOW} - 'chmod u+x wazuh-regex'${NC}" | |
exit 1 | |
fi | |
# Load all checks data once | |
all_checks=$(yq '.checks' "$YAML_FILE") | |
main | |
exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment