Skip to content

Commit 8146d6f

Browse files
author
Peter Wagstaff
committed
Fix mising nist controls
1 parent 2cc9408 commit 8146d6f

3 files changed

Lines changed: 934 additions & 733 deletions

File tree

index.html

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -765,8 +765,17 @@ <h2 class="toggles-title">Additional Security Assumptions</h2>
765765
const sl5ControlsData = await sl5ControlsResponse.json();
766766
// Tag all SL5 controls with catalog: 'sl5'
767767
Object.values(sl5ControlsData.controls || {}).forEach(ctrl => { ctrl.catalog = 'sl5'; });
768-
const mergedControls = { ...nistData.controls, ...sl5ControlsData.controls };
769-
allControls = Object.values(mergedControls);
768+
// Merge NIST and SL5 controls, only overwriting with SL5 if the value is not null/undefined
769+
const mergedControls = { ...nistData.controls };
770+
for (const [key, value] of Object.entries(sl5ControlsData.controls || {})) {
771+
if (value) mergedControls[key] = value;
772+
}
773+
// Append all SL5 controls to the NIST controls without overwriting
774+
const nistArray = Object.values(nistData.controls || {});
775+
const sl5Array = Array.isArray(sl5ControlsData.controls)
776+
? sl5ControlsData.controls
777+
: Object.values(sl5ControlsData.controls || {});
778+
allControls = nistArray.concat(sl5Array);
770779
// Ensure all controls have a family property
771780
allControls.forEach(ctrl => {
772781
if (!ctrl.family) ctrl.family = "Unknown";

nist_catalog/nist_sorter.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env python3
2+
"""
3+
NIST SP 800-53 Control Catalog JSON Sorter
4+
5+
This script sorts the NIST SP 800-53 control catalog JSON file so that:
6+
1. Controls are grouped by family
7+
2. Within each family, controls are sorted in ascending order
8+
3. Control enhancements always appear after their parent control
9+
4. Enhancement numbers are sorted numerically (e.g., AC-1(1), AC-1(2), AC-1(10))
10+
11+
Usage:
12+
python nist_sorter.py input_file.json [output_file.json]
13+
14+
If no output file is specified, the script will overwrite the input file.
15+
"""
16+
17+
import json
18+
import re
19+
import sys
20+
from pathlib import Path
21+
from typing import Dict, List, Any, Tuple
22+
23+
24+
def parse_control_id(control_id: str) -> Tuple[str, int, int]:
25+
"""
26+
Parse a control ID and return components for sorting.
27+
28+
Args:
29+
control_id: Control ID like "AC-1", "AC-1(1)", "AC-14(1)", etc.
30+
31+
Returns:
32+
Tuple of (family, base_number, enhancement_number)
33+
Enhancement number is 0 for base controls, positive for enhancements
34+
"""
35+
# Match patterns like "AC-1" or "AC-1(1)"
36+
match = re.match(r'^([A-Z]{2,3})-(\d+)(?:\((\d+)\))?$', control_id)
37+
38+
if not match:
39+
raise ValueError(f"Invalid control ID format: {control_id}")
40+
41+
family = match.group(1)
42+
base_number = int(match.group(2))
43+
enhancement_number = int(match.group(3)) if match.group(3) else 0
44+
45+
return family, base_number, enhancement_number
46+
47+
48+
def sort_controls(controls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
49+
"""
50+
Sort controls by family, then by base control number, then by enhancement number.
51+
52+
Args:
53+
controls: List of control dictionaries
54+
55+
Returns:
56+
Sorted list of controls
57+
"""
58+
def sort_key(control: Dict[str, Any]) -> Tuple[str, int, int]:
59+
"""Generate sort key for a control."""
60+
control_id = control.get('id', '')
61+
try:
62+
return parse_control_id(control_id)
63+
except ValueError as e:
64+
print(f"Warning: {e}", file=sys.stderr)
65+
# Fallback: put malformed IDs at the end
66+
return ('ZZZ', 9999, 9999)
67+
68+
return sorted(controls, key=sort_key)
69+
70+
71+
def validate_json_structure(data: Dict[str, Any]) -> None:
72+
"""
73+
Validate that the JSON has the expected structure.
74+
75+
Args:
76+
data: Parsed JSON data
77+
78+
Raises:
79+
ValueError: If structure is not as expected
80+
"""
81+
if not isinstance(data, dict):
82+
raise ValueError("JSON root must be an object")
83+
84+
if 'controls' not in data:
85+
raise ValueError("JSON must contain a 'controls' key")
86+
87+
if not isinstance(data['controls'], list):
88+
raise ValueError("'controls' must be an array")
89+
90+
# Check a few sample controls for expected structure
91+
controls = data['controls']
92+
if controls:
93+
sample_control = controls[0]
94+
required_fields = ['id', 'family']
95+
for field in required_fields:
96+
if field not in sample_control:
97+
print(f"Warning: Control missing expected field '{field}'", file=sys.stderr)
98+
99+
100+
def print_sorting_summary(original_controls: List[Dict[str, Any]],
101+
sorted_controls: List[Dict[str, Any]]) -> None:
102+
"""Print a summary of the sorting operation."""
103+
104+
# Count controls by family
105+
def count_by_family(controls):
106+
family_counts = {}
107+
for control in controls:
108+
family = control.get('family', 'Unknown')
109+
if family not in family_counts:
110+
family_counts[family] = {'base': 0, 'enhancements': 0}
111+
112+
if control.get('isEnhancement', False):
113+
family_counts[family]['enhancements'] += 1
114+
else:
115+
family_counts[family]['base'] += 1
116+
return family_counts
117+
118+
original_counts = count_by_family(original_controls)
119+
sorted_counts = count_by_family(sorted_controls)
120+
121+
print(f"Processed {len(sorted_controls)} controls across {len(sorted_counts)} families:")
122+
123+
for family in sorted(sorted_counts.keys()):
124+
base_count = sorted_counts[family]['base']
125+
enh_count = sorted_counts[family]['enhancements']
126+
total = base_count + enh_count
127+
print(f" {family}: {total} total ({base_count} base, {enh_count} enhancements)")
128+
129+
# Check if any controls were moved
130+
moves = 0
131+
for i, (orig, sort) in enumerate(zip(original_controls, sorted_controls)):
132+
if orig['id'] != sort['id']:
133+
moves += 1
134+
135+
if moves > 0:
136+
print(f"\nReordered {moves} controls for proper sorting.")
137+
else:
138+
print("\nAll controls were already in correct order.")
139+
140+
141+
def main():
142+
"""Main function."""
143+
if len(sys.argv) < 2:
144+
print("Usage: python nist_sorter.py input_file.json [output_file.json]")
145+
sys.exit(1)
146+
147+
input_file = Path(sys.argv[1])
148+
output_file = Path(sys.argv[2]) if len(sys.argv) > 2 else input_file
149+
150+
if not input_file.exists():
151+
print(f"Error: Input file '{input_file}' not found.")
152+
sys.exit(1)
153+
154+
try:
155+
# Read and parse JSON
156+
print(f"Reading {input_file}...")
157+
with open(input_file, 'r', encoding='utf-8') as f:
158+
data = json.load(f)
159+
160+
# Validate structure
161+
validate_json_structure(data)
162+
163+
# Get controls array
164+
original_controls = data['controls']
165+
print(f"Found {len(original_controls)} controls to sort.")
166+
167+
# Sort controls
168+
print("Sorting controls...")
169+
sorted_controls = sort_controls(original_controls)
170+
171+
# Update data with sorted controls
172+
data['controls'] = sorted_controls
173+
174+
# Write output
175+
print(f"Writing sorted controls to {output_file}...")
176+
with open(output_file, 'w', encoding='utf-8') as f:
177+
json.dump(data, f, indent=2, ensure_ascii=False)
178+
179+
# Print summary
180+
print_sorting_summary(original_controls, sorted_controls)
181+
print(f"\nSorting complete! Output saved to: {output_file}")
182+
183+
except json.JSONDecodeError as e:
184+
print(f"Error: Invalid JSON in '{input_file}': {e}")
185+
sys.exit(1)
186+
except Exception as e:
187+
print(f"Error: {e}")
188+
sys.exit(1)
189+
190+
191+
if __name__ == "__main__":
192+
main()

0 commit comments

Comments
 (0)