⚠️ Not affiliated with, endorsed by, or sponsored by the Center for Internet Security (CIS). See DISCLAIMER.md. This project implements hardening aligned with the CIS Benchmarks™; it references CIS as a standard only.
English | 简体中文 | 日本語 | ภาษาไทย
part of the ohbs-* family: ohbs-image · ohbs-host · ohbs-cloud
oh baseline host
Repository / CLI / package:
ohbs-host· Full name: oh baseline host — part of the oh baseline (ohbs) family. Ansible playbooks and a local CLI that run the CIS security benchmarks against 10 Linux distributions and 4 Windows Server versions. Two modes —scan(read-only assessment) andapply(remediate) — each producing per-host HTML reports with structured audit logs. 4,400+ rules, zero third-party dependencies.
Table of Contents
- Quick Start
- Architecture
- Workflow
- Suites
- Audit Logging
- Reports
- Fine-Grained Execution
- Audit / Gate Mode
- Fleet Scan
- Drift Detection, Verification & Watch
- Tailoring, Waivers, and Dry-Run
- Export Formats
- Privilege Modes
- Directory Structure
- Notes
- Roadmap
- CIS Benchmarks Disclaimer
- License
Quick Start
Install from source (only supported method)
git clone https://github.com/susunola/ohbs-host.git
cd ohbs-host
pip install -e .
This installs the ohbs-host command. Note: this project is not published to
PyPI. The Ansible role directories (engines, rules catalogs, report
templates) are resolved relative to the source checkout at runtime, so an
editable install (-e .) from the cloned repo is required — a regular
pip install of a built wheel would not include them.
Run
# List supported OS presets
ohbs-host list
# L1 scan (read-only)
ohbs-host scan --os rhel9 --profile L1 --output output/
# L1 apply (remediate)
ohbs-host apply --os ubuntu2204 --profile L1 --output output/
# Audit / gate mode (exit non-zero on findings)
ohbs-host audit --os rhel9 --profile L1 --output output/
# Fleet scan across multiple hosts (local tagged mode by default)
ohbs-host fleet scan --os rhel9 --fleet-hosts web1,web2,db1 --output output/
# Tailor rule inputs and waive exceptions
ohbs-host scan --os rhel9 --variables '{"min_len": 14}' \
--waivers '{"1.1.1.1": "legacy app exception"}'
# Dry-run remediation
ohbs-host apply --os rhel9 --simulate
# Drift detection: compare two scan results (baseline vs latest)
ohbs-host diff output/result-before.json output/result-after.json --exit-code
# Periodic watch: scan every 6h, report only when configuration drifts
ohbs-host watch --os rhel9 --interval 21600 --alert-cmd "curl -fsS https://hooks.example.com/alert"
# Remediate: apply fixes only for rules that failed in a previous scan
ohbs-host remediate --os rhel9 --result output/result-scan-*.json
# Pack per-run evidence (result JSON, config, host facts, per-rule detail)
ohbs-host scan --os rhel9 --evidence-dir output/evidence
# L2 full apply + allow disruptive rules + audit log
ohbs-host apply --os tencentos4 --profile L2 --allow-disruptive \
--audit-log output/audit.log --output output/
# Scan only specific rules
ohbs-host scan --os sles15 --include "1.1.1,1.1.2,5.2" --output output/
--os values: tencentos3 tencentos4 · rhel8 rhel9 rhel10 · sles15 sles16 · ubuntu2004 ubuntu2204 ubuntu2404 · win2016 win2019 win2022 win2025
Configuration file
Create ohbs-host.toml in the working directory (or point to another path with --config / $OHBS_HOST_CONFIG):
[profile]
os = "rhel9"
profile = "L1"
platform = "server"
[rules]
# Accepts comma-separated strings or TOML arrays
include = []
exclude = []
sections = ["1.1", "5"]
families = []
[output]
format = "both" # html | cli | both
directory = "./output"
strict = false # exit non-zero when residual failures remain
[engine]
timeout = 600
allow_disruptive = false
backup_dir = ""
audit_log = ""
evidence_dir = "" # pack evidence tar.gz per run into this directory (empty = off)
# Tailor rule inputs (keys must match rule params)
[variables]
# min_len = 14
# Waive specific rules. Value may be a plain string or a table with an
# optional `expires` date (YYYY-MM-DD) — an expired waiver no longer exempts
# the rule, which is assessed normally and flagged as expired.
[waivers]
# "1.1.1.1" = "legacy app"
# "5.2.10" = { reason = "managed elsewhere", expires = "2026-12-31" }
# Notification — POST a JSON run summary after scan/audit/apply/remediate
[notify]
webhook_url = "" # empty = off
# Fleet scan defaults
[fleet]
hosts = []
remote = false
user = "root"
remote_engine = "/opt/ohbs-host/ohbs_engine.py"
CLI arguments always override config-file values.
Via Ansible
ansible-playbook -i ohbs-rhel9-ansible/inventory/hosts.ini ohbs-rhel9-ansible/scan.yml
ansible-playbook -i ohbs-rhel9-ansible/inventory/hosts.ini ohbs-rhel9-ansible/apply.yml \
-e cis_profile=L2 -e cis_allow_disruptive=true
Architecture
Each suite ships a single-file engine (Python 3 on Linux, PowerShell on Windows) with zero third-party dependencies. Ansible handles file transfer, remote execution, and Jinja2 report rendering. The engine produces result.json and an optional audit.log (JSON-lines) suitable for SIEM ingestion.
Workflow
scan (read-only)
- Preflight — validate target: Python 3.6+ (PowerShell 5.1+ on Windows), root/Administrator privileges.
- Push — copy engine + catalog (
rules.json,guidance.json,sections.json) to/tmp/cis-scan/(C:\Windows\Temp\cis-scanon Windows). - Run — engine iterates rules in
--mode scan, collects evidence, writesresult.json. Nothing is modified. - Fetch — pull
result.json(andaudit.logif enabled) back to control machine. - Report — Jinja2 renders
result.json+ host facts (hostname, IP, MAC, OS, kernel) into an HTML report.
apply (remediate)
Steps 1, 2, 4, 5 are identical. Step 3 differs:
- Engine runs in
--mode apply. For each failing rule in a remediable family: backs up the original file to/var/backups/cis-<os>/, applies the fix, then re-checks the rule to confirm. Rules requiring reboot/service restart are skipped unlesscis_allow_disruptive=true. Every action is recorded in the audit log.
Reports show the current assessment. If an apply run introduces regressions, the report surfaces them in a dedicated block.
Suites
| Suite | Benchmark | Engine | Rules |
|---|---|---|---|
ohbs-tencentos3-ansible/ |
CIS TencentOS Linux 3 v1.0.0 | Python 3 | 322 |
ohbs-tencentos4-ansible/ |
CIS TencentOS Linux 4 v1.0.0 | Python 3 | 275 |
ohbs-rhel8-ansible/ |
CIS RHEL 8 v4.0.0 | Python 3 | 322 |
ohbs-rhel9-ansible/ |
CIS RHEL 9 v2.0.0 | Python 3 | 297 |
ohbs-rhel10-ansible/ |
CIS RHEL 10 v1.0.1 | Python 3 | 328 |
ohbs-sles15-ansible/ |
CIS SLES 15 v2.0.1 | Python 3 | 286 |
ohbs-sles16-ansible/ |
CIS SLES 16 v1.0.0 | Python 3 | 336 |
ohbs-ubuntu2004-ansible/ |
CIS Ubuntu 20.04 LTS v3.0.0 | Python 3 | 312 |
ohbs-ubuntu2204-ansible/ |
CIS Ubuntu 22.04 LTS v3.0.0 | Python 3 | 306 |
ohbs-ubuntu2404-ansible/ |
CIS Ubuntu 24.04 LTS v2.0.0 | Python 3 | 332 |
ohbs-win2016-ansible/ |
CIS Windows Server 2016 v3.0.0 | PowerShell | 337 |
ohbs-win2019-ansible/ |
CIS Windows Server 2019 v3.0.0 | PowerShell | 338 |
ohbs-win2022-ansible/ |
CIS Windows Server 2022 v3.0.0 | PowerShell | 342 |
ohbs-win2025-ansible/ |
CIS Windows Server 2025 v2.1.0 | PowerShell | 360 |
Each suite is self-contained: its own inventory, group_vars, scan.yml / apply.yml, role tree, and templates.
Audit Logging
When --audit-log is set, the engine writes one JSON line per rule execution — append-safe, newline-delimited JSON, compatible with log aggregators, SIEM platforms, and compliance auditors.
| Field | Description |
|---|---|
ts |
ISO-8601 UTC timestamp with millisecond precision |
host |
Target hostname |
version |
Engine version |
mode |
scan or apply |
profile |
L1 or L2 |
rule |
CIS rule ID (e.g. 1.1.1.1) |
title |
Human-readable rule title |
status |
pass, fail, manual, error, notapplicable |
apply_status |
applied, already, skipped_disruptive, failed, n/a |
detail |
Evidence or remediation summary (truncated to 200 chars) |
duration_ms |
Execution time in milliseconds |
# Via CLI
python3 ohbs_cli.py scan --os rhel9 --audit-log output/audit-$(hostname).log
# Via Ansible
ansible-playbook ... -e cis_audit_log=/var/log/cis-audit.log
Reports
Two HTML reports are produced. Both are static, self-contained, print-ready, and load zero third-party assets.
| Report | Template | When rendered |
|---|---|---|
| Per-host | report.html.j2 |
Every run (scan or apply) |
| Fleet index | index.html.j2 |
Multi-host inventory, or cis_report_index=true |
A findings.csv.j2 template is also available — enable with cis_report_csv=true.
Per-host report — a single host's complete compliance posture:
- Score banner with traffic-light coloring (green ≥ 90, amber ≥ 70, red otherwise)
- Hardening Index — a weighted score that reflects the risk of each rule, so high-impact failures stand out
- System facts (hostname, IP, MAC, OS, kernel, arch, virtualization, uptime)
- Findings table with status, family, level, evidence, remediation hint, and CIS page reference
- Sort by rule ID or by priority (risk weight) for faster triage
- Filters by status / family / level / section
- Regression block in
applymode for rules that fail post-remediation re-check - Waived badge with reason when a rule is excepted via
--waiversor[waivers] - Before/After diff in apply mode showing each remediated rule's prior status
Fleet index — multi-host compliance dashboard:
- Aggregate pass percentage across all hosts
- Stat cards: to-fix, L1 fixed, L2 fixed, manual review, fix failed, host count
- Per-host score bar, pass/fail pills, applied counts, deep-link to per-host report
Fine-Grained Execution
| Parameter | Purpose |
|---|---|
--mode scan / --mode apply |
Read-only check / remediate |
--profile L1 / --profile L2 |
Baseline / defense-in-depth |
--include 1.1.1,1.1.2,5.2 |
Run only these rules |
--exclude 1.5,1.6 |
Skip these rules |
--sections 1,5 |
Run rules whose IDs start with these prefixes |
--families sysctl,kmod |
Run rules from these remediable families |
--audit-log audit.log |
Write structured audit trail |
When using Ansible, corresponding variables are in each suite's README under "Key Variables."
Audit / Gate Mode
ohbs-host audit is a strict, CI-friendly scan that exits non-zero when any rule fails or errors. It produces the same HTML/JSON reports as scan plus a concise audit-gate-<host>-<profile>.json artifact:
ohbs-host audit --os rhel9 --profile L1 --output output/
# exit 0 if fully compliant, exit 2 if failures remain
Fleet Scan
ohbs-host fleet scan repeats a scan across multiple hosts and aggregates the results into a single fleet HTML report and JSON file.
# Local tagged mode — runs on this machine, tags each result with the host name
ohbs-host fleet scan --os rhel9 --fleet-hosts web1,web2,db1
# Remote SSH mode — run engine on each host (configure [fleet] in ohbs-host.toml)
ohbs-host fleet scan --os rhel9 --fleet-remote
Remote mode requires the engine and catalog to already exist on the target hosts. See ohbs-host.toml.example for the [fleet] layout.
Drift Detection, Verification & Watch
ohbs-host diff compares two scan result JSONs and classifies every rule change — new failures (drift), regressions, recoveries, and waiver transitions — as a CLI summary and a self-contained HTML report. Use --exit-code in CI to fail the pipeline when drift appears:
ohbs-host diff output/result-2026-01-01.json output/result-2026-02-01.json --exit-code
Apply verification — after ohbs-host apply, a pre/post scan comparison reports which rules were actually fixed, which are still failing, and — critically — which rules regressed because the remediation itself broke them. A standalone verify-*.html report is written alongside the apply report.
ohbs-host watch runs a periodic scan loop with change-only, de-duplicated alerting (edge-triggered like Wazuh SCA): a rule is alerted when it starts failing and only again when it clears — a persistent failure never re-pages. Quiet runs print a single line; --json emits one machine-readable event per line for SIEM/automation:
ohbs-host watch --os rhel9 --interval 21600 --alert-cmd "curl -fsS https://hooks.example.com/alert"
ohbs-host watch --os rhel9 --interval 3600 --json # event stream
Waiver hygiene — waivers may carry approval metadata; expired or malformed entries are flagged at scan time, and a waiver whose rule id does not exist in the catalog (a silent no-op) is called out:
ohbs-host scan --os rhel9 --waivers '{"1.1.1.1": {"reason": "legacy app", "approved_by": "alice", "expires": "2026-12-31"}}'
Tailoring, Waivers, and Dry-Run
- Variables — override rule parameters without editing the catalog:
ohbs-host scan --os rhel9 --variables '{"min_len": 14}'
- Waivers — mark specific rules as excepted with an audit reason:
ohbs-host scan --os rhel9 --waivers '{"1.1.1.1": "legacy app exception"}'
Waivers may carry anexpiresdate ("5.2.10": {"reason": "...", "expires": "2026-12-31"}). Expired waivers no longer exempt the rule — it is assessed normally and flagged as[waiver expired]in the table/report. Useohbs-host audit --fail-on-expired-waiverto make an expired waiver fail the gate. - Simulate — dry-run apply mode that reports what would change without touching the system:
ohbs-host apply --os rhel9 --simulate
- Remediate — apply fixes only for the rules that failed in a previous scan:
ohbs-host remediate --os rhel9 --result output/result-scan-*.json
- Evidence snapshot — pack the result JSON, effective config, host facts, and
per-rule detail into
<hostname>-<timestamp>-evidence.tar.gz:ohbs-host scan --os rhel9 --evidence-dir output/evidence
- Webhook notify — POST a JSON run summary after
scan/audit/apply/remediate(fire-and-warn, never blocks the run):ohbs-host scan --os rhel9 --webhook https://hooks.example.com/hook
Export Formats
Result JSON can be converted to common compliance / CI formats:
python3 scripts/export_sarif.py output/result-scan-*.json output/findings.sarif
python3 scripts/export_xccdf.py output/result-scan-*.json output/findings.xccdf.xml
python3 scripts/export_junit.py output/result-scan-*.json output/findings.junit.xml
python3 scripts/export_prometheus.py output/result-scan-*.json
python3 scripts/export_pdf.py output/report-*.html output/report.pdf # requires `pip install -e .[pdf]`
python3 scripts/append_history.py output/result-scan-*.json history.jsonl
python3 scripts/plot_history.py history.jsonl output/history.html # SVG trend dashboard
python3 scripts/export_tailoring.py ohbs-host.toml tailoring.xml # XCCDF 1.2 tailoring export
python3 scripts/import_tailoring.py tailoring.xml ohbs-host-tailoring.toml # reverse import
Privilege Modes
apply requires root-equivalent privileges — UID 0 on Linux (via sudo or direct root) or Administrator on Windows.
Linux scan — coverage by privilege level
| Check family | Root | Non-root + caps¹ | Plain user |
|---|---|---|---|
| Packages, services, processes | ✅ | ✅ | ✅ |
| File perms (non-root files) | ✅ | ✅ | ✅ |
Kernel params (/proc/sys/) |
✅ | ✅² | ❌ |
| File perms (root-only files) | ✅ | ✅³ | ❌ |
SSH config (sshd -T) |
✅ | ✅⁴ | ❌ |
Audit rules (auditctl -l) |
✅ | ✅⁴ | ❌ |
| Sudoers, shadow, logs | ✅ | ✅³ | ❌ |
¹ Non-root + caps = regular user granted specific privileges via capability or sudo.
² Needs cap_sys_ptrace. ³ Needs cap_dac_read_search. ⁴ Needs sudo for the specific command.
Set up a non-root scan user (Linux):
# Option A — capability-based (persistent)
sudo setcap cap_sys_ptrace,cap_dac_read_search+ep $(which python3)
# Option B — sudo rules for specific commands
# /etc/sudoers.d/cis-scan
cis-scanner ALL=(ALL) NOPASSWD: /usr/sbin/sshd -T *
cis-scanner ALL=(ALL) NOPASSWD: /usr/sbin/auditctl -l
Windows
Scan works as non-Admin with RemoteSigned execution policy. Apply requires Administrator — use -RunAsAdministrator or Ansible become: true.
Directory Structure
ohbs-host/
├── ohbs_cli.py # Local CLI entrypoint (--os switches targets)
├── args.py # argparse declaration (all subcommands)
├── dispatch.py # Runtime orchestration & dispatch table
├── defaults.py # Hard-coded CLI defaults
├── commands_scan.py # scan/audit/apply/check commands
├── commands_watch.py # diff/watch commands
├── fleet.py # Fleet scan across multiple hosts
├── info.py # Rule detail lookup (info command)
├── report.py # HTML/CLI report rendering
├── engine.py # Engine subprocess invocation
├── display.py # CLI table/color output helpers
├── presets.py # OS_PRESETS registry
├── catalog.py # Rule catalog lookups
├── ohbs_host_diff.py # Drift detection / verification / watch logic
├── ohbs_host_config.py # ohbs-host.toml loader & merge
├── docs/
│ ├── architecture.svg
│ └── screenshots/
├── scripts/ # SARIF/XCCDF/JUnit/Prometheus exporters
├── tests/ # pytest suite (engine, CLI, drift, exporters)
├── ohbs-tencentos3-ansible/ # TencentOS 3
├── ohbs-tencentos4-ansible/ # TencentOS 4
├── ohbs-rhel8-ansible/ # RHEL 8
├── ohbs-rhel9-ansible/ # RHEL 9
├── ohbs-rhel10-ansible/ # RHEL 10
├── ohbs-sles15-ansible/ # SLES 15
├── ohbs-sles16-ansible/ # SLES 16
├── ohbs-ubuntu2004-ansible/ # Ubuntu 20.04 LTS
├── ohbs-ubuntu2204-ansible/ # Ubuntu 22.04 LTS
├── ohbs-ubuntu2404-ansible/ # Ubuntu 24.04 LTS
├── ohbs-win2016-ansible/ # Windows Server 2016
├── ohbs-win2019-ansible/ # Windows Server 2019
├── ohbs-win2022-ansible/ # Windows Server 2022
└── ohbs-win2025-ansible/ # Windows Server 2025
├── ansible.cfg
├── scan.yml | apply.yml | site.yml
├── inventory/ group_vars/
├── reports/ # HTML / JSON / CSV / audit output
└── roles/cis_<os>/
├── files/ engine, rules.json, guidance.json, sections.json
├── tasks/ preflight, run, report, gate
└── templates/ report.html.j2, index.html.j2, findings.csv.j2
Notes
applymodifies configuration files in place. Originals are backed up to/var/backups/cis-<os>/.- Rules marked
disruptive(reboot, remount, service restart) are skipped by default. Pass-e cis_allow_disruptive=trueto opt in. Run during a maintenance window. - Six families are intentionally never auto-remediated (require human judgment):
bootloader_password,info_only,manual,partition,root_access,sshd_access. The engine reports them but does not modify. - Linux engines auto-detect the package manager (dnf/yum, apt, zypper) and distro family at runtime.
- Before running
applyin production, runscanin a test environment, review the report, then proceed.
Roadmap
- CI pipeline for per-suite regression testing
ohbs-host diff— compare two scan results and show drift between runs ✅ (implemented)ohbs-host watch— periodic scan mode with alerting on new failures ✅ (implemented)- Molecule-based integration tests for every Linux role (
molecule testin each role directory) - macOS CIS benchmark support
CIS Benchmarks Disclaimer
Independent project — ohbs-host is not affiliated with, sponsored by, or endorsed by the Center for Internet Security (CIS).
License
Automation scripts are licensed under the MIT License. CIS Benchmark content is copyright © Center for Internet Security, Inc. and used under their terms of use.
Metadata
Release files for ohbs-host 1.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ohbs_host-1.3.1.tar.gz | 73.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ohbs_host-1.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 124.1 kB
Release files / ohbs_host-1.3.1.tar.gz
| Download URL | ohbs_host-1.3.1.tar.gz |
|---|---|
| Size | 73.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4712e58e3d9a8d3585eec0c86adde0f2fd0447218ed6115ed0abb7a81b72fe5e
|
|
BLAKE2b-256 checksum How to use checksums |
2f634f0c02f6ac5274648d3f3aa626d63e62595508e9fe9c8c5e6eceefec6c84
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.12
|
Release files / ohbs_host-1.3.1-py3-none-any.whl
| Download URL | ohbs_host-1.3.1-py3-none-any.whl |
|---|---|
| Size | 50.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cec36881dfc134d026b13677c9a2d65e94bb2d8d5f2022f2648bffd862d385ed
|
|
BLAKE2b-256 checksum How to use checksums |
b2c0e74e71776453b43218738d7490f20fa259f95ce50027514de4dadfee5bb7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.12
|