Open, free and versioned security configuration baselines, maintained by the CIRSEC community.
Published at https://baselines.cirsec.org.
CIRSEC Baselines describe how to securely configure systems and services (cloud tenants, operating systems, network devices, software). Each recommendation, called a control, is written from scratch by domain experts from primary sources (vendor documentation, RFCs, public-domain guides), and released under a permissive license so anyone can use it, including in commercial products.
When a control covers the same ground as another framework (CIS Benchmarks, ISO/IEC 27001, NIST SP 800-53, CISA SCuBA, ANSSI...), it links to it through a typed mapping. We reference other frameworks, we never copy them.
Contents
- Principles
- Repository layout
- Control identifiers
- Standards, profiles and domains
- Control format
- Checks, engines and facts
- Remediation
- Parameters and customization
- Tailoring and scan results
- Generated artifacts
- Mappings and threats
- Translations
- Lifecycle and versioning
- Extensions
- Validation rules
- Editor support
- Tooling, CI and releases
- Contributing
- Roadmap
- License
- Clean room. Controls are written from primary sources only. Contributors must not have a proprietary benchmark open while writing a control. Mappings to other frameworks are added afterwards, in a separate pass, ideally by another contributor.
- Provenance. Every control lists the sources it was written from (
references). Every commit is signed off (DCO). - Stable identifiers. A control ID is never renumbered and never reused.
- Reference, don't reproduce. For frameworks with restrictive licenses, only identifiers appear in this repository (
reference_policyinbaselines.yml). - Customizable. Tunable values are parameters with per-profile defaults, so organizations adapt a baseline without forking it.
- Stable format. The schemas are designed to be complete from day one. Growth happens through content, not format changes. Tool-specific data goes in
x-fields. - Intent, not code. A control declares the expected state through declarative engines (
sysctl,sshd-config,graph-api...). Checks and fixes for every output format (shell, Ansible, PowerShell...) are generated by the tooling. Handwritten code is the exception. - Open by default. Control text is CC BY 4.0. Code (scripts, generated artifacts) and tooling are Apache 2.0.
baselines/
├── baselines.yml # global settings: licenses, locales, ID format, engines, frameworks
├── LICENSE # Apache 2.0 (tooling)
├── LICENSE-CONTENT # CC BY 4.0 (control content)
├── schema/ # JSON Schemas (draft 2020-12)
│ ├── baselines.schema.json
│ ├── category.schema.json
│ ├── standard.schema.json
│ ├── control.schema.json
│ ├── control-i18n.schema.json
│ ├── engines/<engine>.schema.json # spec of each engine
│ ├── tailoring.schema.json # organization overrides, read by scanners and generators
│ └── result.schema.json # scan output contract
├── templates/ # copy-paste starting points, valid against the schemas
│ ├── control.yml
│ ├── control-i18n.yml
│ └── tailoring.yml
├── SECURITY.md # how to report a dangerous fix or a false negative
├── tools/ # validator, generators, release builder, tests
│ ├── validate.py
│ ├── tailor.py # checks a tailoring file against the content
│ ├── generate.py # audit/remediation scripts and Ansible role for one profile
│ ├── oscal.py # OSCAL catalog and profiles
│ ├── build.py
│ ├── common.py
│ ├── lib/cirsec.sh # shell runtime inlined into every generated control script
│ ├── requirements.txt
│ └── tests/ # with fixtures/ (test-only standard) and golden/ (expected outputs)
├── .github/
│ ├── CODEOWNERS # reviewers per standard
│ ├── ISSUE_TEMPLATE/ # new control, wrong check result, broken fix
│ └── workflows/
│ ├── ci.yml # validate, test, lint scripts, build, DCO, on every push and PR
│ └── release.yml # manual release of one standard (workflow_dispatch)
└── categories/
├── cloud/ # category
│ ├── _category.yml
│ └── m365/ # standard
│ ├── _standard.yml
│ ├── controls/ # one file per control, in the default locale (en)
│ │ └── CB-M365-AUTH-001.yml
│ └── i18n/
│ └── fr/ # translations, one file per control
│ └── CB-M365-AUTH-001.yml
└── os/
├── _category.yml
└── debian/
├── _standard.yml
├── controls/
├── scripts/ # (optional) handwritten fix scripts, one folder per control
│ └── CB-DEB-AUTH-007/
│ └── fix.sh
└── i18n/fr/
| Level | Location | File | Schema |
|---|---|---|---|
| Root | / |
baselines.yml |
baselines.schema.json |
| Category | categories/<category>/ |
_category.yml |
category.schema.json |
| Standard | categories/<category>/<standard>/ |
_standard.yml |
standard.schema.json |
| Control | .../<standard>/controls/ |
<ID>.yml |
control.schema.json |
| Translation | .../<standard>/i18n/<locale>/ |
<ID>.yml |
control-i18n.schema.json |
| Script | .../<standard>/scripts/<ID>/ |
any name | linted per engine, Apache 2.0 |
- The folder tree is the source of truth. No hand-maintained index lists categories, standards or controls. Indexes and the website are generated from the tree, so they cannot drift.
- One file per control. A standard can hold hundreds of controls. One file each keeps reviews readable, avoids merge conflicts between experts working in parallel, and gives a clean
git log/git blameper control, which is what proves provenance. - One standard per product, not per product version.
debiancovers Debian 12 and 13. Version differences are handled per control (applicability.versions,audit.checks[].versions). This avoids duplicating hundreds of controls at each release. A new major product version is just a new entry inapplies_to. _prefix for metadata files. They sort first and are clearly distinct from content.- Small metadata is translated inline, controls in separate files. Categories and standards carry an
i18nblock. Controls are large and translated by different people, so each translation is its own file with its own history.
categories/demo/buffet is a deliberately silly standard (all-you-can-eat buffet security) that uses every field of the format, with comments explaining each one. Read it before writing your first control. It relies on demo-only entries in baselines.yml (demo-sensor engine, demo-food-code and demo-buffet-threats frameworks) and is excluded from releases and from the public site.
| Control | Shows |
|---|---|
CB-BUF-HYG-001 |
Full example: automated checks per version, fixes generated from checks (from_check) with effects, parameters with profile defaults, setting, mappings, threats, verification per version, French translation |
CB-BUF-GUARD-001 |
Manual control, applicability block with versions, facts and requires, requires relation |
CB-BUF-HYG-002 |
Partial assessment, enum and boolean parameters, logic: any, check limited by a fact, handwritten fix in a script file, hardened-only profile, review status |
CB-BUF-ABUSE-001 |
Deprecated control with its replacement |
CB-BUF-ABUSE-002 |
Replacement (supersedes), conflicts relation, x- extension fields |
- Category and standard folder names are lowercase slugs (
cloud,m365,debian) equal to theidfield of their metadata file. They appear in URLs and never change once published. - A control file is named exactly
<ID>.yml, incontrols/and in eachi18n/<locale>/.
CB-<STANDARD>-<DOMAIN>-<NNN>
CB-M365-AUTH-001
| Segment | Meaning | Defined in |
|---|---|---|
CB |
CIRSEC Baselines | fixed |
STANDARD |
Standard code (uppercase, unique, never changes) |
_standard.yml → code |
DOMAIN |
Functional area at creation time (AUTH, NET, MAIL...) | _standard.yml → domains |
NNN |
Sequential number inside the domain, 3 digits | assigned on creation |
The domain is frozen in the ID. A control is never renumbered, never moved to another domain, and its number is never reused. If a control must change meaning or domain, it is deprecated and replaced by a new one (see Lifecycle).
A standard (_standard.yml) declares:
| Field | Purpose |
|---|---|
id, code |
URL slug and ID code. Both permanent. |
status |
draft, active or deprecated. |
version |
SemVer of the current or upcoming release. released holds its date. |
applies_to |
Products, vendors and supported versions. |
profiles |
Named sets of controls (e.g. baseline, hardened). extends chains them. |
domains |
Allowed DOMAIN codes for control IDs. |
facts |
Machine-detectable conditions (package installed, desktop environment...). See Facts. |
maintainers |
People responsible for the standard. |
primary_sources |
Sources the standard is written from. Never a proprietary benchmark. |
i18n |
Translated title, description, domain and profile names. |
Profiles replace the usual "Level 1 / Level 2". A control lists the profiles it belongs to. With hardened: extends: baseline, applying hardened applies every baseline control too, plus the hardened ones, and uses hardened parameter defaults. Standards can define other profiles (e.g. workstation, small-business) without any format change.
A profile extends at most one parent. A control shared by two branches (e.g. server-hardened and desktop-hardened) lists both profiles, and both in profile_defaults when its values differ.
The full contract is schema/control.schema.json. The annotated starting point is templates/control.yml.
| Group | Field | Req. | Purpose |
|---|---|---|---|
| Identity | id |
yes | Permanent ID, equal to the file name. |
title |
yes | The recommendation, as the desired state, in our own words. | |
finding |
yes | How a failure appears in a report: title (the problem) and optional description. |
|
status |
yes | draft, review, active, deprecated. |
|
profiles |
yes | Profile keys from the standard. | |
risk |
yes | severity (info to critical) and optional score (0-10). |
|
assessment |
yes | automated, partial or manual. automated requires at least one check. |
|
tags |
Free lowercase slugs. | ||
| Scope | applicability |
versions, facts (machine-detectable), requires (human-only: licenses, editions), conditions. Omitted = always applies. |
|
| Content | description |
yes | What the setting or practice is. Markdown. |
rationale |
yes | Why it matters. Markdown. | |
impact |
yes | Side effects of applying it. "None known." if none. | |
notes |
Edge cases, exceptions, vendor quirks. | ||
setting |
location, default_value, recommended_value for single-setting controls. |
||
parameters |
Tunable values. See below. | ||
| Audit | audit.manual |
yes | Human check procedure. |
audit.logic |
all (default) or any of the checks must pass. |
||
audit.checks[] |
Machine checks: id, engine, description, versions, facts, spec validated by the engine schema. |
||
| Remediation | remediation.manual |
yes | Human fix procedure. |
remediation.effects |
Restart, services, disruption, lockout risk. Required with scripts. | ||
remediation.rollback |
How to revert, in prose. | ||
remediation.scripts[] |
Automated fixes: from_check, or engine + content / file. See Remediation. |
||
| Links | references[] |
yes | Primary sources used, with kind and accessed date. |
mappings[] |
Links to control frameworks. | ||
threats[] |
ATT&CK techniques, CWE, CVE mitigated. | ||
related_controls[] |
requires, related or conflicts with other CIRSEC controls. |
||
| People | authors |
yes | Writers. |
reviewers |
Experts who reviewed. | ||
verified[] |
Tests against real environments: date, by, versions, environment, notes. Required on every applicable version before an automated or partial control becomes active. |
||
| Lifecycle | introduced_in |
yes | Standard version where it first appeared. |
created, updated |
yes | Dates. updated is bumped on every content change. |
|
deprecated_in, deprecation_reason, superseded_by |
Required when status: deprecated. |
||
supersedes |
On the replacing control. |
title is the rule, finding.title is what a report shows when the check fails:
title: Block user consent to third-party applications
finding:
title: Users can grant third-party apps access to company dataengine must be a key declared under engines in baselines.yml. Each engine declares:
capabilities:check: evaluates a declarativespec.fix: can generate the remediation from a checkspec(scripts[].from_check).script: accepts handwritten code (scripts[].contentorfile).
spec_schema: the JSON Schema inschema/engines/that everyspecof this engine must match.
| Engine | Capabilities | For |
|---|---|---|
sysctl, package, systemd-unit, file-perms, sshd-config, dconf |
check, fix | Linux |
windows-registry |
check, fix | Windows |
graph-api |
check, fix | Microsoft 365 |
exchange-powershell, osquery, file-content, postmortem |
check | read-only sources |
shell, powershell |
check, script | escape hatch |
Prefer declarative engines. A control says what (key: net.ipv4.ip_forward, value: 0), and the engine's generators produce the check and the fix for every output format. A bug is fixed once in the generator, for every control. A new output format (Terraform, Intune...) is a new generator, without touching any control. Script engines are for what no declarative engine covers.
Engines describe semantics, not implementations: a scanner may evaluate sysctl by reading /proc/sys, through osquery, or any other way. Adding an engine is a change to baselines.yml plus one schema in schema/engines/.
Script engine checks exit with 0 pass, 1 fail, 2 not applicable, anything else error. Stdout is the evidence.
Scanners report results per audit.checks[].id, so a multi-check control can say exactly which part failed.
applicability.conditions and applicability.requires are text for humans. To let a scanner decide on its own that a control does not apply, a standard declares facts: conditions with a detection check.
# _standard.yml
facts:
- key: openssh-server
title: OpenSSH server is installed
detect:
engine: package
spec: { name: openssh-server, state: installed }
- key: desktop-gnome
title: GNOME desktop is installed
detect:
engine: package
spec: { name: gnome-shell, state: installed }Facts are referenced by key, and all listed facts must be true:
applicability.facts: the whole control. When a fact is false, the control isnot_applicable.audit.checks[].factsandremediation.scripts[].facts: one check or script. This is how one control "lock the screen" holds adconfcheck fordesktop-gnomeand another check fordesktop-kde.
requires stays for what no tool can detect (licenses, contracts).
remediation:
manual: |
Human procedure, always required.
effects: # required when scripts are present
restart: service # none | service | session | reboot
services: [ssh] # required when restart is service
disruption: low # none | low | high
lockout_risk: true # can cut off admin access: disabled by default in generated artifacts
rollback: |
Human procedure to revert.
scripts:
- id: dropin
from_check: sshd-effective # 1. generated from a check (engine with the fix capability)
- id: inline
engine: shell # 2. short handwritten code (engine with the script capability)
content: |
some-command --value "$CB_MAX_DAYS"
- id: long
engine: shell # 3. longer code in <standard>/scripts/<ID>/fix.sh
file: fix.shfrom_checkfirst. The fix is generated from the check's spec, versions and facts.- Handwritten code receives parameters as environment variables
CB_<NAME>(max_days→CB_MAX_DAYS), never through{{ }}. Textual substitution into code is a quoting and injection trap. The validator rejects{{ }}in code. - Script files live in
<standard>/scripts/<control ID>/. Every file must be referenced, so it can be linted, tested and reviewed like code. - Fixes are idempotent and back up what they change. Automated rollback is not promised,
rollbackis prose. effectslets generators restart each service once at the end, and keep fixes withlockout_riskoff unless the tailoring file opts in.assessmentonly describes the audit. A control can beautomatedto check and have no automated fix (e.g. a Microsoft 365 setting without a write API).
Values an organization may want to tune are declared as parameters, and referenced as {{ name }} in text (setting.recommended_value, description, audit.manual...) and in declarative check specs. Handwritten code reads them from CB_<NAME> environment variables instead.
setting:
recommended_value: "At most {{ max_days }} days"
parameters:
- name: max_days
type: integer # string | integer | number | boolean | enum | list
description: Maximum allowed lifetime.
unit: days
default: 180 # used by every profile without its own value
profile_defaults:
hardened: 90
minimum: 1
maximum: 365Resolution order for a value:
- the override in the user's tailoring file;
profile_defaultsof the profile, then of each profile it extends, nearest first (desktop-hardened→desktop→core);default.
minimum, maximum and allowed bound what users may set. allowed is required for type: enum.
In a spec, a value that is exactly one placeholder (value: "{{ min_temp_c }}") is replaced by the parameter with its type (integer, boolean, list). A placeholder inside a longer string is replaced as text.
bundle.json holds the resolved values of every control for every profile (resolved_parameters).
Tailoring (schema/tailoring.schema.json, example in templates/tailoring.yml): how an organization adapts a released standard without forking it. It picks a profile, overrides parameters within their bounds, excludes controls with a reason and an optional review date, and opts in to fixes with lockout_risk. The file lives in the organization's repository.
Scan results (schema/result.schema.json): what any scanner outputs, so any dashboard can read it.
| Control status | Meaning |
|---|---|
pass / fail |
Checks evaluated with audit.logic |
not_applicable |
A version or fact does not match |
error |
A check could not run |
manual |
assessment: manual, a human must check |
review |
assessment: partial and every check passed, a human must confirm the rest |
excluded |
Removed by the tailoring file |
Each check result carries evidence (observed, expected, source), and the result names the standard version, profile and resolved parameters, so it can be reproduced.
Check a tailoring file against the content (controls exist, parameters within bounds, past review dates):
python tools/tailor.py acme-servers.ymltools/generate.py turns the controls of one profile into code. Every release contains them for each profile, built with the profile defaults. Regenerate with your tailoring file to apply your overrides and exclusions.
python tools/generate.py --standard debian --tailoring acme-servers.yml --out gen/| Output | Use |
|---|---|
controls/<ID>.sh |
One self-contained POSIX sh script per control. check prints one line per check and exits 0 pass, 1 fail, 2 not applicable, 3 error, 4 manual or review. fix is a dry run, fix --apply applies. |
audit.sh |
Runs every check, prints a result JSON on stdout and a readable report on stderr: summary and score, failures by severity with observed and expected values, errors, what to check by hand (audit.manual), next commands. On a terminal the JSON goes to ./cirsec-result-<host>-<date>.json instead. Colors on a terminal only, never with NO_COLOR. --verbose lists every entry. Exit 1 if anything fails. |
remediate.sh |
Runs every enabled fix in dependency order (related_controls: requires), dry run unless --apply. Backs up changed files under /var/backups/cirsec/<date>/, restarts each service from effects once at the end, says when a reboot or new login is needed. |
ansible/ |
site.yml and a role running the same control scripts: one task pair (check, fix) per control, tagged with the control ID, cirsec_<standard>_<id>_enabled per control, handlers from effects, --check mode supported. |
Rules applied by every generator:
- Only
activecontrols (--include-draftsaddsdraftandreviewfor testing). - A fix runs only when its check fails, and the check runs again afterwards. A check that cannot run (
error) blocks its fix. - Fixes with
lockout_riskare disabled unless the tailoring file opts in (fixes:), orCB_ENABLE_<ID>=1forremediate.sh. - The product version comes from
/etc/os-release(VERSION_IDmajor), orCB_PRODUCT_VERSION. When it is unknown, version-specific checks reporterror, nevernot_applicable. - Engines without a shell generator (
graph-api,windows-registry,demo-sensor...) reporterrorin shell outputs. They need a scanner that implements them. - A fix can only be generated for checks using
==,<=or>=(the fix sets the expected value).
Generators live in tools/generate.py, one check and one fix function per engine. A bug is fixed there once, for every control. tools/tests/golden/ holds the expected output for the test standard in tools/tests/fixtures/lab. After an intended change, refresh it with UPDATE_GOLDEN=1 python -m pytest -q tools/tests/test_generate.py and review the diff.
A mapping says how our control relates to a control in another framework. Only frameworks of kind: control-framework can be used.
mappings:
- framework: cis-m365
framework_version: "7.0.0"
id: "x.y.z"
relation: partial
note: Ours also covers service principals.relation |
Meaning |
|---|---|
equivalent |
Same intent and same expected configuration |
stricter |
Ours requires more than theirs |
looser |
Ours requires less than theirs |
partial |
Overlap, each covers things the other does not |
related |
Same topic, different approach |
threats:
- framework: mitre-attack
id: T1528
- framework: cwe
id: CWE-284Only frameworks of kind threat-taxonomy, weakness-taxonomy or vulnerability-database can be used here.
references (what we wrote from) never contains a proprietary benchmark. mappings may, but never alone: a control whose mappings all point to id-only frameworks gets a warning, so map an open one (ANSSI, NIST, CISA) too. Frameworks carry a display_order so the site lists open public frameworks first and CIS last.
A benchmark published per product version is declared once per version (cis-debian-12, cis-debian-13), since numbering differs between them. framework_version is then the benchmark's own version.
Every framework is declared once in baselines.yml with:
kind: which field may use it.id_pattern: regex every referenced identifier must match (catches typos).display_order: sort key on the site, lower first.reference_policy:id-only: only identifiers may appear in this repository. No titles, no excerpts. Applies to CIS Benchmarks and ISO/IEC standards.id-title: identifiers and official titles are allowed (NIST, CISA, ANSSI, MITRE...).
Adding a framework is a change to baselines.yml, not to the schemas.
- The default locale (
en) lives incontrols/. Available locales are listed inbaselines.yml. - A translation is
i18n/<locale>/<ID>.yml, containing only text fields (title, finding, description, rationale, impact, notes, conditions, setting text, parameter descriptions, manual audit and remediation). IDs, checks, parameter values and mappings always come from the source control. - Missing fields fall back to the source.
source_updatedrecords the source control'supdateddate at translation time. When the source is updated later, the translation is reported as stale.- Categories and standards are translated inline through their
i18nblock.
draft ──► review ──► active ──► deprecated
draft: being written.review: a second expert is reviewing it.active: part of a release.deprecated: kept forever for history, never deleted. Requiresdeprecated_in,deprecation_reasonandsuperseded_by([]if retired without replacement). The replacing control lists the old one insupersedes.
- Each standard has its own SemVer version in
_standard.yml. - A release is a git tag
<standard-id>/v<version>, e.g.m365/v1.0.0. No version folders: history lives in git. - The website and the resolver read tags to serve any past version, e.g.
https://baselines.cirsec.org/m365/v1.0.0/CB-M365-AUTH-001.
| Change | Bump |
|---|---|
| Control deprecated, profile membership changed, parameter default made stricter | major |
| New control, new domain, new profile, new supported product version | minor |
A fix becomes more disruptive (effects gets worse) |
minor |
| Wording, references, mappings, translations, fixes to checks and scripts (same expected state) | patch |
Every object that may need tool-specific data accepts fields prefixed with x- (root, categories, standards, facts, controls, checks, scripts, translations, tailoring, results). They are ignored by the official tooling and never needed to understand a control.
x-mlab:
rule_id: m365-consent-001Anything that proves generally useful through x- fields can be promoted to the core format in a future schema_version.
JSON Schemas cover the structure of each file. tools/validate.py also enforces cross-file rules:
_category.ymland_standard.ymlidequal their folder name.- Standard
codeis unique across the repository. - Control
idequals its file name, starts withCB-<standard code>-, and its DOMAIN is declared in the standard. - Control IDs are unique across the repository.
profilesexist in the standard. Profileextendschains have no cycles.applicability.versionsandchecks[].versionsare within the standard'sapplies_toversions.- Every
engineis declared inbaselines.ymlwith aspec_schemathat exists. Each checkspec(and each factdetect.spec) matches it. The engine has thecheckcapability for checks,fixfor the check targeted byfrom_check, andscriptforcontent/file. mappings[].frameworkhas kindcontrol-framework.threats[].frameworkhas another kind. Everyidmatches the framework'sid_pattern.{{ name }}placeholders reference declared parameters. Parameter defaults match their type and bounds.related_controls,supersedesandsuperseded_bypoint to existing controls.- Translations: file name,
idand folder locale match an existing control and an available locale. Translations withsource_updatedolder than the source'supdatedare reported as stale (warning). updatedis not earlier thancreated.- Lifecycle:
introduced_inanddeprecated_inare not after the standard version,deprecated_inis afterintroduced_in, deprecation fields only appear on deprecated controls, a control can onlysupersedesa deprecated one. - Layout: no stray files or folders,
.ymlextension only, YAML files without duplicate keys. - Facts: unique keys per standard.
applicability.facts,checks[].factsandscripts[].factsreference declared facts. - Remediation:
effectsis present when there are scripts.from_checknames a check of the same control.fileexists underscripts/<ID>/. Every file inscripts/belongs to a control and is referenced. No{{ }}in code. - Verification:
verified[].versionsare standard versions. Anactivecontrol that is notmanualis verified on every version it applies to.
Warnings (do not fail CI unless --strict): stale translations, one-sided supersedes/superseded_by, dates in the future, mappings only to id-only frameworks.
Every YAML file starts with a yaml-language-server comment pointing to its schema. With the YAML extension (VS Code, Neovim, Zed...), you get autocompletion, inline documentation and validation while typing.
# yaml-language-server: $schema=../../../../schema/control.schema.jsonRequires Python 3.11+.
pip install -r tools/requirements-dev.txt
python tools/validate.py # check everything (add --strict to fail on warnings)
python -m pytest -q tools/tests # test the tooling itself
python tools/build.py --standard buffet --allow-demo --no-tag-check # local test build into dist/
python tools/generate.py --standard buffet --profile hardened --include-drafts --out gen/ # generated scripts
python tools/tailor.py templates/tailoring.yml # check a tailoring file.github/workflows/ci.yml runs on every push and pull request:
validate.py: schemas and cross-file rules. Errors are shown as annotations on the PR files.pytest: every validation rule is tested by breaking it on purpose in a copy of the repository. Generated scripts are run end to end, compared to golden files, and checked withshellcheck.- Script files:
shellcheckfor.sh, PSScriptAnalyzer for.ps1. build.py --all: builds every standard at its current version. The archives are attached to the run as apreview-bundlesartifact for 7 days.- On pull requests: every commit carries a
Signed-off-bymatching its author (DCO).
build.py produces, for one standard:
| File | Content |
|---|---|
cirsec-baselines-<standard>-<version>.zip |
Controls with status active or deprecated (never draft or review), their translations and script files, the category and standard metadata, baselines.yml, the schemas, LICENSE and LICENSE-CONTENT, plus generated: bundle.json (everything in one JSON: resolved parameters per profile, script files inlined), index.json (lightweight list), oscal/ (OSCAL 1.1.2 catalog and one profile per profile), generated/<profile>/ (generated artifacts) |
...zip.sha256 |
SHA-256 of the archive |
RELEASE_NOTES.md |
Summary (counts per status, profile, severity, translation coverage) and changes since the previous tag of the same standard: added, deprecated, updated, removed |
Archives are reproducible: building the same commit twice gives the same checksum.
- Open a pull request that bumps
version(and optionallyreleased) in the standard's_standard.yml, and setsintroduced_in/deprecated_inon the controls it touches. Merge it. - Run Actions → Release → Run workflow on the default branch, with:
standard: the folder name, e.g.m365version: the version you just set, e.g.1.0.0prerelease: optional
- The workflow validates, refuses if the version does not match
_standard.ymlor if the tag already exists, builds, then creates the tag<standard>/v<version>and the GitHub release with the archive, the checksum and the notes.
The demo category can never be released.
- Write from primary sources. Vendor docs, RFCs, man pages, public-domain guides (NIST, CISA, ANSSI). List them in
references. - Never copy text from proprietary benchmarks, and do not keep one open while writing. Mappings are added in a separate pass.
- Sign off your commits (
git commit -s). By signing off you certify, under the Developer Certificate of Origin, that your contribution is your own work and contains no text you have no right to publish. CI refuses pull requests with unsigned commits. - Start from
templates/, one control per file, validated against the schemas. - New controls start as
draft, move toreviewonce a second expert reads them, and toactivewhen they ship in a release, after being verified on every version they apply to. - Prefer a declarative engine over a script. If none fits, consider adding the engine (schema in
schema/engines/, generator intools/generate.py) rather than writing the same script twice. - Dangerous fix or false negative? Report it privately, see SECURITY.md.
- Repository layout and global settings
- JSON Schemas (root, category, standard, control, translation)
- Templates
- Validator (schemas and cross-file rules)
- CI on every push and pull request
- Release builder and manual release workflow (
bundle.json,index.json, notes) - Format v2: declarative engines, facts, effects, tailoring, scan results
- Generators: audit and remediation scripts, Ansible role, OSCAL export
- Test matrix on real VMs per standard and version (check, fix, check, fix again)
- PowerShell and Graph generators (Windows, Microsoft 365)
- Read-only scanner implementing every check engine
- First standard: Microsoft 365
- Website and versioned resolver on baselines.cirsec.org
- Control text (
categories/**,templates/**): CC BY 4.0 - Code inside controls (
remediation.scripts[].content, files underscripts/, specs of script engines) and every artifact generated from controls (playbooks, scripts): Apache 2.0 - Schemas and tooling: Apache 2.0
By signing off a contribution (DCO), you license its text under CC BY 4.0 and its code under Apache 2.0.
CIRSEC Baselines is an independent project. It is not affiliated with, endorsed by, or certified by the Center for Internet Security, ISO, NIST, CISA, ANSSI, MITRE or any vendor. Third-party framework identifiers are used for reference only.