300 lines
8.6 KiB
Bash
Executable File
300 lines
8.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ©AngelaMos | 2026
|
|
# cisaudit.sh
|
|
#
|
|
# Main entry point for the CIS benchmark compliance auditor
|
|
#
|
|
# Parses CLI arguments (level filtering, output format, categories,
|
|
# baseline compare/save, test-root override, threshold), sources all
|
|
# library modules and check files, then dispatches through run_checks
|
|
# to execute registered controls, compute_scores for scoring, and
|
|
# generate_report for terminal/JSON/HTML output. Supports --list-controls
|
|
# for a dry-run control inventory. Exits non-zero when the overall score
|
|
# falls below --threshold.
|
|
#
|
|
# Connects to:
|
|
# lib/constants.sh - version, exit codes, ANSI colors, section names
|
|
# lib/utils.sh - logging, progress, environment detection helpers
|
|
# lib/registry.sh - control registration and result tracking
|
|
# lib/engine.sh - score computation (overall, per-section, per-level)
|
|
# lib/report_terminal.sh - ANSI terminal report renderer
|
|
# lib/report_json.sh - JSON report emitter
|
|
# lib/report_html.sh - standalone HTML report generator
|
|
# lib/baseline.sh - baseline save/load/diff
|
|
# controls/registry_data.sh - CIS control definitions (70+ register_control calls)
|
|
# checks/*.sh - per-section check implementations
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
declare -g QUIET="false"
|
|
declare -g OPT_LEVEL="all"
|
|
declare -g OPT_FORMAT="terminal"
|
|
declare -g OPT_OUTPUT=""
|
|
declare -g OPT_CATEGORIES="all"
|
|
declare -g OPT_BASELINE=""
|
|
declare -g OPT_SAVE_BASELINE=""
|
|
declare -g OPT_THRESHOLD=0
|
|
declare -g OPT_LIST_CONTROLS="false"
|
|
|
|
source "${SCRIPT_DIR}/lib/constants.sh"
|
|
source "${SCRIPT_DIR}/lib/utils.sh"
|
|
source "${SCRIPT_DIR}/lib/registry.sh"
|
|
source "${SCRIPT_DIR}/lib/engine.sh"
|
|
|
|
_source_if_exists() {
|
|
local path="$1"
|
|
if [[ -f "$path" ]]; then
|
|
source "$path"
|
|
fi
|
|
}
|
|
|
|
_source_if_exists "${SCRIPT_DIR}/lib/report_terminal.sh"
|
|
_source_if_exists "${SCRIPT_DIR}/lib/report_json.sh"
|
|
_source_if_exists "${SCRIPT_DIR}/lib/report_html.sh"
|
|
_source_if_exists "${SCRIPT_DIR}/lib/baseline.sh"
|
|
_source_if_exists "${SCRIPT_DIR}/controls/registry_data.sh"
|
|
|
|
for check_file in "${SCRIPT_DIR}"/checks/*.sh; do
|
|
[[ -f "$check_file" ]] && source "$check_file"
|
|
done
|
|
|
|
print_version() {
|
|
echo "cisaudit v${VERSION}"
|
|
}
|
|
|
|
print_help() {
|
|
echo -e "${BOLD}cisaudit${RESET} — CIS Benchmark Compliance Auditor for Linux"
|
|
echo ""
|
|
echo -e "${BOLD}USAGE${RESET}"
|
|
echo " cisaudit [OPTIONS]"
|
|
echo ""
|
|
echo -e "${BOLD}OPTIONS${RESET}"
|
|
echo " -l, --level LEVEL Benchmark level: 1, 2, or all (default: all)"
|
|
echo " -f, --format FORMAT Output: terminal, json, html (default: terminal)"
|
|
echo " -o, --output FILE Write report to file (default: stdout)"
|
|
echo " -c, --categories LIST Categories to audit: 1,2,3,4,5,6 (default: all)"
|
|
echo " -b, --baseline FILE Compare against a previous baseline JSON"
|
|
echo " -s, --save-baseline FILE Save results as a baseline JSON file"
|
|
echo " -t, --test-root DIR Use DIR as system root (for testing)"
|
|
echo " --threshold PCT Minimum pass % to exit 0 (default: 0)"
|
|
echo " --list-controls List all registered controls and exit"
|
|
echo " -q, --quiet Suppress progress output"
|
|
echo " -v, --version Print version and exit"
|
|
echo " -h, --help Print this help and exit"
|
|
echo ""
|
|
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
echo " sudo cisaudit"
|
|
echo " sudo cisaudit -l 1 -f json -o report.json"
|
|
echo " sudo cisaudit -c 5 -f terminal"
|
|
echo " cisaudit -t testdata/fixtures -f json"
|
|
echo " cisaudit --list-controls"
|
|
echo ""
|
|
echo -e "${BOLD}BENCHMARK${RESET}"
|
|
echo " ${CIS_BENCHMARK}"
|
|
}
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-l|--level)
|
|
OPT_LEVEL="$2"
|
|
shift 2
|
|
;;
|
|
-f|--format)
|
|
OPT_FORMAT="$2"
|
|
shift 2
|
|
;;
|
|
-o|--output)
|
|
OPT_OUTPUT="$2"
|
|
shift 2
|
|
;;
|
|
-c|--categories)
|
|
OPT_CATEGORIES="$2"
|
|
shift 2
|
|
;;
|
|
-b|--baseline)
|
|
OPT_BASELINE="$2"
|
|
shift 2
|
|
;;
|
|
-s|--save-baseline)
|
|
OPT_SAVE_BASELINE="$2"
|
|
shift 2
|
|
;;
|
|
-t|--test-root)
|
|
SYSROOT="${2%/}"
|
|
shift 2
|
|
;;
|
|
--threshold)
|
|
OPT_THRESHOLD="$2"
|
|
shift 2
|
|
;;
|
|
--list-controls)
|
|
OPT_LIST_CONTROLS="true"
|
|
shift
|
|
;;
|
|
-q|--quiet)
|
|
QUIET="true"
|
|
shift
|
|
;;
|
|
-v|--version)
|
|
print_version
|
|
exit "$EXIT_OK"
|
|
;;
|
|
-h|--help)
|
|
print_help
|
|
exit "$EXIT_OK"
|
|
;;
|
|
*)
|
|
fail "Unknown option: $1 (use --help for usage)"
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
list_controls() {
|
|
local id
|
|
printf "${BOLD}%-10s %-8s %-8s %-50s${RESET}\n" "ID" "Level" "Scored" "Title"
|
|
printf "%-10s %-8s %-8s %-50s\n" "----------" "--------" "--------" "--------------------------------------------------"
|
|
for id in "${REGISTERED_IDS[@]}"; do
|
|
printf "%-10s %-8s %-8s %-50s\n" \
|
|
"$id" \
|
|
"${CTRL_LEVEL[$id]}" \
|
|
"${CTRL_SCORED[$id]}" \
|
|
"${CTRL_TITLE[$id]}"
|
|
done
|
|
echo ""
|
|
echo "Total: ${#REGISTERED_IDS[@]} controls"
|
|
}
|
|
|
|
should_run_check() {
|
|
local id="$1"
|
|
|
|
if [[ "$OPT_LEVEL" != "all" && "${CTRL_LEVEL[$id]}" != "$OPT_LEVEL" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
if [[ "$OPT_CATEGORIES" != "all" ]]; then
|
|
local section_num="${id%%.*}"
|
|
if [[ ! ",$OPT_CATEGORIES," == *",$section_num,"* ]]; then
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
run_checks() {
|
|
local total="${#REGISTERED_IDS[@]}"
|
|
local count=0
|
|
local id fn
|
|
|
|
for id in "${REGISTERED_IDS[@]}"; do
|
|
((count++)) || true
|
|
fn="${CTRL_CHECK_FN[$id]}"
|
|
|
|
if ! should_run_check "$id"; then
|
|
continue
|
|
fi
|
|
|
|
progress "${count}/${total}" "${CTRL_TITLE[$id]}"
|
|
|
|
if declare -f "$fn" &>/dev/null; then
|
|
"$fn" || true
|
|
else
|
|
record_result "$id" "$STATUS_SKIP" "Check function ${fn} not implemented"
|
|
fi
|
|
done
|
|
|
|
clear_progress
|
|
}
|
|
|
|
generate_report() {
|
|
local output=""
|
|
|
|
case "$OPT_FORMAT" in
|
|
terminal)
|
|
if declare -f emit_terminal_report &>/dev/null; then
|
|
output=$(emit_terminal_report)
|
|
else
|
|
fail "Terminal reporter not loaded"
|
|
fi
|
|
;;
|
|
json)
|
|
if declare -f emit_json_report &>/dev/null; then
|
|
output=$(emit_json_report)
|
|
else
|
|
fail "JSON reporter not loaded"
|
|
fi
|
|
;;
|
|
html)
|
|
if declare -f emit_html_report &>/dev/null; then
|
|
output=$(emit_html_report)
|
|
else
|
|
fail "HTML reporter not loaded"
|
|
fi
|
|
;;
|
|
*)
|
|
fail "Unknown format: ${OPT_FORMAT} (use terminal, json, or html)"
|
|
;;
|
|
esac
|
|
|
|
if [[ -n "$OPT_OUTPUT" ]]; then
|
|
echo "$output" > "$OPT_OUTPUT"
|
|
info "Report written to ${OPT_OUTPUT}"
|
|
else
|
|
echo "$output"
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
check_bash_version
|
|
parse_args "$@"
|
|
|
|
if [[ "$OPT_LIST_CONTROLS" == "true" ]]; then
|
|
list_controls
|
|
exit "$EXIT_OK"
|
|
fi
|
|
|
|
detect_os
|
|
local is_root=0
|
|
check_root && is_root=1
|
|
|
|
info "cisaudit v${VERSION} — ${CIS_BENCHMARK}"
|
|
info "Target: ${SYSROOT} | OS: ${DETECTED_ID} ${DETECTED_VERSION} | Level: ${OPT_LEVEL}"
|
|
info "Controls: $(get_total_controls) registered"
|
|
[[ "$QUIET" == "true" ]] || echo "" >&2
|
|
|
|
run_checks
|
|
compute_scores
|
|
|
|
generate_report
|
|
|
|
if [[ -n "$OPT_SAVE_BASELINE" ]]; then
|
|
if declare -f save_baseline &>/dev/null; then
|
|
save_baseline "$OPT_SAVE_BASELINE"
|
|
info "Baseline saved to ${OPT_SAVE_BASELINE}"
|
|
else
|
|
warn "Baseline module not loaded"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$OPT_BASELINE" ]]; then
|
|
if declare -f diff_baseline &>/dev/null; then
|
|
diff_baseline "$OPT_BASELINE"
|
|
else
|
|
warn "Baseline module not loaded"
|
|
fi
|
|
fi
|
|
|
|
local score_int="${SCORE_OVERALL%.*}"
|
|
if (( score_int < OPT_THRESHOLD )); then
|
|
exit "$EXIT_FAIL"
|
|
fi
|
|
|
|
exit "$EXIT_OK"
|
|
}
|
|
|
|
main "$@"
|