Skip to content

Commit b4a3f6c

Browse files
author
Peter Wagstaff
committed
Merge branch 'update-cnssi-1253-2022' into 'main'
Update CNSSI and Classified Information overlays to 2022 versions See merge request sl5tf/control-overlays-selector!4
2 parents f77f8aa + 5d44306 commit b4a3f6c

25 files changed

Lines changed: 31056 additions & 14 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.DS_Store
2+
__pycache__
466 KB
Binary file not shown.
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Extract Classified Information Overlay 2022 controls from Section 6.
4+
5+
This script extracts control specifications from the 2022 version of the
6+
Classified Information Overlay PDF, which has a simpler format than the
7+
previous version.
8+
9+
Usage:
10+
python extract_classified_information_2022.py <pdf_file>
11+
"""
12+
13+
import fitz # PyMuPDF
14+
import json
15+
import re
16+
import sys
17+
from typing import Dict, Optional, Tuple
18+
19+
def parse_control_header(text: str) -> Optional[Tuple[str, str]]:
20+
"""
21+
Parse a control header line to extract control ID and name.
22+
23+
Examples:
24+
- "AC-3(4), Access Enforcement | Discretionary Access Controls"
25+
- "AC-5, Separation of Duties"
26+
- "AC-1, (Access Control) Policy and Procedures"
27+
28+
Returns: (control_id, control_name) or None
29+
"""
30+
# Pattern for base controls and enhancements
31+
pattern = r'^([A-Z]{2}-\d{1,2}(?:\(\d+\))?),\s*(.+)$'
32+
match = re.match(pattern, text.strip())
33+
if match:
34+
control_id = match.group(1)
35+
control_name = match.group(2).strip()
36+
37+
# Remove family name in parentheses at the beginning if present
38+
# e.g., "(Access Control) Policy and Procedures" -> "Policy and Procedures"
39+
family_pattern = r'^\([^)]+\)\s*(.+)$'
40+
family_match = re.match(family_pattern, control_name)
41+
if family_match:
42+
control_name = family_match.group(1)
43+
44+
return control_id, control_name
45+
return None
46+
47+
def extract_controls_from_pdf(pdf_path: str) -> Dict:
48+
"""
49+
Extract all controls from Section 6 of the PDF.
50+
51+
Returns a dictionary mapping control IDs to their specifications.
52+
"""
53+
doc = fitz.open(pdf_path)
54+
controls = {}
55+
56+
# Find the start of Section 6
57+
section_6_start = None
58+
section_7_start = None
59+
60+
for page_num in range(len(doc)):
61+
page = doc[page_num]
62+
text = page.get_text()
63+
64+
if "6. Detailed Overlay Control Specifications" in text or "6. Detailed Overlay Control Specifications" in text:
65+
section_6_start = page_num
66+
print(f"Found Section 6 on page {page_num + 1}")
67+
elif section_6_start is not None and ("7." in text and "Implementation Considerations" in text):
68+
section_7_start = page_num
69+
print(f"Found Section 7 on page {page_num + 1}")
70+
break
71+
72+
if section_6_start is None:
73+
print("ERROR: Could not find Section 6")
74+
return controls
75+
76+
# Process pages in Section 6
77+
# Include the page where section 7 starts since it may have controls before section 7
78+
end_page = (section_7_start + 1) if section_7_start else len(doc)
79+
80+
current_control = None
81+
current_field = None
82+
83+
for page_num in range(section_6_start, end_page):
84+
page = doc[page_num]
85+
text = page.get_text()
86+
87+
# Split into lines and process
88+
lines = text.split('\n')
89+
90+
for i, line in enumerate(lines):
91+
line = line.strip()
92+
93+
# Stop if we hit Section 7
94+
if "7." in line and "Implementation Considerations" in line:
95+
print(f"Stopping at Section 7 on page {page_num + 1}")
96+
doc.close()
97+
return controls
98+
99+
# Skip empty lines and page headers/footers
100+
if not line or line == "Classified System Overlay" or line.isdigit():
101+
continue
102+
if "Attachment 5 to Appendix E" in line:
103+
continue
104+
if line == "09/30/2022":
105+
continue
106+
107+
# Check if this is a control header
108+
control_info = parse_control_header(line)
109+
if control_info:
110+
control_id, control_name = control_info
111+
current_control = control_id
112+
current_field = None
113+
114+
controls[control_id] = {
115+
"control_id": control_id,
116+
"name": control_name,
117+
"selected": True, # All controls in this overlay are selected
118+
"justification": None,
119+
"parameter_value": None,
120+
"guidance": None,
121+
"references": None
122+
}
123+
continue
124+
125+
# Check for field headers
126+
if current_control:
127+
if line.startswith("Justification to Select:"):
128+
current_field = "justification"
129+
content = line[len("Justification to Select:"):].strip()
130+
if content:
131+
controls[current_control]["justification"] = content
132+
elif line.startswith("Parameter Value:"):
133+
current_field = "parameter_value"
134+
content = line[len("Parameter Value:"):].strip()
135+
if content:
136+
controls[current_control]["parameter_value"] = content
137+
elif line.startswith("Guidance:"):
138+
current_field = "guidance"
139+
content = line[len("Guidance:"):].strip()
140+
if content:
141+
controls[current_control]["guidance"] = content
142+
elif line.startswith("Reference(s):") or line.startswith("Reference:"):
143+
current_field = "references"
144+
content = line[line.find(":") + 1:].strip()
145+
if content:
146+
controls[current_control]["references"] = content
147+
# Continue previous field
148+
elif current_field and line:
149+
# Check if this might be a new control (safety check)
150+
if not parse_control_header(line):
151+
if controls[current_control][current_field]:
152+
controls[current_control][current_field] += " " + line
153+
else:
154+
controls[current_control][current_field] = line
155+
156+
doc.close()
157+
return controls
158+
159+
def print_summary(controls: Dict):
160+
"""Print a summary of extracted controls."""
161+
print(f"\n=== EXTRACTION SUMMARY ===")
162+
print(f"Total controls extracted: {len(controls)}")
163+
164+
if not controls:
165+
print("No controls found!")
166+
return
167+
168+
# Count by family
169+
families = {}
170+
base_controls = 0
171+
enhancements = 0
172+
173+
for control_id in controls.keys():
174+
family = control_id.split('-')[0]
175+
families[family] = families.get(family, 0) + 1
176+
177+
if '(' in control_id:
178+
enhancements += 1
179+
else:
180+
base_controls += 1
181+
182+
print(f"Base controls: {base_controls}")
183+
print(f"Enhancements: {enhancements}")
184+
185+
print("\nControls by family:")
186+
for family, count in sorted(families.items()):
187+
print(f" {family}: {count}")
188+
189+
# Show some examples
190+
print("\nFirst 5 controls:")
191+
for i, (control_id, control) in enumerate(list(controls.items())[:5]):
192+
print(f"\n{control_id}: {control['name']}")
193+
if control['justification']:
194+
print(f" Justification: {control['justification'][:100]}...")
195+
if control['parameter_value']:
196+
print(f" Parameter: {control['parameter_value'][:100]}...")
197+
if control['guidance']:
198+
print(f" Guidance: {control['guidance'][:100]}...")
199+
if control['references']:
200+
print(f" References: {control['references']}")
201+
202+
def main():
203+
if len(sys.argv) < 2:
204+
print("Usage: python extract_classified_information.py <pdf_file>")
205+
sys.exit(1)
206+
207+
pdf_file = sys.argv[1]
208+
output_file = "extracted_classified_information.json"
209+
210+
print("Extracting Classified Information Overlay...")
211+
controls = extract_controls_from_pdf(pdf_file)
212+
213+
if controls:
214+
# Save to JSON
215+
with open(output_file, 'w', encoding='utf-8') as f:
216+
json.dump(controls, f, indent=2, ensure_ascii=False)
217+
print(f"\nSaved {len(controls)} controls to {output_file}")
218+
219+
print_summary(controls)
220+
else:
221+
print("No controls were extracted.")
222+
223+
if __name__ == "__main__":
224+
main()

0 commit comments

Comments
 (0)