Skip to content

Commit e00e1b3

Browse files
author
Peter Wagstaff
committed
Improved website for new SL5 writing plan
1 parent 24f0596 commit e00e1b3

10 files changed

Lines changed: 27204 additions & 4557 deletions
Binary file not shown.
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Classified Information Overlay Control Extractor
4+
5+
Extracts control and enhancement information from the Classified Information Overlay PDF.
6+
Outputs a JSON file with all controls, enhancements, and their attributes.
7+
8+
Usage:f
9+
python classified_information_overlay_extractor.py <pdf_file>
10+
python classified_information_overlay_extractor.py <pdf_file> --debug-page N
11+
python classified_information_overlay_extractor.py <pdf_file> --search-9
12+
"""
13+
14+
import fitz # PyMuPDF for PDF parsing
15+
import json
16+
import re
17+
import sys
18+
from typing import Dict, List
19+
20+
class ClassifiedControlExtractor:
21+
"""
22+
Extracts controls and enhancements from a classified overlay PDF.
23+
"""
24+
def __init__(self, pdf_path: str):
25+
self.pdf_path = pdf_path
26+
self.controls = {} # All extracted controls
27+
self.current_control = None # Current control/enhancement being processed
28+
self.current_base_control = None # Current base control (e.g., "AC-3")
29+
self.current_attribute = None # Current attribute being appended to
30+
31+
def extract_controls(self) -> Dict:
32+
"""
33+
Extract all controls and enhancements from the PDF.
34+
"""
35+
try:
36+
doc = fitz.open(self.pdf_path)
37+
for page_num in range(len(doc)):
38+
page = doc[page_num]
39+
self._process_page(page, page_num + 1)
40+
doc.close()
41+
return self.controls
42+
except Exception as e:
43+
print(f"Error processing PDF: {e}")
44+
return {}
45+
46+
def _process_page(self, page, page_num: int):
47+
"""
48+
Process a single PDF page: extract formatted text and parse controls/attributes.
49+
"""
50+
try:
51+
text_dict = page.get_text("dict")
52+
formatted_text = self._extract_formatted_text(text_dict)
53+
self._find_controls_and_attributes(formatted_text, page_num)
54+
except Exception as e:
55+
pass
56+
57+
def _extract_formatted_text(self, text_dict: dict) -> List[dict]:
58+
"""
59+
Extract lines of text with formatting (bold, font, etc.) from a PDF text dict.
60+
Skips footers and page numbers.
61+
"""
62+
formatted_text = []
63+
for block in text_dict.get("blocks", []):
64+
if block.get("type") == 0: # Text block
65+
for line in block.get("lines", []):
66+
line_text = ""
67+
line_formats = []
68+
for span in line.get("spans", []):
69+
text = span.get("text", "")
70+
flags = span.get("flags", 0)
71+
line_text += text
72+
line_formats.append({
73+
"text": text,
74+
"bold": bool(flags & 16),
75+
"font": span.get("font", ""),
76+
"flags": flags
77+
})
78+
if line_text.strip():
79+
# Skip footers and page numbers
80+
stripped = line_text.strip()
81+
if (stripped == "Classified Information Overlay" or
82+
stripped.isdigit() or
83+
stripped == "May 9, 2014"):
84+
continue
85+
formatted_text.append({
86+
"text": stripped,
87+
"formats": line_formats
88+
})
89+
return formatted_text
90+
91+
def _flexible_enhancement_match(self, line_text: str):
92+
"""
93+
Match enhancement lines like 'Control Enhancement: 4, 5, 6' with flexible patterns.
94+
"""
95+
patterns = [
96+
r'^Control Enhancement:\s*(\d+(?:,\s*\d+)*)$',
97+
r'^Control\s+Enhancement:\s*(\d+(?:,\s*\d+)*)$',
98+
r'^Control\s*Enhancement\s*:\s*(\d+(?:,\s*\d+)*)$',
99+
r'^Control Enhancement\s*:\s*(\d+(?:,\s*\d+)*).*$',
100+
r'^Control Enhancement\s*:\s*(\d+(?:,\s*\d+)*)\s*$'
101+
]
102+
for pattern in patterns:
103+
match = re.match(pattern, line_text, re.IGNORECASE)
104+
if match:
105+
return match
106+
return None
107+
108+
def _find_controls_and_attributes(self, formatted_text: List[dict], page_num: int):
109+
"""
110+
Parse formatted lines to find controls, enhancements, and their attributes.
111+
"""
112+
for line_data in formatted_text:
113+
line_text = line_data["text"]
114+
formats = line_data["formats"]
115+
is_bold = any(fmt.get("bold", False) for fmt in formats)
116+
# Base control: e.g., "AC-3, ACCESS ENFORCEMENT"
117+
base_control_match = re.match(r'^([A-Z]{2}-\d{1,2}),?\s*(.+)$', line_text)
118+
enhancement_match = self._flexible_enhancement_match(line_text)
119+
if base_control_match and is_bold:
120+
control_id = base_control_match.group(1)
121+
control_name = base_control_match.group(2).strip()
122+
self.current_control = control_id
123+
self.current_base_control = control_id
124+
self.current_attribute = None
125+
self.controls[control_id] = {
126+
"name": control_name,
127+
"attributes": {},
128+
"page": page_num
129+
}
130+
continue
131+
elif enhancement_match:
132+
if self.current_base_control:
133+
enhancement_numbers = enhancement_match.group(1).split(',')
134+
for enhancement_num in enhancement_numbers:
135+
enhancement_num = enhancement_num.strip()
136+
enhancement_id = f"{self.current_base_control}({enhancement_num})"
137+
base_name = self.controls.get(self.current_base_control, {}).get("name", "")
138+
self.controls[enhancement_id] = {
139+
"name": base_name,
140+
"attributes": {},
141+
"page": page_num
142+
}
143+
self.current_control = f"{self.current_base_control}({enhancement_numbers[-1].strip()})"
144+
self.current_attribute = None
145+
continue
146+
# Attribute line: e.g., "Justification to Select: ..."
147+
if self.current_control and ':' in line_text:
148+
attribute_result = self._extract_attribute_from_line(line_text)
149+
if attribute_result:
150+
attr_name, attr_content = attribute_result
151+
# Append or set attribute
152+
attrs = self.controls[self.current_control]["attributes"]
153+
if attr_name in attrs:
154+
attrs[attr_name] += " " + attr_content.strip()
155+
else:
156+
attrs[attr_name] = attr_content.strip()
157+
self.current_attribute = attr_name
158+
continue
159+
# Continuation of previous attribute
160+
elif self.current_control and self.current_attribute and line_text.strip():
161+
if not (base_control_match or enhancement_match or self._extract_attribute_from_line(line_text)):
162+
attrs = self.controls[self.current_control]["attributes"]
163+
attrs[self.current_attribute] += " " + line_text.strip()
164+
165+
def _extract_attribute_from_line(self, line_text: str):
166+
"""
167+
If line starts with a known attribute, return (attribute_name, content).
168+
"""
169+
known_attributes = [
170+
"Justification to Select",
171+
"Supplemental Guidance",
172+
"Parameter Value(s)",
173+
"Parameter Value",
174+
"Regulatory/Statutory Reference(s)",
175+
"Control Extension",
176+
"Control Extension(s)",
177+
"Control Extension and Parameter Value(s)"
178+
]
179+
for attr in known_attributes:
180+
if line_text.startswith(attr + ":"):
181+
content = line_text[len(attr) + 1:].strip()
182+
return attr, content
183+
return None
184+
185+
def save_to_json(self, output_file: str):
186+
"""
187+
Save extracted controls to a JSON file.
188+
"""
189+
try:
190+
with open(output_file, 'w', encoding='utf-8') as f:
191+
json.dump(self.controls, f, indent=2, ensure_ascii=False)
192+
print(f"\nSaved {len(self.controls)} controls to {output_file}")
193+
except Exception as e:
194+
print(f"Error saving to JSON: {e}")
195+
196+
def print_summary(self):
197+
"""
198+
Print a summary of extracted controls and enhancements.
199+
"""
200+
print(f"\n=== EXTRACTION SUMMARY ===")
201+
print(f"Total controls extracted: {len(self.controls)}")
202+
if not self.controls:
203+
print("No controls found!")
204+
return
205+
families = {}
206+
enhancements = 0
207+
base_controls = 0
208+
for control_id in self.controls.keys():
209+
family = control_id.split('-')[0]
210+
families[family] = families.get(family, 0) + 1
211+
if '(' in control_id:
212+
enhancements += 1
213+
else:
214+
base_controls += 1
215+
print(f"Base controls: {base_controls}")
216+
print(f"Control enhancements: {enhancements}")
217+
print("\nControls by family:")
218+
for family, count in sorted(families.items()):
219+
print(f" {family}: {count} controls")
220+
print("\nFirst 10 controls found:")
221+
for i, (control_id, control_info) in enumerate(list(self.controls.items())[:10]):
222+
print(f" {control_id}: {control_info['name']}")
223+
if control_info['attributes']:
224+
print(f" Attributes: {list(control_info['attributes'].keys())}")
225+
else:
226+
print(f" Attributes: None found")
227+
228+
def main():
229+
if len(sys.argv) < 2:
230+
print("Usage: python classified_information_overlay_extractor.py <pdf_file>")
231+
sys.exit(1)
232+
pdf_file = sys.argv[1]
233+
output_file = "extracted_classified_information_overlay.json"
234+
print("Classified Information Overlay Control Extractor\n" + "=" * 50)
235+
extractor = ClassifiedControlExtractor(pdf_file)
236+
controls = extractor.extract_controls()
237+
if controls:
238+
extractor.save_to_json(output_file)
239+
extractor.print_summary()
240+
else:
241+
print("No controls were extracted.")
242+
243+
if __name__ == "__main__":
244+
main()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"AC-3(2)": {
3+
"name": "ACCESS ENFORCEMENT",
4+
"attributes": {
5+
"Justification to Select": "White House Memorandum, Near-term Measures to Reduce the Risk of High-Impact Unauthorized Disclosures, requires the implementation of two-stage controls (review and concurrence of a second person) for all transfers of data from a classified computer network to removable media, if the transfer is not part of an approved internal use process such as encrypted back-ups.",
6+
"Parameter Value": "The information system enforces dual authorization for all transfers of data from a classified computer network to removable media.",
7+
"Regulatory/Statutory Reference(s)": "EO 13587, Sec 6.1; White House Memorandum, Near-term Measures to Reduce the Risk of High-Impact Unauthorized Disclosures, Task D-1."
8+
},
9+
"page": 5
10+
}
11+
}

0 commit comments

Comments
 (0)