Skip to content

Commit 26c22c9

Browse files
committed
Add NIST 800-53 infrastructure: build system, scripts, and workflow
Implement infrastructure for NIST 800-53 Rev 5 split-by-family control files with OSCAL metadata enrichment and automated CIS benchmark integration. Build System Updates: - ssg/controls.py: Add OSCAL fields to allowed keys - parameters: Organization-Defined Parameters (ODPs) - guidance: Implementation advice text - related_controls: Control dependency references - Backward compatible: New fields loaded but not required for processing Synchronization Scripts: - sync_nist_split.py: Generate split control files from OSCAL catalog - Extracts control metadata (description, parameters, guidance, related_controls) - Inverts CIS→NIST mappings to populate rule selections - Escapes Jinja2 syntax in OSCAL text ({{ → [[) - Outputs 21 family files + metadata file - harvest_cis_nist_mappings.py: Extract CIS→NIST mappings from benchmarks - generate_product_family_guards.py: Apply Jinja2 product guards - generate_variable_to_products.py: Build variable→products mapping - download_oscal.py: Download NIST OSCAL catalog JSON - compare_profile_rules.py: Validate CIS profile coverage - generate_nist_based_cis_profile.py: Generate CIS-NIST profiles - generate_cis_nist_workflow.sh: Master workflow orchestration Automation Workflow: - .github/workflows/cis-nist-sync.yml: Weekly automation - Runs Sundays at 2 PM UTC - Downloads latest NIST OSCAL catalog - Regenerates reference control files - Applies product guards for RHEL family - Creates PR when changes detected Documentation: - controls/README_nist_800_53.md: Comprehensive guide for control files - Split-by-family architecture explained - OSCAL metadata documentation - Manual review process for automated PRs - Example scenarios and troubleshooting - utils/nist_sync/README.md: Toolkit documentation - Script usage and workflows - Local development guide - Integration with CI/CD Architecture: - Split-by-family: 21 family files (AC, AU, CM, etc.) instead of monolithic - OSCAL metadata: Enriched controls with official NIST documentation - Product guards: Jinja2 conditionals for RHEL family (rhel8, rhel9, rhel10) - Two file sets: Real files (human-edited) + Reference files (auto-generated) Testing: - Verified RHEL 9 datastream builds successfully - Profile comparisons show exact CIS coverage - Build system ignores new OSCAL fields (backward compatible)
1 parent dd86a53 commit 26c22c9

17 files changed

Lines changed: 6396 additions & 3 deletions

.github/workflows/cis-nist-sync.yml

Lines changed: 594 additions & 0 deletions
Large diffs are not rendered by default.

controls/README_nist_800_53.md

Lines changed: 586 additions & 0 deletions
Large diffs are not rendered by default.

ssg/controls.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ class Control(ssg.entities.common.SelectionHandler, ssg.entities.common.XCCDFEnt
155155
related_rules=list,
156156
rules=list,
157157
controls=list,
158+
# OSCAL metadata fields (for documentation, ignored during processing)
159+
parameters=list,
160+
guidance=str,
161+
related_controls=list,
158162
)
159163

160164
MANDATORY_KEYS = {
@@ -181,6 +185,10 @@ def __init__(self):
181185
self.original_title = ""
182186
self.related_rules = []
183187
self.rules = []
188+
# OSCAL metadata fields (for documentation)
189+
self.parameters = []
190+
self.guidance = ""
191+
self.related_controls = []
184192

185193
def __hash__(self):
186194
"""
@@ -242,10 +250,14 @@ def from_control_dict(cls, control_dict, env_yaml=None, default_level=None):
242250
control.mitigation = control_dict.get('mitigation')
243251
control.fixtext = control_dict.get('fixtext')
244252
control.check = control_dict.get('check')
245-
control.tickets = control_dict.get('tickets')
253+
control.tickets = control_dict.get('tickets', []) # Default to empty list if not present
246254
control.original_title = control_dict.get('original_title')
247-
control.related_rules = control_dict.get('related_rules')
248-
control.rules = control_dict.get('rules')
255+
control.related_rules = control_dict.get('related_rules', []) # Default to empty list if not present
256+
control.rules = control_dict.get('rules', []) # Default to empty list if not present
257+
# OSCAL metadata fields
258+
control.parameters = control_dict.get('parameters', [])
259+
control.guidance = control_dict.get('guidance', '')
260+
control.related_controls = control_dict.get('related_controls', [])
249261

250262
if control.status == "automated":
251263
control.automated = "yes"

utils/nist_sync/.gitignore

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Downloaded OSCAL data files
2+
data/nist_800_53_rev5_*.json
3+
4+
# Generated files (regenerated from source data)
5+
data/variable_to_products.json
6+
7+
# Generated profile files (derived from CIS profiles)
8+
../../products/rhel8/profiles/cis_nist.profile
9+
../../products/rhel9/profiles/cis_nist.profile
10+
../../products/rhel10/profiles/cis_nist.profile
11+
12+
# Note: data/cis_nist_mappings.json is COMMITTED (not ignored)
13+
# It's only regenerated when CIS benchmark PDFs are updated
14+
15+
# CIS Benchmark documents (large binary files)
16+
*.pdf
17+
18+
# Markdown conversions of PDFs
19+
CIS_Red_Hat_Enterprise_Linux_*.md
20+
21+
# Python virtual environment
22+
venv/
23+
__pycache__/
24+
*.pyc
25+
*.pyo
26+
27+
# Temporary files
28+
*.tmp
29+
*.bak
30+
31+
# Documentation (keep only workflow docs)
32+
!CIS_NIST_WORKFLOW.md
33+
!README.md
34+
35+
# Auto-generated CIS-NIST profiles (derived from CIS profiles)
36+
products/*/profiles/cis_nist.profile

utils/nist_sync/README.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# NIST 800-53 / CIS Synchronization Toolkit
2+
3+
Automated tooling to generate and maintain NIST 800-53 control files with CIS benchmark mappings and OSCAL metadata.
4+
5+
## Quick Start
6+
7+
```bash
8+
# Run the complete workflow (generates reference files with OSCAL metadata)
9+
./generate_cis_nist_workflow.sh
10+
11+
# Test locally before committing
12+
./test_workflow_local.sh
13+
```
14+
15+
## Architecture
16+
17+
The toolkit generates **split-by-family** control files:
18+
19+
```
20+
controls/nist_800_53/ # 21 family files (AC, AU, CM, etc.)
21+
├── ac.yml # Access Control (147 controls)
22+
├── au.yml # Audit and Accountability (69 controls)
23+
├── cm.yml # Configuration Management (66 controls)
24+
└── ... (18 more families) # Total: 1,196 NIST controls + 102 unmapped CIS items
25+
```
26+
27+
Each control includes:
28+
- **OSCAL metadata**: description, parameters (ODPs), guidance, related controls
29+
- **CIS rule mappings**: Rules extracted from CIS benchmark references
30+
- **Product guards**: Jinja2 conditionals for RHEL family (rhel8, rhel9, rhel10)
31+
- **Baseline levels**: Low, moderate, high applicability
32+
33+
## Scripts
34+
35+
### Main Workflow
36+
37+
- **`generate_cis_nist_workflow.sh`** - Master orchestration script
38+
- Downloads NIST OSCAL catalog
39+
- Scans CIS control files for mappings
40+
- Generates all 21 family files with OSCAL metadata
41+
- Applies product guards
42+
- Compares with previous version
43+
- Used by GitHub Actions weekly automation
44+
45+
### Core Components
46+
47+
- **`sync_nist_split.py`** - Generate split control files from OSCAL catalog
48+
- Extracts control metadata (description, parameters, guidance, related controls)
49+
- Inverts CIS→NIST mappings to populate rule selections
50+
- Escapes Jinja2 syntax in OSCAL text (`{{ }}``[[ ]]`)
51+
- Outputs 21 family files + metadata file
52+
53+
- **`harvest_cis_nist_mappings.py`** - Extract CIS→NIST mappings
54+
- Scans CIS benchmark PDFs/documents
55+
- Builds rule→NIST control mapping
56+
- Outputs `data/cis_nist_mappings.json`
57+
58+
- **`generate_product_family_guards.py`** - Add Jinja2 product guards
59+
- Scans CIS control files to determine rule availability per product
60+
- Generates family-based guards: `{{% if product.startswith('rhel') %}}`
61+
- Groups variable variants into if/elif blocks
62+
- Processes all 21 family files
63+
64+
- **`generate_variable_to_products.py`** - Build variable→products mapping
65+
- Scans CIS control files for variable assignments
66+
- Outputs `data/variable_to_products.json`
67+
- Used by guard generator for variable-specific guards
68+
69+
### Utilities
70+
71+
- **`download_oscal.py`** - Download NIST OSCAL catalog JSON
72+
- **`compare_profile_rules.py`** - Validate CIS profile coverage
73+
- **`generate_nist_based_cis_profile.py`** - Generate CIS-NIST profiles
74+
- **`test_workflow_local.sh`** - Local testing script
75+
76+
## File Locations
77+
78+
```
79+
controls/
80+
├── nist_800_53.yml # Top-level metadata (points to subdirectory)
81+
└── nist_800_53/*.yml # 👤 REAL family files (human-edited)
82+
83+
shared/references/controls/
84+
├── nist_800_53_cis_reference.yml # Reference metadata
85+
└── nist_800_53_cis_reference/*.yml # 🤖 REFERENCE family files (auto-generated)
86+
87+
utils/nist_sync/
88+
├── *.py # Python scripts
89+
├── *.sh # Bash orchestration
90+
└── data/ # Generated mappings
91+
├── cis_nist_mappings.json # CIS→NIST mappings
92+
└── variable_to_products.json # Variable availability
93+
94+
.github/workflows/
95+
└── cis-nist-sync.yml # Weekly automation workflow
96+
```
97+
98+
## Workflows
99+
100+
### Weekly Automation (GitHub Actions)
101+
102+
Every Sunday at 2 PM UTC:
103+
1. Downloads latest NIST OSCAL catalog
104+
2. Regenerates reference family files with OSCAL metadata
105+
3. Applies product guards for RHEL family
106+
4. Compares with previous version
107+
5. Creates PR if changes detected
108+
109+
**Reference files updated automatically. Real files require manual review.**
110+
111+
### Local Development
112+
113+
```bash
114+
# Run full workflow
115+
cd utils/nist_sync
116+
./generate_cis_nist_workflow.sh
117+
118+
# Compare reference vs real files
119+
diff -ur shared/references/controls/nist_800_53_cis_reference/ \
120+
controls/nist_800_53/
121+
122+
# Test profile coverage
123+
python3 compare_profile_rules.py \
124+
../../products/rhel9/profiles/cis.profile \
125+
../../products/rhel9/profiles/cis_nist.profile \
126+
--product rhel9
127+
```
128+
129+
### Manual Sync
130+
131+
To regenerate reference files manually:
132+
133+
```bash
134+
cd utils/nist_sync
135+
136+
# Step 1: Download OSCAL catalog
137+
python3 download_oscal.py
138+
139+
# Step 2: Generate family files with OSCAL metadata
140+
python3 sync_nist_split.py
141+
142+
# Step 3: Generate variable mapping
143+
python3 generate_variable_to_products.py
144+
145+
# Step 4: Apply product guards to all families
146+
for f in ../../shared/references/controls/nist_800_53_cis_reference/*.yml; do
147+
python3 generate_product_family_guards.py --target rhel \
148+
--control-file "$f" --output "$f"
149+
done
150+
```
151+
152+
Then review diff and manually update real control files.
153+
154+
## Documentation
155+
156+
- **Main documentation**: `controls/README_nist_800_53.md`
157+
- **Workflow details**: `.github/workflows/cis-nist-sync.yml`
158+
- **Script usage**: Run any script with `--help` flag
159+
160+
## Requirements
161+
162+
```bash
163+
pip install -r requirements.txt
164+
```
165+
166+
Dependencies:
167+
- `ruamel.yaml` - Round-trip YAML parsing
168+
- Python 3.8+
169+
170+
## Key Concepts
171+
172+
### Split-by-Family Architecture
173+
174+
Controls are organized into 21 family directories instead of a single monolithic file:
175+
- Easier to navigate and edit
176+
- Clearer git diffs
177+
- Parallel processing support
178+
- Better organization for 1,196 controls
179+
180+
### OSCAL Metadata Enrichment
181+
182+
Each control includes metadata from the official NIST OSCAL catalog:
183+
- **description**: Full control statement with sub-parts (a, b, c...)
184+
- **parameters**: Organization-Defined Parameters with labels and guidelines
185+
- **guidance**: Implementation advice (500-3000 characters)
186+
- **related_controls**: Control dependency references
187+
188+
### Product Guards
189+
190+
Jinja2 conditionals enable cross-product rule sharing:
191+
```yaml
192+
{{% if product.startswith('rhel') %}}
193+
- rule_for_all_rhel_versions
194+
{{% endif %}}
195+
196+
{{% if product == "rhel9" %}}
197+
- rhel9_specific_rule
198+
{{% endif %}}
199+
```
200+
201+
### Two File Sets
202+
203+
- **Reference files** (`shared/references/...`) - Auto-generated from OSCAL + CIS, used for comparison
204+
- **Real files** (`controls/...`) - Human-edited, used for building, never touched by automation
205+
206+
This separation prevents automation from overwriting human edits while tracking upstream changes.
207+
208+
## Troubleshooting
209+
210+
**Build fails with "parameters is not allowed":**
211+
- Update `ssg/controls.py` to include OSCAL fields in allowed keys
212+
213+
**Profile comparison shows missing rules:**
214+
- Check that guards are applied: `grep "if product" controls/nist_800_53/*.yml`
215+
- Verify variable mappings: `cat data/variable_to_products.json`
216+
217+
**OSCAL metadata has Jinja2 syntax errors:**
218+
- Verify escaping: OSCAL uses `[[ ]]`, guards use `{{% %}}`
219+
- Check `sync_nist_split.py` escape_jinja_syntax() function
220+
221+
## Contributing
222+
223+
When modifying scripts:
224+
1. Test locally with `test_workflow_local.sh`
225+
2. Verify builds succeed: `./build_product rhel9 --datastream-only`
226+
3. Check profile coverage: `compare_profile_rules.py`
227+
4. Update this README if workflow changes
228+
229+
## References
230+
231+
- NIST OSCAL: https://pages.nist.gov/OSCAL/
232+
- NIST 800-53: https://csrc.nist.gov/publications/detail/sp/800-53/rev-5/final
233+
- CIS Benchmarks: https://www.cisecurity.org/cis-benchmarks

utils/nist_sync/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# NIST 800-53 Synchronization Utilities

0 commit comments

Comments
 (0)