Part of the oh baseline hardening suite — 生死看淡,不服就干
Repository / CLI / package:
ohbs-cloud· Full name: oh baseline cloud — part of the oh baseline (ohbs) family. A plain Terraform implementation of five CIS Foundations Benchmarks across Tencent Cloud, AWS, Azure, GCP and Alibaba Cloud — 387 security recommendations covering Identity, Logging, Networking, Storage, Database and Kubernetes. Two modes, one codebase:scanfor read-only compliance assessment,applyfor enforcement. No Terraspace, no extra orchestrator — just the Terraform CLI wrapped in a thin Python layer (ohbs-cloud).
| Cloud | Benchmark | Controls |
|---|---|---|
| Tencent Cloud | CIS v1.0.0 | 91 |
| AWS | CIS v7.0.0 | 64 |
| Azure | CIS v6.0.0 | 70 |
| GCP | CIS v5.0.0 | 84 |
| Alibaba Cloud | CIS v2.0.0 | 78 |
All five benchmarks are fully implemented with scan + apply against their
native Terraform providers. Machine-readable control catalogs (derived from the
CIS Benchmarks, per CIS Terms of Use) are included under
benchmarks/; the benchmark PDFs themselves are not
redistributed here — see benchmarks/README.md.
- Capabilities
- Quick Start
- Project Layout
- Commands
- Filtering
- Sections and Stacks
- Scan
- Apply
- HTML Reports
- Control Registry
- Tests
- CI / Validation
- Limitations
- Requirements
| Capability | Count | Detail |
|---|---|---|
| Controls in benchmark | 91 | CIS Foundation v1.0.0 (2025-11-12) |
| Remediably by Terraform | 39 | cis apply can enforce these |
| Detectable by Terraform | 20 | cis scan can assess these |
| Manual / outside Terraform scope | 43 | Reported as MANUAL, never silently dropped |
A control can be both enforceable and detectable (e.g. 4.1 — COS bucket public
access), remediable but not detectable (e.g. 4.3 — COS logging: Terraform sets
it, the provider has no read-back data source), or detectable but not
remediable (e.g. 1.15 — CAM *:* policies: found by audit, but Terraform has
no business deleting a policy it didn't create).
Every selected control appears in the scan report — including the 43 unassessable
ones as MANUAL. A green table that silently drops half the benchmark is worse
than no report.
The machine-readable control catalogs (catalog.json) live under
benchmarks/<cloud>/. Catalogs are extracted from the
Summary Table + profile applicability of each CIS benchmark PDF by
tools/extract_benchmark.py (the PDFs are not redistributed in this repo —
obtain them from cisecurity.org under their Terms
of Use, then run the extractor locally).
| Cloud | Benchmark | Version | Controls | Rem. | Det. | Man. |
|---|---|---|---|---|---|---|
| Tencent Cloud | CIS Tencent Cloud Enterprise Foundation Benchmark | v1.0.0 | 91 | 39 | 20 | 43 |
| Amazon Web Services | CIS Amazon Web Services Foundations Benchmark | v7.0.0 | 64 | 8 | 9 | 50 |
| Microsoft Azure | CIS Microsoft Azure Foundations Benchmark | v6.0.0 | 70 | 11 | 16 | 48 |
| Google Cloud Platform | CIS Google Cloud Platform Foundation Benchmark | v5.0.0 | 84 | 4 | 31 | 49 |
| Alibaba Cloud | CIS Alibaba Cloud Foundation Benchmark | v2.0.0 | 78 | 9 | 13 | 57 |
Rem. = remediable (cis apply), Det. = detectable (cis scan), Man. = reported
as MANUAL because the provider has no enumerable data source or no non-destructive
resource. Every registry is generated by tools/generate_controls.py --cloud NAME
from the catalogs under benchmarks/; the aws, alibaba, gcp and azure
catalogs carry an extra group field on three-level controls. Benchmark PDFs are
© The Center for Internet Security, Inc.
pip install ohbs-cloud # CLI + all five control registries + Terraform stacks
export TENCENTCLOUD_SECRET_ID=<your-secret-id>
export TENCENTCLOUD_SECRET_KEY=<your-secret-key>
export TENCENTCLOUD_REGION=ap-guangzhouSanity check (no cloud, no credentials):
ohbs-cloud list # prints the control registry
pytest test_py -q # 310 tests, offline, no cloud/credentialsFirst scan:
ohbs-cloud scan --profile level1 # table to stdout
ohbs-cloud scan --section 4 --format html -o rpt.html # self-contained HTMLFirst enforcement (dry-run first):
ohbs-cloud apply --tag cos --dry-run # preview
ohbs-cloud apply --tag cos --report # enforce + HTML recordOther clouds (pick with --cloud or CIS_CLOUD):
export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_DEFAULT_REGION=us-east-1
ohbs-cloud --cloud aws list # 64 AWS controls
ohbs-cloud --cloud aws scan --section 6 --format html -o aws-scan.html
ohbs-cloud --cloud aws apply --only 2.8,2.9,6.1.1 --dry-run # preview
export ARM_SUBSCRIPTION_ID=... ARM_TENANT_ID=... # azure: + az login
ohbs-cloud --cloud azure scan --only 9.3.6 --format html -o azure-scan.html
export GOOGLE_APPLICATION_CREDENTIALS=sa.json # gcp
ohbs-cloud --cloud gcp scan --section 6 --format html -o gcp-scan.html
export ALICLOUD_ACCESS_KEY=... ALICLOUD_SECRET_KEY=... ALICLOUD_REGION=cn-hangzhou
ohbs-cloud --cloud alibaba scan --only 5.1 --format html -o alibaba-scan.htmlWhat a cloud can assess is limited by its provider's data sources: azure is
name-based (you list the resources), aws/alibaba/gcp expose list sources in
different areas. Controls the provider cannot observe are reported as MANUAL.
Per-stack import / inventory instructions live in each stacks/<cloud>/ stack.
ohbs_cloud/ Python package (pip install ohbs-cloud)
cli.py CLI entry point (--cloud tencent|aws|azure|gcp|alibaba)
runner.py Runner: shells out to `terraform`
catalog.py, selector.py, reporter.py, control.py, severity.py,
suppress.py, compliance.py, tfcheck.py,
remediation.py, drift.py, schema.py, mcp.py
data/
config/controls.yml Tencent registry — 91 entries
config/<cloud>/controls.yml Per-cloud registries (aws 64, azure 70, gcp 84, alibaba 78)
config/remediation.yml Derived remediation guidance (cloud + control id/glob)
modules/ Reusable Terraform modules
security_group_baseline/
cos_secure_bucket/
cls_audit_alarm/
stacks/
audit/ Read-only: data sources + check blocks, zero resources
iam/ \
logging/ |
network/ | Six hardening stacks (self-contained root modules)
storage/ |
database/ |
kubernetes/ /
benchmarks/ Extracted control catalogs per cloud (PDFs not redistributed)
tencent/catalog.json Tencent Cloud (91 controls) - feeds controls.yml
aws/catalog.json AWS v7.0.0 (64 controls) - feeds config/aws/controls.yml
azure/catalog.json Azure v6.0.0 (70 controls) - feeds config/azure/controls.yml
gcp/catalog.json GCP v5.0.0 (84 controls) - feeds config/gcp/controls.yml
alibaba/catalog.json Alibaba Cloud v2.0.0 (78 controls) - feeds config/alibaba/controls.yml
test_py/ 310 tests — no cloud, no credentials
scripts/
e2e_test.py Real-command E2E (offline no-creds + optional live account)
tools/
extract_benchmark.py Extract <cloud>/catalog.json from a CIS benchmark PDF text
generate_controls.py Generate config/controls.yml from the Tencent catalog
validate.sh terraform init+validate every stack/module offline
docs/
sample-scan.html Example scan report (static)
sample-hardening.html Example hardening report (static)
Each stack under stacks/ is a self-contained root module: it carries its
own provider.tf and backend.tf, so terraform -chdir=stacks/<name> ...
works standalone. State defaults to a per-stack terraform.tfstate; see
stacks/<name>/backend.tf for the COS (tencent) / S3 (aws) remote-backend
guidance. Later clouds nest under stacks/<cloud>/<name>.
cis --cloud aws list Show the registry for a cloud (default: tencent)
cis scan Read-only assessment of selected controls
cis plan Show what cis apply would change
cis apply Enforce selected controls
cis destroy STACK Roll back one hardening stack
cis compliance --dir scans Aggregate per-cloud scan JSONs into one posture
cis check --tf DIR Pre-deploy CIS checks on Terraform definitions
cis diff BASE CUR Compare two scan JSONs (new/still/fixed/dropped)
cis check-drift [BASE CUR | --baseline FILE] Flag regressions vs a baseline
cis batch --accounts a,b Scan several accounts and aggregate
cis mcp MCP JSON-RPC server over stdio (agent/LLM hosts)
- Cross-cloud compliance (
cis compliance --dir scans, Prowler-style): save each cloud's scan withcis --cloud X scan --format json -o scans/X.json, then aggregate all of them into one posture - per-cloud cards, global tally and the failing-control list ordered by severity. - Severity (Prowler-style): every finding carries a risk level (critical/high/medium/low) inferred from the control's tags, shown in all report formats and used to order the compliance failing list.
- Risk score (Prowler ThreatScore-style): each finding also carries a
numeric
score(critical=100…low=10) and every scan/compliance report shows a weightedrisk_scoretotal for FAIL findings, so posture can be tracked as a single number over time. - Structured resource (Cartography/Steampipe-style): each finding carries a
resourcefield (the specific bucket/instance/policy that failed) when the source emits one, shown in scan reports and used as a cleaner suppression target. - Suppression (
config/suppress.yml, CloudSploit-style): declare known exceptions per cloud+control+resource; suppressed findings render as SUPPRESSED and never trip the scan gate. - CI formats:
--format csv/--format junitfor pipelines;--format sarifuploads to GitHub Code Scanning (see the CI workflow).scanexits 1 when any assessed control FAILs (suppressed findings excluded). - IaC pre-deploy checks (
cis check --tf DIR, Steampipe-style): parse .tf files (no credentials, no terraform run) and verify the arguments CIS requires are present, per cloud. Catches the missingenable_log_file_validationbefore you apply -cis scanstill covers what runs on the live cloud. Extra rules can be merged in with--checks FILE(custom checks), andplan --plan-checkruns the static gate before any apply. A custom rule may also carrytitle/severity/remediation/frameworkmetadata so your own policies read as first-class controls (policy-as-code), not just resource/arg checks. - Baseline drift (
cis diff BASE CUR): compare two scan JSONs and see what regressed, what stayed failing, and what you fixed - a lightweight foundation for continuous monitoring. - Drift detection (
cis check-drift): continuous-monitoring check. Runohbs-cloud scan --format json -o scans/base.jsononce to record a baseline, thenohbs-cloud check-drift --baseline scans/base.jsonon a schedule (cron / CI) to flag only regressions — controls now FAILing that were not before. Also works offline:ohbs-cloud check-drift BASE CUR. Exits 1 on any regression so CI can gate. - Remediation guidance: every scan finding carries a
remediationhint (derived fromconfig/remediation.yml, keyed by cloud + control id or glob, with a generic capability fallback). Shown in--format json/markdownand in the HTML scan report; seeohbs_cloud/remediation.py. - Multi-framework view (
--framework nist|pci|djcp): view the control set through another compliance lens (NIST SP 800-53, PCI DSS v4.0, 等保 2.0). - Multi-account batch (
cis batch --accounts a,b,c): scan each account and roll them up into one cross-account posture;scan --push DIRwrites a timestamped JSON copy for the same purpose.
Every finding — from a live scan, an IaC check, or a compliance
aggregate — carries the same stable set of keys (ohbs_cloud/schema.py):
id, title, status, severity, score, evidence, evidence_detail,
resource, remediation. Downstream consumers can rely on the shape without
special-casing each command.
ohbs-cloud mcp exposes the read-only tools over a stdio JSON-RPC exchange so
an agent / LLM host can drive assessments (Prowler-style MCP surface). It
understands tools/list (the registry) and tools/call for list, scan
(dry-run), plan (dry-run), diff and check_drift. Requests are
newline-delimited JSON, responses are JSON-RPC 2.0:
| Code | Meaning |
|---|---|
0 |
Clean, or nothing to do |
1 |
Scan/diff/batch found at least one failing or new control |
2 |
Run broke — bad flags, empty selection, terraform failed |
1 is reserved for findings so CI can gate on it. MANUAL rows never produce a
1: "could not check" is not "broken."
| Flag | Meaning |
|---|---|
--format table|json|markdown|html |
Default table; also csv, junit, sarif |
-o, --output PATH |
Write list/scan report to file |
--push DIR |
Also write a timestamped JSON scan result into DIR |
--report [PATH] |
After apply, write HTML hardening report |
--dry-run |
Print terraform commands, execute nothing |
--verbose |
Echo each terraform invocation |
--no-color |
Disable ANSI colour |
With --format json, all narration goes to stderr — cis scan --format json | jq is always safe.
Every filter is available both as a flag and as an environment variable. They compose.
| Flag | Variable | Meaning |
|---|---|---|
--only 3.5,4.* |
CIS_ONLY |
Exactly these ids/globs. Replaces the enabled: baseline |
--exclude 4.6 |
CIS_EXCLUDE |
Drop these ids/globs. Applied last, always wins |
--section 3,4 |
CIS_SECTIONS |
Restrict to these benchmark sections |
--tag cos,mfa |
CIS_TAGS |
Keep controls carrying any of these tags |
--profile level1 |
CIS_PROFILE |
level1 (67 controls) or level2 (all 91) |
--framework pci |
CIS_FRAMEWORK |
Keep controls mapped to nist, pci or djcp |
Precedence: --only replaces the baseline; --section, --tag and
--profile narrow whatever baseline is in play; --exclude is applied last and
beats everything.
A filter that matches nothing is a hard error:
$ ohbs-cloud scan --only 12.7
error: filter "12.7" matches no control in the benchmark
$ echo $?
2CLI flags overwrite pre-existing CIS_* variables. The resolved selection is
exported to the terraform invocations — ohbs-cloud, the stack filtering and the
enabled_controls variable all resolve to the same answer.
| § | Area | Controls | Remediable | Stack |
|---|---|---|---|---|
| 1 | Identity and Access Management | 16 | 1 | iam |
| 2 | Logging and Monitoring | 20 | 17 | logging (16), network (2.4) |
| 3 | Networking | 7 | 6 | network |
| 4 | Storage | 9 | 6 | storage |
| 5 | TencentDB for MySQL | 6 | 6 | database |
| 6 | Kubernetes Engine | 9 | 3 | kubernetes |
| 7 | Cloud Security Center | 6 | 0 | — (manual) |
| 8 | Cloud Workload Protection | 6 | 0 | — (manual) |
| 9 | Container Security Service | 12 | 0 | — (manual) |
2.4 (VPC flow logs) lives in the network stack — stacks are grouped by the
resource they touch, not by section number.
Stacks always run in fixed order for reproducibility: iam, logging, network,
storage, database, kubernetes.
Deploys the audit stack (zero managed resources — data sources, check blocks
and outputs only), reads back cis_findings, renders the result.
Covers the 20 detectable controls:
1.15 1.16 2.1 2.2 2.3 2.20 3.1 3.3 3.4 3.5 3.6
4.1 4.2 4.8 4.9 5.2 6.8 6.9 8.1 8.2
$ ohbs-cloud scan --profile level1 --dry-run
Scanning 15 control(s) via the audit stack (read-only).
Will scan:
terraform -chdir=stacks/audit apply -auto-approve # 1.15, 1.16, 2.1, 2.2, 2.3, 3.3, 3.4, 4.1, 4.2, 4.8, 4.9, 5.2, 6.8, 6.9, 8.1| Status | Meaning |
|---|---|
PASS |
Assessed, compliant |
FAIL |
Assessed, non-compliant — exit 1 |
SKIPPED |
Enforced by apply but not readable back |
MANUAL |
Outside Terraform scope; verify in console |
Selecting only manual controls is valid:
$ ohbs-cloud scan --section 9 --no-color
No selected control is machine-assessable by the provider.
Selected: 12. Use cis list to see why.
STATUS ID TITLE EVIDENCE
------------------------------------------------------------------------------------
MANUAL 9.1 Ensure Container Security protection is enabled ... verify in console
...
FAIL 0 PASS 0 MANUAL 12 SKIPPED 0Runs one hardening stack at a time — each is a separate terraform apply — so
output is readable and every failure is attributable to a stack. A failing stack
stops the run.
$ ohbs-cloud apply --tag cos --exclude 4.6 --dry-run
Selection: 9/91 controls (remediable 8, detectable 3, manual 0)
Will apply:
terraform -chdir=stacks/logging apply # 2.2, 2.13, 2.18
terraform -chdir=stacks/storage apply # 4.1, 4.3, 4.4, 4.5, 4.7Records what was enforced per stack, plus the controls Terraform could not touch. Same account header as the scan report.
ohbs-cloud apply --tag cos --exclude 4.6 --report # -> ohbs-<ts>.html
cis apply --only 4.* --report harden.html # -> harden.html--report works under --dry-run: stacks are marked planned instead of
ok/fail, giving a preview artifact before touching the account.
cis scan and cis apply produce self-contained HTML reports with an account
header, summary statistics, per-section tables, and a client-side filter bar. No
external assets are loaded. See sample-scan.html and
sample-hardening.html for what they look like.
config/controls.yml is the source of truth. Each entry:
- id: "4.6"
title: "Ensure server-side encryption is set to SSE-COS"
assessment: Manual
profile: "Level 2"
enabled: true
remediate: terraform
detect: none
stack: storage
tags: [cos, encryption, sse-cos]| Field | Meaning |
|---|---|
assessment |
Automated / Manual, as classified by CIS |
profile |
Level 1 / Level 2, as classified by CIS |
enabled |
Participates in scan/apply by default — edit this for permanent scope |
remediate |
terraform / none — can apply enforce it |
detect |
terraform / none — can scan evaluate it |
stack |
Owning Terraform stack (null when unsupported) |
tags |
Free-form selectors for --tag |
Use --only for one-off runs; edit enabled: for permanent exclusions.
The rest of the file is generated by tools/generate_controls.py from the
official CIS benchmark PDF.
pytest test_py -q # 310 tests, offline
pytest test_py/test_selector.py # single fileNo cloud API calls, no credentials, no terraform execution — every CLI test
runs with --dry-run.
| File | Covers |
|---|---|
test_catalog.py |
Registry well-formedness: 91 ids, section sizes, profile split, capability counts |
test_selector.py |
Filter semantics and precedence, to_env/from_env round-trip |
test_wiring.py |
Registry ↔ HCL alignment (tencent) |
test_cloud_wiring.py |
Registry ↔ HCL alignment (aws/azure/gcp/alibaba, parameterised) |
test_cli.py |
Flags, exit codes, output formats, env-vs-flag precedence, --cloud |
test_runner.py |
Exit-code contract, terraform command issuance |
test_features.py |
Severity, risk score, remediation, suppression, resource, compliance, custom checks (tfcheck) |
test_drift.py |
Baseline drift (check-drift): regressions, offline/live, render |
test_schema.py |
Canonical finding schema: key set, defaults, cross-command parity |
test_mcp.py |
MCP JSON-RPC surface: tools/list, tools/call, initialize, error paths |
test_cache_fingerprint.py |
CI cache fingerprint: determinism, invalidation on dep change, graceful failure, concurrent stampede race-safety |
The most important test in the suite. It reads the Terraform as text and
validates against config/controls.yml:
- Every stack's
local.implementedexactly equals the set of remediable controls the registry routes to that stack — no drift, no duplication - The audit stack contains no
resourceormoduleblocks - The audit stack's probe keys exactly equal the 20 detectable controls
- Every resource is gated on the selection — filtered runs are really filtered
- Every
.tfvarswiresenabled_controlsfrom a method that actually exists - HCL is canonically formatted (
terraform fmt, self-skips when the binary is absent)
This catches the failure mode that matters most: "the registry says storage owns
4.7, but storage/main.tf never implements it" — which otherwise surfaces as a
clean-looking report.
.github/workflows/ci.yml runs pytest test_py -q on every push and pull
request, then terraform init -backend=false && terraform validate on every
module and stack. Python + pytest + Terraform CLI only — no cloud account. It
also runs the offline check on the test fixture and uploads the SARIF to
GitHub Code Scanning, so IaC findings appear as PR/commit alerts.
setup-python@v5 uses the built-in cache: pip, keyed on the content of
pyproject.toml (cache-dependency-path). Consequences:
- Hit — when
pyproject.tomlis unchanged, the pip download cache is restored andpip install -e . pytestskips re-downloading wheels. - Invalidation — bumping a dependency in
pyproject.tomlchanges the cache key, so the stale cache is not reused; pip re-resolves and a fresh cache is saved under the new key. setup-python falls back to the previous key when the new one is empty, which is safe (pip's cache is keyed by wheel version and additive).
Verified end-to-end on CI: a run with an unchanged pyproject.toml logs
Cache restored successfully, and editing pyproject.toml produces a
different cache key.
Time limits. GitHub Actions caches are ephemeral: a cache entry is evicted
if it has not been accessed for 7 days, and all entries are removed after
30 days regardless of activity. This is fine here — the pip download cache
is an optimisation, not a correctness dependency. On a cold cache (after a 7/30
day gap, or a repo with no recent CI) pip install simply re-downloads and
re-seeds the cache, so the build still succeeds, just slower. No action needed.
tools/validate.sh # every stack and module
tools/validate.sh storage audit # named targetsCopies stacks/ and modules/ into a scratch tree that mirrors the project
root (so ../../modules/ source paths keep resolving), then runs
terraform init -backend=false && terraform validate. No account required.
- Coverage ceiling. 39/91 remediable, 20/91 detectable. Several
MANUALcontrols are manual only because the provider data source hasn't been wired — theaudit/data.tfgating pattern is the template. Moving controls from manual to detectable is the highest-value contribution. - No drift tracking.
cis scanis point-in-time. Baseline save/compare is the natural next feature for compliance cadence. - No post-apply verification.
applydoes not re-runscanto assert the control flipped to PASS. - Failing stack stops the run. No
--continue-on-error. Current behaviour is intentional (attribution over throughput). - Local state by default. Switch
stacks/<name>/backend.tfto a COS remote backend for multi-operator use. regiondefaults toap-guangzhou. Consider failing loudly whenTENCENTCLOUD_REGIONis unset for a compliance tool.
- Python >= 3.10 (pip install ohbs-cloud)
- Terraform >= 1.5.0
tencentcloudstack/tencentcloudprovider~> 1.81- Tencent Cloud API credentials (
TENCENTCLOUD_SECRET_ID,TENCENTCLOUD_SECRET_KEY,TENCENTCLOUD_REGION)
Credentials are read from the environment only — nothing about an account belongs in this repository.
Independent project — ohbs-cloud is not affiliated with, sponsored by, or endorsed by the Center for Internet Security (CIS). CIS Benchmark content is copyright © Center for Internet Security, Inc. and used under their terms of use.
MIT — see LICENSE.
