regscale-cli 6.27.1.0__py3-none-any.whl → 6.27.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/_version.py +1 -1
- regscale/core/app/utils/app_utils.py +41 -7
- regscale/integrations/commercial/aws/scanner.py +3 -2
- regscale/integrations/commercial/microsoft_defender/defender_api.py +1 -1
- regscale/integrations/commercial/sicura/api.py +65 -29
- regscale/integrations/commercial/sicura/scanner.py +36 -7
- regscale/integrations/commercial/tenablev2/commands.py +4 -4
- regscale/integrations/commercial/tenablev2/scanner.py +1 -2
- regscale/integrations/commercial/wizv2/scanner.py +40 -16
- regscale/integrations/public/cci_importer.py +400 -9
- regscale/models/integration_models/aqua.py +2 -2
- regscale/models/integration_models/cisa_kev_data.json +76 -3
- regscale/models/integration_models/flat_file_importer/__init__.py +4 -6
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.2.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.2.0.dist-info}/RECORD +23 -23
- tests/regscale/integrations/commercial/test_sicura.py +0 -1
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +86 -0
- tests/regscale/integrations/public/test_cci.py +596 -1
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.2.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.2.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.2.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.2.0.dist-info}/top_level.txt +0 -0
|
@@ -6,10 +6,11 @@ import xml.etree.ElementTree as ET
|
|
|
6
6
|
from typing import Dict, List, Optional, Tuple
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
|
+
from rich.progress import Progress, TaskID
|
|
9
10
|
|
|
10
11
|
from regscale.core.app.application import Application
|
|
11
12
|
from regscale.core.app.utils.app_utils import create_progress_object, error_and_exit
|
|
12
|
-
from regscale.models.regscale_models import Catalog, SecurityControl, CCI
|
|
13
|
+
from regscale.models.regscale_models import Catalog, SecurityControl, CCI, ControlObjective
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger("regscale")
|
|
15
16
|
|
|
@@ -30,6 +31,7 @@ class CCIImporter:
|
|
|
30
31
|
"""
|
|
31
32
|
self.xml_data = xml_data
|
|
32
33
|
self.normalized_cci: Dict[str, List[Dict]] = {}
|
|
34
|
+
self.cci_grouped_by_index: Dict[str, str] = {}
|
|
33
35
|
self.verbose = verbose
|
|
34
36
|
self.reference_version = version
|
|
35
37
|
self._user_context: Optional[Tuple[Optional[str], int]] = None
|
|
@@ -46,6 +48,222 @@ class CCIImporter:
|
|
|
46
48
|
parts = ref_index.strip().split()
|
|
47
49
|
return parts[0] if parts else ""
|
|
48
50
|
|
|
51
|
+
@staticmethod
|
|
52
|
+
def format_index(index: str) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Format index according to ControlObjective matching requirements.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
'AC-1 a 1' -> 'AC-1(a)(1)'
|
|
58
|
+
'IA-13 (03) (a)' -> 'IA-13(03)(a)'
|
|
59
|
+
'AC-1 a 1 (a)' -> 'AC-1(a)(1)(a)'
|
|
60
|
+
|
|
61
|
+
:param str index: Raw index string from XML
|
|
62
|
+
:return: Formatted index string
|
|
63
|
+
:rtype: str
|
|
64
|
+
"""
|
|
65
|
+
import re
|
|
66
|
+
|
|
67
|
+
index = index.strip()
|
|
68
|
+
|
|
69
|
+
# Pattern: match either (text) or non-whitespace text
|
|
70
|
+
pattern = r"\([^)]+\)|[^\s()]+"
|
|
71
|
+
parts = re.findall(pattern, index)
|
|
72
|
+
|
|
73
|
+
if len(parts) <= 1:
|
|
74
|
+
return parts[0] if parts else index
|
|
75
|
+
|
|
76
|
+
# First part is the base control (e.g., 'AC-1', 'IA-13')
|
|
77
|
+
result = parts[0]
|
|
78
|
+
|
|
79
|
+
# Process remaining parts
|
|
80
|
+
for part in parts[1:]:
|
|
81
|
+
if part.startswith("("):
|
|
82
|
+
# Already has parentheses, just append
|
|
83
|
+
result += part
|
|
84
|
+
else:
|
|
85
|
+
# Need to add parentheses
|
|
86
|
+
result += f"({part})"
|
|
87
|
+
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def parse_objective_id(objective_id: str) -> Tuple[Optional[str], Optional[str]]:
|
|
92
|
+
"""
|
|
93
|
+
Parse an objective otherId to extract control base and part.
|
|
94
|
+
|
|
95
|
+
Supports both NIST 800-53 Revision 4 and 5 formats:
|
|
96
|
+
|
|
97
|
+
Revision 5 Examples:
|
|
98
|
+
"ac-1_smt.a" -> ("AC-1", "a")
|
|
99
|
+
"ac-2.3_smt.a" -> ("AC-2(3)", "a")
|
|
100
|
+
"au-10.1_smt.a" -> ("AU-10(1)", "a")
|
|
101
|
+
"ac-2.4_smt" -> ("AC-2(4)", None)
|
|
102
|
+
|
|
103
|
+
Revision 4 Examples:
|
|
104
|
+
"ac-1_smt.a.1" -> ("AC-1", "a")
|
|
105
|
+
"ac-1_smt.b.2" -> ("AC-1", "b")
|
|
106
|
+
"ac-2.3_smt.d" -> ("AC-2(3)", "d")
|
|
107
|
+
|
|
108
|
+
:param str objective_id: Objective otherId value
|
|
109
|
+
:return: Tuple of (control_base, part_letter or None)
|
|
110
|
+
:rtype: Tuple[Optional[str], Optional[str]]
|
|
111
|
+
"""
|
|
112
|
+
import re
|
|
113
|
+
|
|
114
|
+
# Pattern 1: xx-nn[.nn]_smt.x[.nn] (with part letter, optional subpart for rev 4)
|
|
115
|
+
# Matches: ac-1_smt.a, ac-1_smt.a.1, ac-2.3_smt.d
|
|
116
|
+
match = re.match(r"^([a-z]+)-(\d+)(?:\.(\d+))?_smt\.([a-z]+)(?:\.\d+)?$", objective_id.lower())
|
|
117
|
+
|
|
118
|
+
if match:
|
|
119
|
+
family = match.group(1).upper()
|
|
120
|
+
control_num = match.group(2)
|
|
121
|
+
enhancement = match.group(3)
|
|
122
|
+
part = match.group(4)
|
|
123
|
+
else:
|
|
124
|
+
# Pattern 2: xx-nn[.nn]_smt[.x][.nn] (without clear part letter, or just enhancement)
|
|
125
|
+
# Matches: ac-2.4_smt, ac-1_smt
|
|
126
|
+
match = re.match(r"^([a-z]+)-(\d+)(?:\.(\d+))?_smt(?:\.[a-z]+)?(?:\.\d+)?$", objective_id.lower())
|
|
127
|
+
if not match:
|
|
128
|
+
return None, None
|
|
129
|
+
|
|
130
|
+
family = match.group(1).upper()
|
|
131
|
+
control_num = match.group(2)
|
|
132
|
+
enhancement = match.group(3)
|
|
133
|
+
part = None
|
|
134
|
+
|
|
135
|
+
if enhancement:
|
|
136
|
+
# Enhancement like AC-2(3)
|
|
137
|
+
control_base = f"{family}-{control_num}({enhancement})"
|
|
138
|
+
else:
|
|
139
|
+
# Base control like AC-1
|
|
140
|
+
control_base = f"{family}-{control_num}"
|
|
141
|
+
|
|
142
|
+
return control_base, part
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def find_matching_ccis(control_base: str, part: Optional[str], cci_map: Dict[str, str]) -> List[str]:
|
|
146
|
+
"""
|
|
147
|
+
Find all CCI IDs that match the control base and part.
|
|
148
|
+
|
|
149
|
+
Examples:
|
|
150
|
+
control_base="AC-1", part="a" matches:
|
|
151
|
+
- AC-1(a)(1)(a)
|
|
152
|
+
- AC-1(a)(1)(b)
|
|
153
|
+
- AC-1(a)(2)
|
|
154
|
+
- AC-1(a)
|
|
155
|
+
|
|
156
|
+
:param str control_base: Control identifier (e.g., "AC-1", "AC-2(3)")
|
|
157
|
+
:param Optional[str] part: Part letter (e.g., "a", "b") or None for enhancements
|
|
158
|
+
:param Dict[str, str] cci_map: Map of formatted index to comma-separated CCI IDs
|
|
159
|
+
:return: List of CCI ID strings (comma-separated)
|
|
160
|
+
:rtype: List[str]
|
|
161
|
+
"""
|
|
162
|
+
matching_ccis = []
|
|
163
|
+
|
|
164
|
+
for index, cci_ids in cci_map.items():
|
|
165
|
+
# Check if index starts with control_base
|
|
166
|
+
if index.startswith(control_base):
|
|
167
|
+
# Extract the part after control_base
|
|
168
|
+
remainder = index[len(control_base) :]
|
|
169
|
+
|
|
170
|
+
# For enhancements without parts, only match exact control (no remainder)
|
|
171
|
+
# And Check if the remainder starts with (part)
|
|
172
|
+
if (part is None and remainder == "") or remainder.startswith(f"({part})") or remainder == f"({part})":
|
|
173
|
+
matching_ccis.append(cci_ids)
|
|
174
|
+
|
|
175
|
+
return matching_ccis
|
|
176
|
+
|
|
177
|
+
def find_matching_ccis_by_name(self, control_base: str, name: str, cci_map: Dict[str, str]) -> List[str]:
|
|
178
|
+
"""
|
|
179
|
+
Find CCIs by matching control base and objective name field.
|
|
180
|
+
Fallback method when otherId matching fails.
|
|
181
|
+
|
|
182
|
+
Supports both NIST 800-53 Revision 4 and 5 label formats:
|
|
183
|
+
|
|
184
|
+
Revision 5 Examples:
|
|
185
|
+
control_base="AC-2(4)", name="AC-2(4)" -> matches AC-2(4)
|
|
186
|
+
control_base="AC-1", name="a" -> matches AC-1(a), AC-1(a)(1), etc.
|
|
187
|
+
|
|
188
|
+
Revision 4 Examples:
|
|
189
|
+
control_base="AC-1", name="a.1." -> matches AC-1(a), AC-1(a)(1), etc.
|
|
190
|
+
control_base="AC-1", name="b.2." -> matches AC-1(b), AC-1(b)(2), etc.
|
|
191
|
+
|
|
192
|
+
:param str control_base: Control identifier
|
|
193
|
+
:param str name: Objective name field
|
|
194
|
+
:param Dict[str, str] cci_map: Map of formatted index to comma-separated CCI IDs
|
|
195
|
+
:return: List of CCI ID strings (comma-separated)
|
|
196
|
+
:rtype: List[str]
|
|
197
|
+
"""
|
|
198
|
+
import re
|
|
199
|
+
|
|
200
|
+
matching_ccis = []
|
|
201
|
+
|
|
202
|
+
# Remove trailing period and whitespace from name for matching
|
|
203
|
+
clean_name = name.strip().rstrip(".").strip()
|
|
204
|
+
|
|
205
|
+
# If name exactly matches control_base, match that control
|
|
206
|
+
if clean_name == control_base:
|
|
207
|
+
if control_base in cci_map:
|
|
208
|
+
matching_ccis.append(cci_map[control_base])
|
|
209
|
+
return matching_ccis
|
|
210
|
+
|
|
211
|
+
# Try to extract part letter from different formats
|
|
212
|
+
part = None
|
|
213
|
+
|
|
214
|
+
# Check if name is a single letter (Revision 5 format: "a", "b")
|
|
215
|
+
if len(clean_name) == 1 and clean_name.isalpha():
|
|
216
|
+
part = clean_name.lower()
|
|
217
|
+
elif match := re.match(r"^([a-z])\.\d+$", clean_name.lower()):
|
|
218
|
+
part = match.group(1)
|
|
219
|
+
|
|
220
|
+
# If we extracted a part letter, try matching
|
|
221
|
+
if part:
|
|
222
|
+
matching_ccis = self._extract_part_letter(cci_map, control_base, part, matching_ccis)
|
|
223
|
+
|
|
224
|
+
return matching_ccis
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def _extract_part_letter(
|
|
228
|
+
cci_map: Dict[str, str], control_base: str, part: str, matching_ccis: List[str]
|
|
229
|
+
) -> List[str]:
|
|
230
|
+
"""
|
|
231
|
+
Extract the part letter from the name.
|
|
232
|
+
|
|
233
|
+
:param Dict[str, str] cci_map: The map of CCI IDs to their indices.
|
|
234
|
+
:param str control_base: The control base to match against.
|
|
235
|
+
:param str part: The part letter to match against.
|
|
236
|
+
:param List[str] matching_ccis: The list of matching CCI IDs.
|
|
237
|
+
:rtype: List[str]
|
|
238
|
+
"""
|
|
239
|
+
for index, cci_ids in cci_map.items():
|
|
240
|
+
if index.startswith(control_base):
|
|
241
|
+
remainder = index[len(control_base) :]
|
|
242
|
+
if remainder.startswith(f"({part})") or remainder == f"({part})":
|
|
243
|
+
matching_ccis.append(cci_ids)
|
|
244
|
+
return matching_ccis
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def ccis_already_present(current_other_id: str, new_cci_ids: str) -> bool:
|
|
248
|
+
"""
|
|
249
|
+
Check if any of the new CCI IDs are already present in current otherId.
|
|
250
|
+
Prevents duplicate CCI mappings.
|
|
251
|
+
|
|
252
|
+
:param str current_other_id: Current otherId value
|
|
253
|
+
:param str new_cci_ids: Comma-separated string of CCI IDs to add
|
|
254
|
+
:return: True if any CCIs are already present
|
|
255
|
+
:rtype: bool
|
|
256
|
+
"""
|
|
257
|
+
if not current_other_id:
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
# Extract individual CCI IDs from both strings
|
|
261
|
+
existing_ccis: set[str] = {cci.strip() for cci in current_other_id.split(",") if cci.strip().startswith("CCI-")}
|
|
262
|
+
new_ccis: set[str] = {cci.strip() for cci in new_cci_ids.split(",") if cci.strip().startswith("CCI-")}
|
|
263
|
+
|
|
264
|
+
# Check if there's any overlap
|
|
265
|
+
return bool(existing_ccis & new_ccis)
|
|
266
|
+
|
|
49
267
|
@staticmethod
|
|
50
268
|
def _extract_cci_data(cci_item: ET.Element) -> Tuple[Optional[str], str]:
|
|
51
269
|
"""
|
|
@@ -104,20 +322,49 @@ class CCIImporter:
|
|
|
104
322
|
|
|
105
323
|
def parse_cci(self) -> None:
|
|
106
324
|
"""
|
|
107
|
-
Parse CCI items from XML and
|
|
325
|
+
Parse CCI items from XML and create both mapping structures.
|
|
326
|
+
|
|
327
|
+
Creates:
|
|
328
|
+
- normalized_cci: Dict[control_id, List[Dict]] - for SecurityControl mapping
|
|
329
|
+
- cci_grouped_by_index: Dict[formatted_index, str] - for ControlObjective mapping
|
|
108
330
|
|
|
109
331
|
:rtype: None
|
|
110
332
|
"""
|
|
111
333
|
if self.verbose:
|
|
112
334
|
logger.info("Parsing CCI items from XML...")
|
|
113
335
|
|
|
336
|
+
# Track all CCI items with formatted indices for objective mapping
|
|
337
|
+
from collections import defaultdict
|
|
338
|
+
|
|
339
|
+
temp_grouped: Dict[str, List[str]] = defaultdict(list)
|
|
340
|
+
|
|
114
341
|
for cci_item in self.xml_data.findall(".//{http://iase.disa.mil/cci}cci_item"):
|
|
115
342
|
cci_id, definition = self._extract_cci_data(cci_item)
|
|
116
343
|
if not cci_id:
|
|
117
344
|
continue
|
|
118
345
|
|
|
119
346
|
references = cci_item.findall(".//{http://iase.disa.mil/cci}reference")
|
|
120
|
-
|
|
347
|
+
|
|
348
|
+
for ref in references:
|
|
349
|
+
if not self._is_valid_reference(ref):
|
|
350
|
+
continue
|
|
351
|
+
|
|
352
|
+
ref_index = ref.get("index")
|
|
353
|
+
if ref_index:
|
|
354
|
+
# Existing: simple control ID for SecurityControl mapping
|
|
355
|
+
main_control = self._parse_control_id(ref_index)
|
|
356
|
+
self._add_cci_to_control(main_control, cci_id, definition)
|
|
357
|
+
|
|
358
|
+
# NEW: formatted index for ControlObjective mapping
|
|
359
|
+
formatted_index = self.format_index(ref_index)
|
|
360
|
+
temp_grouped[formatted_index].append(cci_id)
|
|
361
|
+
|
|
362
|
+
# Convert to comma-separated format
|
|
363
|
+
self.cci_grouped_by_index = {index: ", ".join(cci_list) for index, cci_list in temp_grouped.items()}
|
|
364
|
+
|
|
365
|
+
if self.verbose:
|
|
366
|
+
logger.info(f"Created {len(self.normalized_cci)} control mappings")
|
|
367
|
+
logger.info(f"Created {len(self.cci_grouped_by_index)} formatted index mappings")
|
|
121
368
|
|
|
122
369
|
@staticmethod
|
|
123
370
|
def _get_catalog(catalog_id: int) -> Catalog:
|
|
@@ -324,9 +571,116 @@ class CCIImporter:
|
|
|
324
571
|
"created": created_count,
|
|
325
572
|
"updated": updated_count,
|
|
326
573
|
"skipped": skipped_count,
|
|
327
|
-
"total_processed":
|
|
574
|
+
"total_processed": len(self.normalized_cci),
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
def map_to_control_objectives(self, catalog_id: int = 1) -> Dict[str, int]:
|
|
578
|
+
"""
|
|
579
|
+
Map grouped CCI data to control objectives in the database.
|
|
580
|
+
Updates the otherId field of existing ControlObjective records.
|
|
581
|
+
|
|
582
|
+
:param int catalog_id: ID of the catalog containing control objectives (default: 1)
|
|
583
|
+
:return: Dictionary with operation statistics
|
|
584
|
+
:rtype: Dict[str, int]
|
|
585
|
+
"""
|
|
586
|
+
if self.verbose:
|
|
587
|
+
logger.info("Mapping CCI data to control objectives...")
|
|
588
|
+
|
|
589
|
+
# Fetch all objectives for the catalog
|
|
590
|
+
objectives: List[ControlObjective] = ControlObjective.get_by_catalog(catalog_id=catalog_id)
|
|
591
|
+
|
|
592
|
+
if self.verbose:
|
|
593
|
+
logger.info(f"Found {len(objectives)} objectives in catalog {catalog_id}")
|
|
594
|
+
|
|
595
|
+
objectives_updated = 0
|
|
596
|
+
objectives_skipped = 0
|
|
597
|
+
objectives_not_found = 0
|
|
598
|
+
|
|
599
|
+
with create_progress_object() as progress:
|
|
600
|
+
logger.info(f"Processing {len(objectives)} objectives...")
|
|
601
|
+
task = progress.add_task("Mapping CCIs to objectives...", total=len(objectives))
|
|
602
|
+
|
|
603
|
+
for obj in objectives:
|
|
604
|
+
objective_id = obj.otherId
|
|
605
|
+
|
|
606
|
+
# Skip objectives without proper otherId
|
|
607
|
+
if not objective_id or "_smt" not in objective_id:
|
|
608
|
+
objectives_not_found += 1
|
|
609
|
+
progress.update(task, advance=1)
|
|
610
|
+
continue
|
|
611
|
+
|
|
612
|
+
# Extract just the objective ID part (before any CCIs)
|
|
613
|
+
# Format: "ac-1_smt.a" or "ac-1_smt.a, CCI-000001, CCI-000002"
|
|
614
|
+
objective_id_parts = objective_id.split(",")
|
|
615
|
+
base_objective_id = objective_id_parts[0].strip()
|
|
616
|
+
|
|
617
|
+
# Parse the objective ID
|
|
618
|
+
control_base, part = self.parse_objective_id(base_objective_id)
|
|
619
|
+
|
|
620
|
+
if not control_base:
|
|
621
|
+
objectives_not_found += 1
|
|
622
|
+
progress.update(task, advance=1)
|
|
623
|
+
continue
|
|
624
|
+
|
|
625
|
+
# Find matching CCIs by otherId
|
|
626
|
+
matching_ccis = self.find_matching_ccis(control_base, part, self.cci_grouped_by_index)
|
|
627
|
+
|
|
628
|
+
# Fallback: try matching by name
|
|
629
|
+
if not matching_ccis and obj.name:
|
|
630
|
+
matching_ccis = self.find_matching_ccis_by_name(control_base, obj.name, self.cci_grouped_by_index)
|
|
631
|
+
|
|
632
|
+
if matching_ccis:
|
|
633
|
+
skipped_count, updated_count = self._handle_matching_ccis(
|
|
634
|
+
control_objective=obj,
|
|
635
|
+
matching_ccis=matching_ccis,
|
|
636
|
+
base_objective_id=base_objective_id,
|
|
637
|
+
)
|
|
638
|
+
objectives_skipped += skipped_count
|
|
639
|
+
objectives_updated += updated_count
|
|
640
|
+
else:
|
|
641
|
+
objectives_not_found += 1
|
|
642
|
+
|
|
643
|
+
progress.update(task, advance=1)
|
|
644
|
+
|
|
645
|
+
return {
|
|
646
|
+
"updated": objectives_updated,
|
|
647
|
+
"skipped": objectives_skipped,
|
|
648
|
+
"not_found": objectives_not_found,
|
|
649
|
+
"total_processed": len(objectives),
|
|
328
650
|
}
|
|
329
651
|
|
|
652
|
+
def _handle_matching_ccis(
|
|
653
|
+
self,
|
|
654
|
+
control_objective: ControlObjective,
|
|
655
|
+
matching_ccis: List[str],
|
|
656
|
+
base_objective_id: str,
|
|
657
|
+
) -> Tuple[int, int]:
|
|
658
|
+
"""
|
|
659
|
+
Handle matching CCIs.
|
|
660
|
+
|
|
661
|
+
:param ControlObjective control_objective: ControlObjective instance
|
|
662
|
+
:param List[str] matching_ccis: List of matching CCI IDs
|
|
663
|
+
:param str base_objective_id: Base objective ID
|
|
664
|
+
:return: Tuple of (number of objectives skipped, number of objectives updated)
|
|
665
|
+
:rtype: Tuple[int, int]
|
|
666
|
+
"""
|
|
667
|
+
# Combine all CCI IDs
|
|
668
|
+
all_cci_ids = ", ".join(matching_ccis)
|
|
669
|
+
|
|
670
|
+
# Check for duplicates
|
|
671
|
+
if self.ccis_already_present(control_objective.otherId, all_cci_ids):
|
|
672
|
+
if self.verbose:
|
|
673
|
+
logger.info(f"Skipping {base_objective_id} - CCIs already present")
|
|
674
|
+
return 1, 0
|
|
675
|
+
|
|
676
|
+
# Update the otherId field
|
|
677
|
+
control_objective.otherId = f"{control_objective.otherId}, {all_cci_ids}"
|
|
678
|
+
control_objective.save()
|
|
679
|
+
|
|
680
|
+
if self.verbose:
|
|
681
|
+
logger.info(f"Updated {base_objective_id} with {all_cci_ids}")
|
|
682
|
+
return 0, 1
|
|
683
|
+
|
|
330
684
|
def get_normalized_cci(self) -> Dict[str, List[Dict]]:
|
|
331
685
|
"""
|
|
332
686
|
Get the normalized CCI data.
|
|
@@ -385,7 +739,25 @@ def _display_results(stats: Dict[str, int]) -> None:
|
|
|
385
739
|
)
|
|
386
740
|
|
|
387
741
|
|
|
388
|
-
def
|
|
742
|
+
def _display_objective_results(stats: Dict[str, int]) -> None:
|
|
743
|
+
"""
|
|
744
|
+
Display control objective mapping results.
|
|
745
|
+
|
|
746
|
+
:param Dict[str, int] stats: Dictionary with operation statistics
|
|
747
|
+
:rtype: None
|
|
748
|
+
"""
|
|
749
|
+
logger.info(
|
|
750
|
+
f"[green]\nControl objective operations completed:"
|
|
751
|
+
f"[green]\n - Updated: {stats['updated']}"
|
|
752
|
+
f"[green]\n - Skipped: {stats['skipped']}"
|
|
753
|
+
f"[green]\n - Not found: {stats['not_found']}"
|
|
754
|
+
f"[green]\n - Total processed: {stats['total_processed']}",
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _process_cci_import(
|
|
759
|
+
importer: CCIImporter, dry_run: bool, verbose: bool, catalog_id: int, disable_objectives: bool = False
|
|
760
|
+
) -> None:
|
|
389
761
|
"""
|
|
390
762
|
Process CCI import with optional database operations.
|
|
391
763
|
|
|
@@ -393,6 +765,7 @@ def _process_cci_import(importer: CCIImporter, dry_run: bool, verbose: bool, cat
|
|
|
393
765
|
:param bool dry_run: Whether to skip database operations
|
|
394
766
|
:param bool verbose: Whether to display verbose output
|
|
395
767
|
:param int catalog_id: ID of the catalog containing security controls
|
|
768
|
+
:param bool disable_objectives: Whether to disable mapping to control objectives
|
|
396
769
|
:rtype: None
|
|
397
770
|
"""
|
|
398
771
|
importer.parse_cci()
|
|
@@ -404,8 +777,15 @@ def _process_cci_import(importer: CCIImporter, dry_run: bool, verbose: bool, cat
|
|
|
404
777
|
_display_verbose_output(normalized_data)
|
|
405
778
|
|
|
406
779
|
if not dry_run:
|
|
780
|
+
# Map to SecurityControl (existing functionality)
|
|
407
781
|
stats = importer.map_to_security_controls(catalog_id)
|
|
408
782
|
_display_results(stats)
|
|
783
|
+
|
|
784
|
+
# Map to ControlObjective (new functionality)
|
|
785
|
+
if not disable_objectives:
|
|
786
|
+
logger.info("\n[cyan]Mapping CCIs to control objectives...[/cyan]")
|
|
787
|
+
obj_stats = importer.map_to_control_objectives(catalog_id)
|
|
788
|
+
_display_objective_results(obj_stats)
|
|
409
789
|
else:
|
|
410
790
|
logger.info("\n[yellow]DRY RUN MODE: No database changes were made[/yellow]")
|
|
411
791
|
|
|
@@ -422,10 +802,21 @@ def _process_cci_import(importer: CCIImporter, dry_run: bool, verbose: bool, cat
|
|
|
422
802
|
@click.option(
|
|
423
803
|
"--catalog-id", "-c", type=click.INT, default=1, help="ID of the catalog containing security controls (default: 1)"
|
|
424
804
|
)
|
|
425
|
-
|
|
426
|
-
""
|
|
805
|
+
@click.option(
|
|
806
|
+
"--disable-objectives",
|
|
807
|
+
"-o",
|
|
808
|
+
is_flag=True,
|
|
809
|
+
help="Disable mapping CCIs to control objectives (updates otherId field)",
|
|
810
|
+
)
|
|
811
|
+
def cci_importer(
|
|
812
|
+
xml_file: str, dry_run: bool, verbose: bool, nist_version: str, catalog_id: int, disable_objectives: bool
|
|
813
|
+
) -> None:
|
|
814
|
+
"""Import CCI data from XML files and map to security controls and/or objectives.
|
|
815
|
+
|
|
816
|
+
By default, maps CCIs to SecurityControl entities. Use --disable-objectives flag
|
|
817
|
+
to also update ControlObjective.otherId fields with CCI mappings.
|
|
427
818
|
|
|
428
|
-
If no XML file is specified, defaults to
|
|
819
|
+
If no XML file is specified, defaults to packaged CCI_List.xml.
|
|
429
820
|
"""
|
|
430
821
|
|
|
431
822
|
try:
|
|
@@ -438,6 +829,6 @@ def cci_importer(xml_file: str, dry_run: bool, verbose: bool, nist_version: str,
|
|
|
438
829
|
xml_file = str(cci_path)
|
|
439
830
|
root = _load_xml_file(xml_file)
|
|
440
831
|
importer = CCIImporter(root, version=nist_version, verbose=verbose)
|
|
441
|
-
_process_cci_import(importer, dry_run, verbose, catalog_id)
|
|
832
|
+
_process_cci_import(importer, dry_run, verbose, catalog_id, disable_objectives)
|
|
442
833
|
except Exception as e:
|
|
443
834
|
error_and_exit(f"Unexpected error: {e}")
|
|
@@ -210,13 +210,13 @@ class Aqua(FlatFileImporter):
|
|
|
210
210
|
self.logger.error(f"Error creating finding: {e}")
|
|
211
211
|
return None
|
|
212
212
|
|
|
213
|
-
def determine_cvss_severity(self, dat: dict) ->
|
|
213
|
+
def determine_cvss_severity(self, dat: dict) -> IssueSeverity:
|
|
214
214
|
"""
|
|
215
215
|
Determine the CVSS severity of the vulnerability
|
|
216
216
|
|
|
217
217
|
:param dict dat: Data row from CSV file
|
|
218
218
|
:return: A severity derived from the CVSS scores
|
|
219
|
-
:rtype:
|
|
219
|
+
:rtype: IssueSeverity
|
|
220
220
|
"""
|
|
221
221
|
precedence_order = [
|
|
222
222
|
self.nvd_cvss_v3_severity,
|
|
@@ -1,9 +1,82 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "CISA Catalog of Known Exploited Vulnerabilities",
|
|
3
|
-
"catalogVersion": "2025.10.
|
|
4
|
-
"dateReleased": "2025-10-
|
|
5
|
-
"count":
|
|
3
|
+
"catalogVersion": "2025.10.20",
|
|
4
|
+
"dateReleased": "2025-10-20T13:56:54.0593Z",
|
|
5
|
+
"count": 1447,
|
|
6
6
|
"vulnerabilities": [
|
|
7
|
+
{
|
|
8
|
+
"cveID": "CVE-2022-48503",
|
|
9
|
+
"vendorProject": "Apple",
|
|
10
|
+
"product": "Multiple Products",
|
|
11
|
+
"vulnerabilityName": "Apple Multiple Products Unspecified Vulnerability",
|
|
12
|
+
"dateAdded": "2025-10-20",
|
|
13
|
+
"shortDescription": "Apple macOS, iOS, tvOS, Safari, and watchOS contain an unspecified vulnerability in JavaScriptCore that when processing web content may lead to arbitrary code execution. The impacted product could be end-of-life (EoL) and\/or end-of-service (EoS). Users should discontinue product utilization.",
|
|
14
|
+
"requiredAction": "Apply mitigations per vendor instructions, follow applicable BOD 22-01 guidance for cloud services, or discontinue use of the product if mitigations are unavailable.",
|
|
15
|
+
"dueDate": "2025-11-10",
|
|
16
|
+
"knownRansomwareCampaignUse": "Unknown",
|
|
17
|
+
"notes": "https:\/\/support.apple.com\/en-us\/HT213340 ; https:\/\/support.apple.com\/en-us\/HT213341 ; https:\/\/support.apple.com\/en-us\/HT213342 ; https:\/\/support.apple.com\/en-us\/HT213345 ; https:\/\/support.apple.com\/en-us\/HT213346 ; https:\/\/nvd.nist.gov\/vuln\/detail\/CVE-2022-48503",
|
|
18
|
+
"cwes": []
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"cveID": "CVE-2025-2746",
|
|
22
|
+
"vendorProject": "Kentico",
|
|
23
|
+
"product": "Xperience CMS",
|
|
24
|
+
"vulnerabilityName": "Kentico Xperience CMS Authentication Bypass Using an Alternate Path or Channel Vulnerability",
|
|
25
|
+
"dateAdded": "2025-10-20",
|
|
26
|
+
"shortDescription": "Kentico Xperience CMS contains an authentication bypass using an alternate path or channel vulnerability that could allow an attacker to control administrative objects.",
|
|
27
|
+
"requiredAction": "Apply mitigations per vendor instructions, follow applicable BOD 22-01 guidance for cloud services, or discontinue use of the product if mitigations are unavailable.",
|
|
28
|
+
"dueDate": "2025-11-10",
|
|
29
|
+
"knownRansomwareCampaignUse": "Unknown",
|
|
30
|
+
"notes": "https:\/\/devnet.kentico.com\/download\/hotfixes ; https:\/\/nvd.nist.gov\/vuln\/detail\/CVE-2025-2746",
|
|
31
|
+
"cwes": [
|
|
32
|
+
"CWE-288"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"cveID": "CVE-2025-2747",
|
|
37
|
+
"vendorProject": "Kentico",
|
|
38
|
+
"product": "Xperience CMS",
|
|
39
|
+
"vulnerabilityName": "Kentico Xperience CMS Authentication Bypass Using an Alternate Path or Channel Vulnerability",
|
|
40
|
+
"dateAdded": "2025-10-20",
|
|
41
|
+
"shortDescription": "Kentico Xperience CMS contains an authentication bypass using an alternate path or channel vulnerability that could allow an attacker to control administrative objects.",
|
|
42
|
+
"requiredAction": "Apply mitigations per vendor instructions, follow applicable BOD 22-01 guidance for cloud services, or discontinue use of the product if mitigations are unavailable.",
|
|
43
|
+
"dueDate": "2025-11-10",
|
|
44
|
+
"knownRansomwareCampaignUse": "Unknown",
|
|
45
|
+
"notes": "https:\/\/devnet.kentico.com\/download\/hotfixes ; https:\/\/nvd.nist.gov\/vuln\/detail\/CVE-2025-2747",
|
|
46
|
+
"cwes": [
|
|
47
|
+
"CWE-288"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"cveID": "CVE-2025-33073",
|
|
52
|
+
"vendorProject": "Microsoft",
|
|
53
|
+
"product": "Windows",
|
|
54
|
+
"vulnerabilityName": "Microsoft Windows SMB Client Improper Access Control Vulnerability",
|
|
55
|
+
"dateAdded": "2025-10-20",
|
|
56
|
+
"shortDescription": "Microsoft Windows SMB Client contains an improper access control vulnerability that could allow for privilege escalation. An attacker could execute a specially crafted malicious script to coerce the victim machine to connect back to the attack system using SMB and authenticate.",
|
|
57
|
+
"requiredAction": "Apply mitigations per vendor instructions, follow applicable BOD 22-01 guidance for cloud services, or discontinue use of the product if mitigations are unavailable.",
|
|
58
|
+
"dueDate": "2025-11-10",
|
|
59
|
+
"knownRansomwareCampaignUse": "Unknown",
|
|
60
|
+
"notes": "https:\/\/msrc.microsoft.com\/update-guide\/en-US\/advisory\/CVE-2025-33073 ; https:\/\/nvd.nist.gov\/vuln\/detail\/CVE-2025-33073",
|
|
61
|
+
"cwes": [
|
|
62
|
+
"CWE-284"
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"cveID": "CVE-2025-61884",
|
|
67
|
+
"vendorProject": "Oracle",
|
|
68
|
+
"product": "E-Business Suite",
|
|
69
|
+
"vulnerabilityName": "Oracle E-Business Suite Server-Side Request Forgery (SSRF) Vulnerability",
|
|
70
|
+
"dateAdded": "2025-10-20",
|
|
71
|
+
"shortDescription": "Oracle E-Business Suite contains a server-side request forgery (SSRF) vulnerability in the Runtime component of Oracle Configurator. This vulnerability is remotely exploitable without authentication.",
|
|
72
|
+
"requiredAction": "Apply mitigations per vendor instructions, follow applicable BOD 22-01 guidance for cloud services, or discontinue use of the product if mitigations are unavailable.",
|
|
73
|
+
"dueDate": "2025-11-10",
|
|
74
|
+
"knownRansomwareCampaignUse": "Unknown",
|
|
75
|
+
"notes": "https:\/\/www.oracle.com\/security-alerts\/alert-cve-2025-61884.html ; https:\/\/nvd.nist.gov\/vuln\/detail\/CVE-2025-61884",
|
|
76
|
+
"cwes": [
|
|
77
|
+
"CWE-918"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
7
80
|
{
|
|
8
81
|
"cveID": "CVE-2025-54253",
|
|
9
82
|
"vendorProject": "Adobe",
|
|
@@ -930,13 +930,13 @@ class FlatFileImporter(ABC):
|
|
|
930
930
|
return dict_content
|
|
931
931
|
|
|
932
932
|
@staticmethod
|
|
933
|
-
def determine_severity(s: str) ->
|
|
933
|
+
def determine_severity(s: Optional[str] = None) -> IssueSeverity:
|
|
934
934
|
"""
|
|
935
935
|
Determine the CVSS severity of the vulnerability
|
|
936
936
|
|
|
937
|
-
:param str s: The severity
|
|
937
|
+
:param Optional[str] s: The severity, defaults to None
|
|
938
938
|
:return: The severity
|
|
939
|
-
:rtype:
|
|
939
|
+
:rtype: IssueSeverity
|
|
940
940
|
"""
|
|
941
941
|
mapping = {
|
|
942
942
|
"critical": IssueSeverity.Critical,
|
|
@@ -949,9 +949,7 @@ class FlatFileImporter(ABC):
|
|
|
949
949
|
"info": IssueSeverity.NotAssigned,
|
|
950
950
|
"unknown": IssueSeverity.NotAssigned,
|
|
951
951
|
}
|
|
952
|
-
severity = "info"
|
|
953
|
-
if s:
|
|
954
|
-
severity = s.lower()
|
|
952
|
+
severity = s.lower() if s else "info"
|
|
955
953
|
return mapping.get(severity, IssueSeverity.NotAssigned)
|
|
956
954
|
|
|
957
955
|
@staticmethod
|