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 ("\n Controls by family:" )
186+ for family , count in sorted (families .items ()):
187+ print (f" { family } : { count } " )
188+
189+ # Show some examples
190+ print ("\n First 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_2022.py <pdf_file>" )
205+ sys .exit (1 )
206+
207+ pdf_file = sys .argv [1 ]
208+ output_file = "extracted_classified_information_2022.json"
209+
210+ print ("Extracting Classified Information Overlay 2022..." )
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"\n Saved { 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