regscale-cli 6.27.1.0__py3-none-any.whl → 6.27.3.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.

Files changed (53) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -0
  3. regscale/core/app/internal/control_editor.py +73 -21
  4. regscale/core/app/internal/login.py +4 -1
  5. regscale/core/app/internal/model_editor.py +219 -64
  6. regscale/core/app/utils/app_utils.py +41 -7
  7. regscale/core/login.py +21 -4
  8. regscale/core/utils/date.py +77 -1
  9. regscale/integrations/commercial/aws/scanner.py +7 -3
  10. regscale/integrations/commercial/microsoft_defender/defender_api.py +1 -1
  11. regscale/integrations/commercial/sicura/api.py +65 -29
  12. regscale/integrations/commercial/sicura/scanner.py +36 -7
  13. regscale/integrations/commercial/synqly/query_builder.py +4 -1
  14. regscale/integrations/commercial/tenablev2/commands.py +4 -4
  15. regscale/integrations/commercial/tenablev2/scanner.py +1 -2
  16. regscale/integrations/commercial/wizv2/scanner.py +40 -16
  17. regscale/integrations/control_matcher.py +78 -23
  18. regscale/integrations/public/cci_importer.py +400 -9
  19. regscale/integrations/public/csam/csam.py +572 -763
  20. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  21. regscale/integrations/public/csam/csam_common.py +154 -0
  22. regscale/integrations/public/csam/csam_controls.py +432 -0
  23. regscale/integrations/public/csam/csam_poam.py +124 -0
  24. regscale/integrations/public/fedramp/click.py +17 -4
  25. regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
  26. regscale/integrations/public/fedramp/poam/scanner.py +74 -7
  27. regscale/integrations/scanner_integration.py +16 -1
  28. regscale/models/integration_models/aqua.py +2 -2
  29. regscale/models/integration_models/cisa_kev_data.json +121 -18
  30. regscale/models/integration_models/flat_file_importer/__init__.py +4 -6
  31. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  32. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +35 -2
  33. regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
  34. regscale/models/platform.py +3 -0
  35. regscale/models/regscale_models/__init__.py +5 -0
  36. regscale/models/regscale_models/component.py +1 -1
  37. regscale/models/regscale_models/control_implementation.py +55 -24
  38. regscale/models/regscale_models/organization.py +3 -0
  39. regscale/models/regscale_models/regscale_model.py +17 -5
  40. regscale/models/regscale_models/security_plan.py +1 -0
  41. regscale/regscale.py +11 -1
  42. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/METADATA +1 -1
  43. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/RECORD +53 -49
  44. tests/regscale/core/test_login.py +171 -4
  45. tests/regscale/integrations/commercial/test_sicura.py +0 -1
  46. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +86 -0
  47. tests/regscale/integrations/public/test_cci.py +596 -1
  48. tests/regscale/integrations/test_control_matcher.py +24 -0
  49. tests/regscale/models/test_control_implementation.py +118 -3
  50. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/LICENSE +0 -0
  51. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/WHEEL +0 -0
  52. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/entry_points.txt +0 -0
  53. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.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 normalize them, mapping to parent control only.
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
- self._process_references(references, cci_id, definition)
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": sum(created_count + updated_count + skipped_count),
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 _process_cci_import(importer: CCIImporter, dry_run: bool, verbose: bool, catalog_id: int) -> None:
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
- def cci_importer(xml_file: str, dry_run: bool, verbose: bool, nist_version: str, catalog_id: int) -> None:
426
- """Import CCI data from XML files and map to security controls.
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 'artifacts/U_CCI_List.xml' in the project directory.
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}")