435 lines
13 KiB
Bash
Executable File
435 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ©AngelaMos | 2026
|
|
# report_html.sh
|
|
|
|
html_escape() {
|
|
local s="$1"
|
|
s="${s//&/\&}"
|
|
s="${s//</\<}"
|
|
s="${s//>/\>}"
|
|
s="${s//\"/\"}"
|
|
printf '%s' "$s"
|
|
}
|
|
|
|
_html_score_color() {
|
|
local score="$1"
|
|
if [[ "$score" == "N/A" ]]; then
|
|
printf '%s' "#565f89"
|
|
return
|
|
fi
|
|
local int_score="${score%.*}"
|
|
if (( int_score >= 80 )); then
|
|
printf '%s' "#9ece6a"
|
|
elif (( int_score >= 60 )); then
|
|
printf '%s' "#e0af68"
|
|
else
|
|
printf '%s' "#f7768e"
|
|
fi
|
|
}
|
|
|
|
_html_status_class() {
|
|
local status="$1"
|
|
case "$status" in
|
|
"$STATUS_PASS") printf '%s' "pass" ;;
|
|
"$STATUS_FAIL") printf '%s' "fail" ;;
|
|
"$STATUS_WARN") printf '%s' "warn" ;;
|
|
"$STATUS_SKIP") printf '%s' "skip" ;;
|
|
esac
|
|
}
|
|
|
|
_emit_html_head() {
|
|
cat <<'CSSEOF'
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>CIS Benchmark Audit Report</title>
|
|
<style>
|
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
:root{
|
|
--bg:#1a1b26;--card:#24283b;--text:#c0caf5;--text-dim:#565f89;
|
|
--pass:#9ece6a;--fail:#f7768e;--warn:#e0af68;--skip:#565f89;
|
|
--border:#414868;--code-bg:#1a1b26;--accent:#7aa2f7;
|
|
}
|
|
html{font-size:16px;scroll-behavior:smooth}
|
|
body{
|
|
background:var(--bg);color:var(--text);
|
|
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;
|
|
line-height:1.6;padding:0;margin:0;
|
|
}
|
|
.container{max-width:1100px;margin:0 auto;padding:1.5rem}
|
|
header{text-align:center;padding:2.5rem 1rem 1.5rem;border-bottom:1px solid var(--border)}
|
|
header h1{font-size:1.75rem;font-weight:700;color:#c0caf5;margin-bottom:.5rem}
|
|
header .meta{font-size:.85rem;color:var(--text-dim);display:flex;flex-wrap:wrap;justify-content:center;gap:.5rem 1.5rem;margin-top:.5rem}
|
|
header .meta span{white-space:nowrap}
|
|
.dashboard{
|
|
display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
|
|
gap:1rem;margin:2rem 0;
|
|
}
|
|
.card{
|
|
background:var(--card);border:1px solid var(--border);border-radius:10px;
|
|
padding:1.25rem 1rem;text-align:center;
|
|
}
|
|
.card .label{font-size:.75rem;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:.35rem}
|
|
.card .value{font-size:1.85rem;font-weight:700}
|
|
.card.overall .value{font-size:2.5rem}
|
|
.card.pass .value{color:var(--pass)}
|
|
.card.fail .value{color:var(--fail)}
|
|
.card.warn .value{color:var(--warn)}
|
|
.card.skip .value{color:var(--skip)}
|
|
.card.total .value{color:var(--accent)}
|
|
.level-scores{
|
|
display:flex;justify-content:center;gap:2.5rem;margin-bottom:2rem;
|
|
flex-wrap:wrap;
|
|
}
|
|
.level-scores .ls{
|
|
background:var(--card);border:1px solid var(--border);border-radius:8px;
|
|
padding:.75rem 1.5rem;text-align:center;
|
|
}
|
|
.level-scores .ls .label{font-size:.75rem;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim)}
|
|
.level-scores .ls .val{font-size:1.35rem;font-weight:700}
|
|
h2{font-size:1.25rem;margin:2rem 0 1rem;padding-bottom:.5rem;border-bottom:1px solid var(--border)}
|
|
table{width:100%;border-collapse:collapse;margin-bottom:2rem;font-size:.9rem}
|
|
thead th{
|
|
text-align:left;padding:.6rem .75rem;border-bottom:2px solid var(--border);
|
|
font-size:.75rem;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);
|
|
}
|
|
thead th.num{text-align:center}
|
|
tbody td{padding:.55rem .75rem;border-bottom:1px solid var(--border)}
|
|
tbody td.num{text-align:center;font-variant-numeric:tabular-nums}
|
|
tbody td.pass{color:var(--pass)}
|
|
tbody td.fail{color:var(--fail)}
|
|
tbody td.warn{color:var(--warn)}
|
|
tbody td.skip{color:var(--skip)}
|
|
tbody td.score{font-weight:600}
|
|
.progress-bar{
|
|
background:var(--bg);border-radius:6px;height:10px;overflow:hidden;
|
|
min-width:80px;border:1px solid var(--border);
|
|
}
|
|
.progress-fill{height:100%;border-radius:6px;transition:width .3s ease}
|
|
.section-group{margin-bottom:1.5rem}
|
|
.section-group h3{font-size:1rem;color:var(--accent);margin-bottom:.75rem}
|
|
details.control{
|
|
background:var(--card);border:1px solid var(--border);border-radius:8px;
|
|
margin-bottom:.5rem;overflow:hidden;
|
|
}
|
|
details.control summary{
|
|
display:flex;align-items:center;gap:.65rem;padding:.65rem 1rem;
|
|
cursor:pointer;user-select:none;font-size:.9rem;list-style:none;
|
|
}
|
|
details.control summary::-webkit-details-marker{display:none}
|
|
details.control summary::before{
|
|
content:"";display:inline-block;width:0;height:0;
|
|
border-left:5px solid var(--text-dim);border-top:4px solid transparent;border-bottom:4px solid transparent;
|
|
transition:transform .2s ease;flex-shrink:0;
|
|
}
|
|
details.control[open] summary::before{transform:rotate(90deg)}
|
|
.badge{
|
|
display:inline-block;padding:.15em .6em;border-radius:999px;
|
|
font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;
|
|
line-height:1.4;flex-shrink:0;
|
|
}
|
|
.badge.pass{background:rgba(158,206,106,.15);color:var(--pass)}
|
|
.badge.fail{background:rgba(247,118,142,.15);color:var(--fail)}
|
|
.badge.warn{background:rgba(224,175,104,.15);color:var(--warn)}
|
|
.badge.skip{background:rgba(86,95,137,.2);color:var(--skip)}
|
|
.badge.level{background:rgba(122,162,247,.12);color:var(--accent);font-size:.65rem}
|
|
.control-id{font-family:"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",Menlo,monospace;font-size:.85rem;color:var(--text-dim);flex-shrink:0}
|
|
.control-title{flex:1;min-width:0}
|
|
.control-detail{
|
|
padding:.75rem 1rem 1rem 2.5rem;border-top:1px solid var(--border);
|
|
font-size:.85rem;
|
|
}
|
|
.control-detail .evidence{
|
|
background:var(--code-bg);border:1px solid var(--border);border-radius:6px;
|
|
padding:.6rem .85rem;margin-bottom:.65rem;
|
|
font-family:"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",Menlo,monospace;
|
|
font-size:.8rem;color:var(--text-dim);white-space:pre-wrap;word-break:break-word;
|
|
}
|
|
.control-detail .remediation{color:var(--warn)}
|
|
.control-detail .remediation code{
|
|
background:var(--code-bg);border:1px solid var(--border);border-radius:4px;
|
|
padding:.15em .4em;font-size:.8rem;
|
|
font-family:"SF Mono",SFMono-Regular,Consolas,"Liberation Mono",Menlo,monospace;
|
|
}
|
|
footer{
|
|
text-align:center;padding:2rem 1rem;margin-top:2rem;border-top:1px solid var(--border);
|
|
font-size:.8rem;color:var(--text-dim);
|
|
}
|
|
@media(max-width:640px){
|
|
.dashboard{grid-template-columns:repeat(2,1fr)}
|
|
.card.overall{grid-column:1/-1}
|
|
table{font-size:.8rem}
|
|
thead th,tbody td{padding:.4rem .5rem}
|
|
details.control summary{padding:.5rem .75rem;font-size:.82rem;gap:.45rem}
|
|
}
|
|
@media print{
|
|
:root{--bg:#fff;--card:#f8f8f8;--text:#1a1a1a;--text-dim:#666;--border:#ddd;--code-bg:#f0f0f0}
|
|
body{background:#fff;color:#1a1a1a}
|
|
.card{box-shadow:none;border:1px solid #ddd}
|
|
details.control{break-inside:avoid}
|
|
.badge.pass{background:#e8f5e9;color:#2e7d32}
|
|
.badge.fail{background:#ffebee;color:#c62828}
|
|
.badge.warn{background:#fff8e1;color:#f57f17}
|
|
.badge.skip{background:#f5f5f5;color:#757575}
|
|
}
|
|
</style>
|
|
</head>
|
|
CSSEOF
|
|
}
|
|
|
|
_emit_html_header() {
|
|
local timestamp="$1"
|
|
local hostname="$2"
|
|
|
|
local score_color
|
|
score_color=$(_html_score_color "$SCORE_OVERALL")
|
|
|
|
local total=$((TOTAL_PASS + TOTAL_FAIL + TOTAL_WARN + TOTAL_SKIP))
|
|
|
|
local l1_display
|
|
if [[ "$SCORE_LEVEL1" == "N/A" ]]; then
|
|
l1_display="N/A"
|
|
else
|
|
l1_display="${SCORE_LEVEL1}%"
|
|
fi
|
|
|
|
local l2_display
|
|
if [[ "$SCORE_LEVEL2" == "N/A" ]]; then
|
|
l2_display="N/A"
|
|
else
|
|
l2_display="${SCORE_LEVEL2}%"
|
|
fi
|
|
|
|
local l1_color
|
|
l1_color=$(_html_score_color "$SCORE_LEVEL1")
|
|
local l2_color
|
|
l2_color=$(_html_score_color "$SCORE_LEVEL2")
|
|
|
|
local esc_hostname
|
|
esc_hostname=$(html_escape "$hostname")
|
|
local esc_os
|
|
esc_os=$(html_escape "${DETECTED_ID} ${DETECTED_VERSION}")
|
|
local esc_bench
|
|
esc_bench=$(html_escape "$CIS_BENCHMARK")
|
|
|
|
cat <<HEADEREOF
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>CIS Benchmark Audit Report</h1>
|
|
<div class="meta">
|
|
<span>${esc_bench}</span>
|
|
<span>${esc_hostname}</span>
|
|
<span>OS: ${esc_os}</span>
|
|
<span>${timestamp}</span>
|
|
</div>
|
|
</header>
|
|
<section class="dashboard">
|
|
<div class="card overall">
|
|
<div class="label">Overall Score</div>
|
|
<div class="value" style="color:${score_color}">${SCORE_OVERALL}%</div>
|
|
</div>
|
|
<div class="card total">
|
|
<div class="label">Total</div>
|
|
<div class="value">${total}</div>
|
|
</div>
|
|
<div class="card pass">
|
|
<div class="label">Pass</div>
|
|
<div class="value">${TOTAL_PASS}</div>
|
|
</div>
|
|
<div class="card fail">
|
|
<div class="label">Fail</div>
|
|
<div class="value">${TOTAL_FAIL}</div>
|
|
</div>
|
|
<div class="card warn">
|
|
<div class="label">Warn</div>
|
|
<div class="value">${TOTAL_WARN}</div>
|
|
</div>
|
|
<div class="card skip">
|
|
<div class="label">Skip</div>
|
|
<div class="value">${TOTAL_SKIP}</div>
|
|
</div>
|
|
</section>
|
|
<div class="level-scores">
|
|
<div class="ls">
|
|
<div class="label">Level 1 Score</div>
|
|
<div class="val" style="color:${l1_color}">${l1_display}</div>
|
|
</div>
|
|
<div class="ls">
|
|
<div class="label">Level 2 Score</div>
|
|
<div class="val" style="color:${l2_color}">${l2_display}</div>
|
|
</div>
|
|
</div>
|
|
HEADEREOF
|
|
}
|
|
|
|
_emit_html_section_table() {
|
|
cat <<'TABLESTART'
|
|
<h2>Section Breakdown</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Section</th>
|
|
<th class="num">Pass</th>
|
|
<th class="num">Fail</th>
|
|
<th class="num">Warn</th>
|
|
<th class="num">Skip</th>
|
|
<th class="num">Score</th>
|
|
<th>Progress</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
TABLESTART
|
|
|
|
local section
|
|
for section in "${SECTION_ORDER[@]}"; do
|
|
local p="${SECTION_PASS[$section]:-0}"
|
|
local f="${SECTION_FAIL[$section]:-0}"
|
|
local w="${SECTION_WARN[$section]:-0}"
|
|
local s="${SECTION_SKIP[$section]:-0}"
|
|
local score="${SCORE_BY_SECTION[$section]:-N/A}"
|
|
|
|
local score_display
|
|
if [[ "$score" == "N/A" ]]; then
|
|
score_display="N/A"
|
|
else
|
|
score_display="${score}%"
|
|
fi
|
|
|
|
local bar_width
|
|
if [[ "$score" == "N/A" ]]; then
|
|
bar_width="0"
|
|
else
|
|
bar_width="$score"
|
|
fi
|
|
|
|
local bar_color
|
|
bar_color=$(_html_score_color "$score")
|
|
|
|
local esc_section
|
|
esc_section=$(html_escape "$section")
|
|
|
|
printf '<tr>\n'
|
|
printf '<td>%s</td>\n' "$esc_section"
|
|
printf '<td class="num pass">%d</td>\n' "$p"
|
|
printf '<td class="num fail">%d</td>\n' "$f"
|
|
printf '<td class="num warn">%d</td>\n' "$w"
|
|
printf '<td class="num skip">%d</td>\n' "$s"
|
|
printf '<td class="num score" style="color:%s">%s</td>\n' "$bar_color" "$score_display"
|
|
printf '<td><div class="progress-bar"><div class="progress-fill" style="width:%s%%;background:%s"></div></div></td>\n' "$bar_width" "$bar_color"
|
|
printf '</tr>\n'
|
|
done
|
|
|
|
cat <<'TABLEEND'
|
|
</tbody>
|
|
</table>
|
|
TABLEEND
|
|
}
|
|
|
|
_emit_html_details() {
|
|
printf '<h2>Detailed Results</h2>\n'
|
|
|
|
local section
|
|
for section in "${SECTION_ORDER[@]}"; do
|
|
local has_results=0
|
|
local id
|
|
for id in "${RESULT_ORDER[@]}"; do
|
|
if [[ "${CTRL_SECTION[$id]}" == "$section" ]]; then
|
|
has_results=1
|
|
break
|
|
fi
|
|
done
|
|
|
|
if (( has_results == 0 )); then
|
|
continue
|
|
fi
|
|
|
|
local esc_section
|
|
esc_section=$(html_escape "$section")
|
|
|
|
printf '<div class="section-group">\n'
|
|
printf '<h3>%s</h3>\n' "$esc_section"
|
|
|
|
for id in "${RESULT_ORDER[@]}"; do
|
|
if [[ "${CTRL_SECTION[$id]}" != "$section" ]]; then
|
|
continue
|
|
fi
|
|
|
|
local status="${RESULT_STATUS[$id]}"
|
|
local title="${CTRL_TITLE[$id]}"
|
|
local level="${CTRL_LEVEL[$id]}"
|
|
local evidence="${RESULT_EVIDENCE[$id]:-}"
|
|
local remediation="${CTRL_REMEDIATION[$id]:-}"
|
|
|
|
local cls
|
|
cls=$(_html_status_class "$status")
|
|
|
|
local esc_id
|
|
esc_id=$(html_escape "$id")
|
|
local esc_title
|
|
esc_title=$(html_escape "$title")
|
|
|
|
local open_attr=""
|
|
if [[ "$status" == "$STATUS_FAIL" ]]; then
|
|
open_attr=" open"
|
|
fi
|
|
|
|
printf '<details class="control %s"%s>\n' "$cls" "$open_attr"
|
|
printf '<summary>\n'
|
|
printf '<span class="badge %s">%s</span>\n' "$cls" "$status"
|
|
printf '<span class="control-id">%s</span>\n' "$esc_id"
|
|
printf '<span class="control-title">%s</span>\n' "$esc_title"
|
|
printf '<span class="badge level">L%s</span>\n' "$level"
|
|
printf '</summary>\n'
|
|
|
|
if [[ -n "$evidence" || -n "$remediation" ]]; then
|
|
printf '<div class="control-detail">\n'
|
|
if [[ -n "$evidence" ]]; then
|
|
local esc_evidence
|
|
esc_evidence=$(html_escape "$evidence")
|
|
printf '<div class="evidence">%s</div>\n' "$esc_evidence"
|
|
fi
|
|
if [[ -n "$remediation" ]]; then
|
|
local esc_remediation
|
|
esc_remediation=$(html_escape "$remediation")
|
|
printf '<p class="remediation">Remediation: <code>%s</code></p>\n' "$esc_remediation"
|
|
fi
|
|
printf '</div>\n'
|
|
fi
|
|
|
|
printf '</details>\n'
|
|
done
|
|
|
|
printf '</div>\n'
|
|
done
|
|
}
|
|
|
|
_emit_html_footer() {
|
|
local esc_version
|
|
esc_version=$(html_escape "$VERSION")
|
|
|
|
cat <<FOOTEREOF
|
|
<footer>
|
|
cisaudit v${esc_version}
|
|
</footer>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
FOOTEREOF
|
|
}
|
|
|
|
emit_html_report() {
|
|
local timestamp
|
|
timestamp=$(date '+%Y-%m-%d %H:%M:%S %Z')
|
|
local hostname
|
|
hostname=$(hostname 2>/dev/null || echo "unknown")
|
|
|
|
_emit_html_head
|
|
_emit_html_header "$timestamp" "$hostname"
|
|
_emit_html_section_table
|
|
_emit_html_details
|
|
_emit_html_footer
|
|
}
|