regscale-cli 6.25.1.0__py3-none-any.whl → 6.27.0.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 (146) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +19 -4
  4. regscale/core/app/internal/evidence.py +419 -2
  5. regscale/core/app/internal/login.py +0 -1
  6. regscale/core/app/utils/catalog_utils/common.py +1 -1
  7. regscale/dev/code_gen.py +24 -20
  8. regscale/integrations/commercial/jira.py +367 -126
  9. regscale/integrations/commercial/qualys/__init__.py +7 -8
  10. regscale/integrations/commercial/qualys/scanner.py +8 -3
  11. regscale/integrations/commercial/sicura/api.py +14 -13
  12. regscale/integrations/commercial/sicura/commands.py +8 -2
  13. regscale/integrations/commercial/sicura/scanner.py +49 -39
  14. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  15. regscale/integrations/commercial/synqly/assets.py +17 -0
  16. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  17. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  18. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  19. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  20. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  21. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  22. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  23. regscale/integrations/commercial/wizv2/click.py +64 -79
  24. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  25. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  26. regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
  27. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  28. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  29. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  30. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  31. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  33. regscale/integrations/commercial/wizv2/issue.py +1 -1
  34. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  35. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  36. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  37. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  38. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  39. regscale/integrations/commercial/wizv2/reports.py +1 -1
  40. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  41. regscale/integrations/commercial/wizv2/scanner.py +39 -99
  42. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  43. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  44. regscale/integrations/commercial/wizv2/variables.py +89 -3
  45. regscale/integrations/compliance_integration.py +60 -41
  46. regscale/integrations/control_matcher.py +377 -0
  47. regscale/integrations/due_date_handler.py +14 -8
  48. regscale/integrations/milestone_manager.py +291 -0
  49. regscale/integrations/public/__init__.py +1 -0
  50. regscale/integrations/public/cci_importer.py +37 -38
  51. regscale/integrations/public/fedramp/click.py +60 -2
  52. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  53. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  54. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  55. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  56. regscale/integrations/scanner_integration.py +277 -153
  57. regscale/models/integration_models/cisa_kev_data.json +282 -9
  58. regscale/models/integration_models/nexpose.py +36 -10
  59. regscale/models/integration_models/qualys.py +3 -4
  60. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  61. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  62. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  63. regscale/models/locking.py +12 -8
  64. regscale/models/platform.py +1 -2
  65. regscale/models/regscale_models/control_implementation.py +47 -22
  66. regscale/models/regscale_models/issue.py +256 -95
  67. regscale/models/regscale_models/milestone.py +1 -1
  68. regscale/models/regscale_models/regscale_model.py +6 -1
  69. regscale/templates/__init__.py +0 -0
  70. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
  71. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
  72. tests/regscale/integrations/commercial/__init__.py +0 -0
  73. tests/regscale/integrations/commercial/conftest.py +28 -0
  74. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  75. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  76. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  77. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  78. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  79. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  80. tests/regscale/integrations/commercial/test_burp.py +48 -0
  81. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  82. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  83. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  84. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  85. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  86. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  87. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  88. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  89. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  90. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  91. tests/regscale/integrations/commercial/test_snow.py +423 -0
  92. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  93. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  94. tests/regscale/integrations/commercial/test_stig.py +33 -0
  95. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  96. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  97. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  98. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  99. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  100. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  101. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  102. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  103. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  104. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  105. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  106. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  107. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  108. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  109. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  110. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  111. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  112. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  113. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  114. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  115. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  116. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  117. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  118. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  119. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  120. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  121. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  122. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  123. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  124. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  125. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  126. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  127. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  128. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  129. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  130. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
  131. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  132. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  133. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  134. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  135. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  136. tests/regscale/integrations/public/test_fedramp.py +301 -0
  137. tests/regscale/integrations/test_control_matcher.py +1397 -0
  138. tests/regscale/integrations/test_control_matching.py +155 -0
  139. tests/regscale/integrations/test_milestone_manager.py +408 -0
  140. tests/regscale/models/test_issue.py +378 -1
  141. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  142. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  143. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
  144. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
  145. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
  146. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
regscale/_version.py CHANGED
@@ -33,7 +33,7 @@ def get_version_from_pyproject() -> str:
33
33
  return match.group(1)
34
34
  except Exception:
35
35
  pass
36
- return "6.25.1.0" # fallback version
36
+ return "6.27.0.0" # fallback version
37
37
 
38
38
 
39
39
  __version__ = get_version_from_pyproject()
@@ -2,9 +2,9 @@
2
2
 
3
3
  from typing import Optional
4
4
 
5
- from regscale.regscale import cli
6
- from regscale.models.click_models import ClickCommand
7
5
  from regscale.airflow.click_mixins import AirflowClickGroup
6
+ from regscale.models.click_models import ClickCommand
7
+ from regscale.regscale import cli
8
8
 
9
9
  AIRFLOW_CLICK_GROUP = AirflowClickGroup.from_group(cli, prefix="regscale")
10
10
  AIRFLOW_CLICK_OPERATORS = AIRFLOW_CLICK_GROUP.flatten_operator()
@@ -269,7 +269,7 @@ class Application(metaclass=Singleton):
269
269
  "token": DEFAULT_POPULATED,
270
270
  "userId": "enter RegScale user id here",
271
271
  "useMilestones": False,
272
- "preventAutoClose": True,
272
+ "preventAutoClose": False,
273
273
  "otx": "enter AlienVault API key here",
274
274
  "wizAccessToken": DEFAULT_POPULATED,
275
275
  "wizAuthUrl": "https://auth.wiz.io/oauth/token",
@@ -664,7 +664,8 @@ class Application(metaclass=Singleton):
664
664
 
665
665
  def save_config(self, conf: dict) -> None:
666
666
  """
667
- Save Configuration to init.yaml
667
+ Save Configuration to init.yaml using atomic file operations to prevent corruption
668
+ during parallel writes.
668
669
 
669
670
  :param dict conf: Application configuration
670
671
  :rtype: None
@@ -681,8 +682,22 @@ class Application(metaclass=Singleton):
681
682
  try:
682
683
  self.logger.debug(f"Saving config to {self.config_file}.")
683
684
  with self._config_lock:
684
- with open(self.config_file, "w", encoding="utf-8") as file:
685
- yaml.dump(conf, file)
685
+ # Use atomic file operations: write to temp file, then rename
686
+ # This prevents corruption when multiple processes write simultaneously
687
+ import tempfile
688
+
689
+ config_dir = os.path.dirname(self.config_file) or "."
690
+ temp_fd, temp_path = tempfile.mkstemp(dir=config_dir, prefix=".tmp_", suffix=".yaml", text=True)
691
+ try:
692
+ with os.fdopen(temp_fd, "w", encoding="utf-8") as temp_file:
693
+ yaml.dump(conf, temp_file)
694
+ # Atomic rename - this is atomic on POSIX systems
695
+ os.replace(temp_path, self.config_file)
696
+ except Exception:
697
+ # Clean up temp file if something goes wrong
698
+ with contextlib.suppress(OSError):
699
+ os.unlink(temp_path)
700
+ raise
686
701
  except OSError:
687
702
  self.logger.error(f"Could not save config to {self.config_file}.")
688
703
 
@@ -24,7 +24,8 @@ from regscale.core.app.api import Api
24
24
  from regscale.core.app.application import Application
25
25
  from regscale.core.app.utils.app_utils import check_file_path, create_progress_object, error_and_exit
26
26
  from regscale.models.app_models.click import regscale_ssp_id
27
- from regscale.models.regscale_models import Assessment, File, Project, SecurityPlan
27
+ from regscale.models.regscale_models import Assessment, File, Project, SecurityPlan, Evidence, Component
28
+ from regscale.models.regscale_models.control_implementation import ControlImplementation
28
29
 
29
30
  logger = getLogger("regscale")
30
31
 
@@ -198,7 +199,7 @@ def package_builder(ssp_id: int, path: Path):
198
199
  app = Application()
199
200
  api = Api()
200
201
  with create_progress_object() as progress:
201
- task = progress.add_task("[white]Building and zipping evidence folder for audit...", total=6)
202
+ task = progress.add_task("[white]Building and zipping evidence folder for audit...", total=8)
202
203
  try:
203
204
  # Obtaining MEGA Api for given Organizer Record.
204
205
  ssp = SecurityPlan.fetch_mega_api_data(ssp_id)
@@ -221,6 +222,16 @@ def package_builder(ssp_id: int, path: Path):
221
222
 
222
223
  progress.update(task, advance=1)
223
224
 
225
+ # Process evidence lockers at SSP level
226
+ process_ssp_evidence_lockers(
227
+ ssp_id=ssp_id,
228
+ path=path,
229
+ module_folder=module_folder,
230
+ api=api,
231
+ )
232
+
233
+ progress.update(task, advance=1)
234
+
224
235
  # Checking MEGA Api for Attachments at Control level
225
236
  process_control_attachments(
226
237
  ssp=ssp,
@@ -231,6 +242,19 @@ def package_builder(ssp_id: int, path: Path):
231
242
  api=api,
232
243
  task=task,
233
244
  )
245
+
246
+ progress.update(task, advance=1)
247
+
248
+ # Process components and their evidence
249
+ process_components_evidence(
250
+ ssp_id=ssp_id,
251
+ path=path,
252
+ module_folder=module_folder,
253
+ api=api,
254
+ )
255
+
256
+ progress.update(task, advance=1)
257
+
234
258
  # Creating zip file and removing temporary Evidence Folder
235
259
  new_path = Path("./evidence.zip")
236
260
  zip_folder(path, new_path)
@@ -349,6 +373,9 @@ def process_control_attachments(
349
373
  # Adding any Attachments at Control level to corresponding folder
350
374
  _download_control_attachments(control_attachments, api, path, module_folder_name)
351
375
 
376
+ # Process evidence lockers for controls
377
+ _process_control_evidence_lockers(control_attachments, api, path, module_folder_name)
378
+
352
379
  progress.update(task, advance=1)
353
380
 
354
381
  else:
@@ -388,6 +415,396 @@ def _download_control_attachments(
388
415
  json.dump(f, file_drop, indent=4, separators=(", ", ": "))
389
416
 
390
417
 
418
+ def _get_control_folder_name(control_attachments: list[dict], control_id: int) -> str | None:
419
+ """
420
+ Get the control folder name for a given control ID
421
+
422
+ :param list[dict] control_attachments: List of control attachments
423
+ :param int control_id: Control ID to find folder name for
424
+ :return: Control folder name or None
425
+ :rtype: str | None
426
+ """
427
+ for f in control_attachments:
428
+ if f["parentId"] == control_id:
429
+ return f["controlId"]
430
+ return None
431
+
432
+
433
+ def _download_control_evidence_items(
434
+ evidence_items: list[dict], control_folder_name: str, path: Path, module_folder_name: str, api: Api
435
+ ) -> None:
436
+ """
437
+ Download evidence items for a control
438
+
439
+ :param list[dict] evidence_items: List of evidence items
440
+ :param str control_folder_name: Name of the control folder
441
+ :param Path path: Base path for downloads
442
+ :param str module_folder_name: Module folder name
443
+ :param Api api: API object
444
+ :rtype: None
445
+ """
446
+ logger.info(f"Found {len(evidence_items)} evidence items for control {control_folder_name}")
447
+
448
+ for evidence_item in evidence_items:
449
+ file_name = evidence_item.get("trustedDisplayName", f"evidence_{evidence_item.get('id', 'unknown')}")
450
+ output_path = f"{path}/{module_folder_name}/{control_folder_name}/{file_name}"
451
+
452
+ if download_evidence_file(api, evidence_item, output_path):
453
+ logger.info(f"Downloaded evidence file: {file_name}")
454
+ else:
455
+ logger.warning(f"Failed to download evidence file: {file_name}")
456
+
457
+
458
+ def _process_control_evidence_lockers(
459
+ control_attachments: list[dict], api: Api, path: Path, module_folder_name: str
460
+ ) -> None:
461
+ """
462
+ Process evidence lockers for controls
463
+
464
+ :param list[dict] control_attachments: List of control attachments
465
+ :param Api api: RegScale CLI API object
466
+ :param Path path: directory for file location
467
+ :param str module_folder_name: name of the module folder
468
+ :rtype: None
469
+ """
470
+ # Get unique control IDs
471
+ control_ids = list({f["parentId"] for f in control_attachments})
472
+
473
+ for control_id in control_ids:
474
+ try:
475
+ # Get evidence from evidence lockers for this control
476
+ evidence_items = get_evidence_by_control(api, control_id)
477
+
478
+ if evidence_items:
479
+ # Find the control ID for folder naming
480
+ control_folder_name = _get_control_folder_name(control_attachments, control_id)
481
+
482
+ if control_folder_name:
483
+ _download_control_evidence_items(evidence_items, control_folder_name, path, module_folder_name, api)
484
+ except Exception as e:
485
+ logger.warning(f"Failed to process evidence lockers for control {control_id}: {e}")
486
+
487
+
488
+ def get_evidence_by_control(api: Api, control_id: int) -> list[dict]:
489
+ """
490
+ Get evidence for a specific control
491
+
492
+ :param Api api: RegScale CLI API object (kept for backward compatibility)
493
+ :param int control_id: Control ID
494
+ :return: List of evidence items
495
+ :rtype: list[dict]
496
+ """
497
+ # Suppress unused parameter warning for backward compatibility
498
+ _ = api
499
+
500
+ try:
501
+ # Use Evidence model method instead of direct API call
502
+ evidence_items = Evidence.get_all_by_parent(parent_id=control_id, parent_module="controls")
503
+ # Convert to dict format for compatibility
504
+ return [evidence.dict() for evidence in evidence_items]
505
+ except Exception as e:
506
+ logger.warning(f"Failed to get evidence for control {control_id}: {e}")
507
+ return []
508
+
509
+
510
+ def get_evidence_by_security_plan(api: Api, ssp_id: int) -> list[dict]:
511
+ """
512
+ Get evidence for a specific security plan
513
+
514
+ :param Api api: RegScale CLI API object (kept for backward compatibility)
515
+ :param int ssp_id: Security Plan ID
516
+ :return: List of evidence items
517
+ :rtype: list[dict]
518
+ """
519
+ # Suppress unused parameter warning for backward compatibility
520
+ _ = api
521
+
522
+ try:
523
+ # Use Evidence model method instead of direct API call
524
+ evidence_items = Evidence.get_all_by_parent(parent_id=ssp_id, parent_module="securityplans")
525
+ # Convert to dict format for compatibility
526
+ return [evidence.dict() for evidence in evidence_items]
527
+ except Exception as e:
528
+ logger.warning(f"Failed to get evidence for security plan {ssp_id}: {e}")
529
+ return []
530
+
531
+
532
+ def get_components_by_ssp(api: Api, ssp_id: int) -> list[dict]:
533
+ """
534
+ Get components for a specific security plan
535
+
536
+ :param Api api: RegScale CLI API object (kept for backward compatibility)
537
+ :param int ssp_id: Security Plan ID
538
+ :return: List of active components
539
+ :rtype: list[dict]
540
+ """
541
+ # Suppress unused parameter warning for backward compatibility
542
+ _ = api
543
+
544
+ try:
545
+ # Use Component model method instead of direct API call
546
+ components = Component.get_all_by_parent(parent_id=ssp_id, parent_module="securityplans")
547
+ # Filter for active components only and convert to dict format
548
+ return [comp.dict() for comp in components if comp.status == "Active"]
549
+ except Exception as e:
550
+ logger.warning(f"Failed to get components for security plan {ssp_id}: {e}")
551
+ return []
552
+
553
+
554
+ def get_controls_by_parent(api: Api, parent_id: int, parent_module: str) -> list[dict]:
555
+ """
556
+ Get controls for a specific parent (SSP or Component)
557
+
558
+ :param Api api: RegScale CLI API object (kept for backward compatibility)
559
+ :param int parent_id: Parent ID
560
+ :param str parent_module: Parent module (securityplans or components)
561
+ :return: List of controls
562
+ :rtype: list[dict]
563
+ """
564
+ # Suppress unused parameter warning for backward compatibility
565
+ _ = api
566
+
567
+ try:
568
+ # Use ControlImplementation model method instead of direct API call
569
+ controls = ControlImplementation.get_all_by_parent(parent_id=parent_id, parent_module=parent_module)
570
+ # Convert to dict format for compatibility
571
+ return [control.dict() for control in controls]
572
+ except Exception as e:
573
+ logger.warning(f"Failed to get controls for parent {parent_id} in module {parent_module}: {e}")
574
+ return []
575
+
576
+
577
+ def download_evidence_file(api: Api, evidence_item: dict, output_path: str) -> bool:
578
+ """
579
+ Download an evidence file
580
+
581
+ :param Api api: RegScale CLI API object
582
+ :param dict evidence_item: Evidence item data
583
+ :param str output_path: Path to save the file
584
+ :return: True if successful, False otherwise
585
+ :rtype: bool
586
+ """
587
+ try:
588
+ file_data = File.download_file_from_regscale_to_memory(
589
+ api=api,
590
+ record_id=evidence_item["parentId"],
591
+ module=evidence_item["parentModule"],
592
+ stored_name=evidence_item["trustedStorageName"],
593
+ file_hash=evidence_item.get("fileHash") or evidence_item.get("shaHash"),
594
+ )
595
+
596
+ if file_data is None:
597
+ logger.warning(f"No data received for evidence file {evidence_item.get('trustedDisplayName', 'unknown')}")
598
+ return False
599
+
600
+ with open(output_path, "wb") as f:
601
+ f.write(file_data)
602
+ return True
603
+ except Exception as e:
604
+ logger.warning(f"Failed to download evidence file {evidence_item.get('trustedDisplayName', 'unknown')}: {e}")
605
+ return False
606
+
607
+
608
+ def process_ssp_evidence_lockers(ssp_id: int, path: Path, module_folder: Path, api: Api) -> None:
609
+ """
610
+ Process evidence lockers at SSP level
611
+
612
+ :param int ssp_id: Security Plan ID
613
+ :param Path path: directory for file location
614
+ :param str module_folder_name: name of the module folder
615
+ :param Path module_folder: path to module folder
616
+ :param Api api: RegScale CLI API object
617
+ :rtype: None
618
+ """
619
+ try:
620
+ # Get evidence from evidence lockers for the SSP
621
+ evidence_items = get_evidence_by_security_plan(api, ssp_id)
622
+
623
+ if evidence_items:
624
+ logger.info(f"Found {len(evidence_items)} evidence items from evidence lockers for SSP {ssp_id}")
625
+
626
+ for evidence_item in evidence_items:
627
+ file_name = evidence_item.get("trustedDisplayName", f"evidence_{evidence_item.get('id', 'unknown')}")
628
+ output_path = module_folder / file_name
629
+
630
+ if download_evidence_file(api, evidence_item, str(output_path)):
631
+ logger.info(f"Downloaded evidence file: {file_name}")
632
+ else:
633
+ logger.warning(f"Failed to download evidence file: {file_name}")
634
+ else:
635
+ logger.info("No evidence found in evidence lockers for SSP")
636
+
637
+ except Exception as e:
638
+ logger.warning(f"Error processing SSP evidence lockers: {e}")
639
+
640
+
641
+ def _download_files_for_parent(
642
+ parent_id: int, parent_module: str, output_folder: Path, api: Api, module_name: str = None
643
+ ) -> None:
644
+ """
645
+ Generalized function to download files for any parent module
646
+
647
+ :param int parent_id: Parent ID (component, control, etc.)
648
+ :param str parent_module: Parent module name (components, controls, etc.)
649
+ :param Path output_folder: Path to output folder
650
+ :param Api api: API object
651
+ :param str module_name: Human-readable module name for logging (optional)
652
+ :rtype: None
653
+ """
654
+ if module_name is None:
655
+ module_name = parent_module
656
+
657
+ try:
658
+ # Use File model method instead of direct API call
659
+ files_data = File.get_files_for_parent_from_regscale(api=api, parent_id=parent_id, parent_module=parent_module)
660
+
661
+ for file_item in files_data:
662
+ file_name = file_item.trustedDisplayName or f"file_{file_item.id}"
663
+ output_path = output_folder / file_name
664
+
665
+ try:
666
+ file_data = File.download_file_from_regscale_to_memory(
667
+ api=api,
668
+ record_id=file_item.id,
669
+ module=parent_module,
670
+ stored_name=file_item.trustedStorageName,
671
+ file_hash=file_item.fileHash or file_item.shaHash,
672
+ )
673
+
674
+ if file_data is None:
675
+ logger.warning(f"No data received for {module_name} file {file_name}")
676
+ continue
677
+
678
+ with open(output_path, "wb") as f:
679
+ f.write(file_data)
680
+ logger.info(f"Downloaded {module_name} file: {file_name}")
681
+ except Exception as e:
682
+ logger.warning(f"Failed to download {module_name} file {file_name}: {e}")
683
+ except Exception as e:
684
+ logger.warning(f"Failed to get {module_name} files for {parent_module} {parent_id}: {e}")
685
+
686
+
687
+ def _download_component_files(component_id: int, component_folder: Path, api: Api) -> None:
688
+ """
689
+ Download files directly attached to a component
690
+
691
+ :param int component_id: Component ID
692
+ :param Path component_folder: Path to component folder
693
+ :param Api api: API object
694
+ :rtype: None
695
+ """
696
+ _download_files_for_parent(
697
+ parent_id=component_id,
698
+ parent_module="components",
699
+ output_folder=component_folder,
700
+ api=api,
701
+ module_name="component",
702
+ )
703
+
704
+
705
+ def _download_control_files(control_id: int, control_folder: Path, api: Api) -> None:
706
+ """
707
+ Download files for a control
708
+
709
+ :param int control_id: Control ID
710
+ :param Path control_folder: Path to control folder
711
+ :param Api api: API object
712
+ :rtype: None
713
+ """
714
+ _download_files_for_parent(
715
+ parent_id=control_id, parent_module="controls", output_folder=control_folder, api=api, module_name="control"
716
+ )
717
+
718
+
719
+ def _download_control_evidence(control_id: int, control_folder: Path, api: Api) -> None:
720
+ """
721
+ Download evidence from evidence lockers for a control
722
+
723
+ :param int control_id: Control ID
724
+ :param Path control_folder: Path to control folder
725
+ :param Api api: API object
726
+ :rtype: None
727
+ """
728
+ evidence_items = get_evidence_by_control(api, control_id)
729
+
730
+ if evidence_items:
731
+ logger.info(f"Found {len(evidence_items)} evidence items for control {control_folder.name}")
732
+
733
+ for evidence_item in evidence_items:
734
+ file_name = evidence_item.get("trustedDisplayName", f"evidence_{evidence_item.get('id', 'unknown')}")
735
+ output_path = control_folder / file_name
736
+
737
+ if download_evidence_file(api, evidence_item, str(output_path)):
738
+ logger.info(f"Downloaded evidence file: {file_name}")
739
+ else:
740
+ logger.warning(f"Failed to download evidence file: {file_name}")
741
+
742
+
743
+ def _process_component_controls(component_id: int, component_folder: Path, api: Api) -> None:
744
+ """
745
+ Process controls for a component
746
+
747
+ :param int component_id: Component ID
748
+ :param Path component_folder: Path to component folder
749
+ :param Api api: API object
750
+ :rtype: None
751
+ """
752
+ controls = get_controls_by_parent(api, component_id, "components")
753
+
754
+ if controls:
755
+ logger.info(f"Found {len(controls)} controls for component {component_folder.name}")
756
+
757
+ for control in controls:
758
+ control_id = control.get("id")
759
+ control_name = control.get("controlId", f"Control_{control_id}")
760
+
761
+ # Create control folder within component folder
762
+ control_folder = component_folder / control_name
763
+ os.makedirs(control_folder, exist_ok=True)
764
+
765
+ # Download control files and evidence
766
+ _download_control_files(control_id, control_folder, api)
767
+ _download_control_evidence(control_id, control_folder, api)
768
+
769
+
770
+ def process_components_evidence(ssp_id: int, path: Path, module_folder: Path, api: Api) -> None:
771
+ """
772
+ Process components and their evidence
773
+
774
+ :param int ssp_id: Security Plan ID
775
+ :param Path path: directory for file location
776
+ :param Path module_folder: path to module folder
777
+ :param Api api: RegScale CLI API object
778
+ :rtype: None
779
+ """
780
+ try:
781
+ # Get components for the SSP
782
+ components = get_components_by_ssp(api, ssp_id)
783
+
784
+ if not components:
785
+ logger.info("No active components found for SSP")
786
+ return
787
+
788
+ logger.info(f"Found {len(components)} active components for SSP {ssp_id}")
789
+
790
+ for component in components:
791
+ component_id = component.get("id")
792
+ component_title = component.get("title", f"Component_{component_id}")
793
+
794
+ # Create component folder
795
+ component_folder = module_folder / component_title
796
+ os.makedirs(component_folder, exist_ok=True)
797
+
798
+ # Download component files
799
+ _download_component_files(component_id, component_folder, api)
800
+
801
+ # Process component controls
802
+ _process_component_controls(component_id, component_folder, api)
803
+
804
+ except Exception as e:
805
+ logger.warning(f"Error processing components evidence: {e}")
806
+
807
+
391
808
  def remove_directory(directory_path: Path) -> None:
392
809
  """
393
810
  This function removes a given directory even if files stored there
@@ -136,7 +136,6 @@ def login(
136
136
  logger.info("New RegScale Token has been updated and saved in init.yaml")
137
137
  # Truncate token for logging purposes
138
138
  logger.debug("Token: %s", regscale_auth.token[:20])
139
- config["domain"] = host
140
139
  app.save_config(config)
141
140
  return regscale_auth.token
142
141
 
@@ -87,5 +87,5 @@ def objective_to_control_dot(input_string: str) -> str:
87
87
  if match:
88
88
  return match.group(1)
89
89
  else:
90
- logger.error(f"Failed to convert objective to control: {input_string}")
90
+ logger.debug(f"Failed to convert objective to control: {input_string}")
91
91
  return input_string
regscale/dev/code_gen.py CHANGED
@@ -170,29 +170,27 @@ def _build_op_kwargs_and_docstring(
170
170
  capabilities=capabilities,
171
171
  )
172
172
 
173
- if ConnectorType.Vulnerabilities.lower() in integration and param_type == "optional_params":
174
- if param_type == "optional_params" and ConnectorType.Vulnerabilities in integration:
173
+ if ConnectorType.Vulnerabilities.lower() in integration:
174
+ vuln_params = {}
175
+ if param_type == "optional_params":
175
176
  vuln_params: dict[str, Param] = {
176
- "vuln_filter": Param(
177
- name="vuln_filter",
178
- type="choice",
179
- description="Filter the vulnerabilities for the selected severity. (Options: critical, high, medium, low, info)",
180
- default=None,
181
- ),
182
177
  "scan_date": Param(
183
178
  name="scan_date",
184
179
  type="string",
185
180
  description="The date of the scan to sync vulnerabilities into RegScale.",
186
181
  default=None,
187
182
  ),
188
- "all_scans": Param(
189
- name="all_scans",
190
- type="boolean",
191
- description="Whether to sync all vulnerabilities into RegScale.",
192
- default=False,
183
+ }
184
+ elif param_type == "expected_params":
185
+ vuln_params: dict[str, Param] = {
186
+ "minimum_severity_filter": Param(
187
+ name="minimum_severity_filter",
188
+ type="choice",
189
+ description="Minimum severity of the vulnerabilities to sync. (Options: critical, high, medium, low, info, all), e.g. providing 'high' will sync all vulnerabilities with a severity of high and critical.",
190
+ default=None,
193
191
  ),
194
192
  }
195
- config[param_type] = {**config[param_type], **vuln_params}
193
+ config[param_type] = {**config[param_type], **vuln_params}
196
194
 
197
195
  # Add filter parameter for Assets and Vulnerabilities connectors
198
196
  if (
@@ -214,7 +212,7 @@ def _build_op_kwargs_and_docstring(
214
212
  doc_string += f"{proper_type}:\n"
215
213
  for param in config[param_type]:
216
214
  op_kwargs, doc_string = _build_other_params(
217
- config=config,
215
+ param_obj=config[param_type][param],
218
216
  param=param,
219
217
  param_type=param_type,
220
218
  proper_type=proper_type,
@@ -258,11 +256,13 @@ def _build_expected_params(
258
256
 
259
257
 
260
258
  def _build_other_params(
261
- config: dict, param: str, param_type: str, proper_type: str, integration: str, op_kwargs: str, doc_string: str
259
+ param_obj: Param, param: str, param_type: str, proper_type: str, integration: str, op_kwargs: str, doc_string: str
262
260
  ) -> tuple[str, str]:
263
261
  """
264
262
  Build the other params for the DAG by adding them to the op_kwargs and docstring
265
263
 
264
+ :param Param param_obj: The parameter object
265
+ :param str param: The name of the parameter
266
266
  :param str param_type: The type of parameter to build
267
267
  :param str integration: The name of the integration, typically connector_integration
268
268
  :param str op_kwargs: The op_kwargs to add to the DAG
@@ -270,7 +270,6 @@ def _build_other_params(
270
270
  :return: The op_kwargs and docstring
271
271
  :rtype: tuple[str, str]
272
272
  """
273
- param_obj: Param = config[param_type][param]
274
273
  param_name = f"{integration.lower()}_{param}" if param_type == "required_secrets" else param
275
274
  jinja_key = f"'{param_name}'"
276
275
  if default := param_obj.default:
@@ -447,14 +446,19 @@ def _build_all_params(
447
446
  :rtype: tuple[list[str], list[str], list[str]]
448
447
  """
449
448
  if connector == ConnectorType.Vulnerabilities:
450
- vuln_filter_option = "@click.option(\n '--vuln_filter',\n help='Filter the vulnerabilities for the selected severity. (Options: critical, high, medium, low, info)',\n required=False,\n type=click.Choice(['critical', 'high', 'medium', 'low', 'info']),\n default=None)\n"
449
+ vuln_filter_option = "@click.option(\n '--minimum_severity_filter','-s',\n help='Minimum severity of the vulnerabilities to sync. (Options: critical, high, medium, low, info), e.g. providing high will sync all vulnerabilities with a severity of high and critical.',\n required=False,\n type=click.Choice(['critical', 'high', 'medium', 'low', 'info']),\n default=None)\n"
451
450
  scan_date_option = f"@click.option(\n '--scan_date',\n help='The date of the scan to sync vulnerabilities from {integration_name}',\n required=False,\n type=click.DateTime(formats=['%Y-%m-%d']),\n default=None)\n"
452
451
  all_vulns_flag = f"@click.option(\n '--all_scans',\n help='Whether to sync all vulnerabilities from {integration_name}',\n required=False,\n is_flag=True,\n default=False)\n"
453
452
  click_options = ["@regscale_ssp_id()", vuln_filter_option, scan_date_option, all_vulns_flag]
454
- function_params = ["regscale_ssp_id: int", "vuln_filter: str", "scan_date: datetime", "all_scans: bool"]
453
+ function_params = [
454
+ "regscale_ssp_id: int",
455
+ "minimum_severity_filter: str",
456
+ "scan_date: datetime",
457
+ "all_scans: bool",
458
+ ]
455
459
  function_kwargs = [
456
460
  "regscale_ssp_id=regscale_ssp_id",
457
- "vuln_filter=vuln_filter",
461
+ "minimum_severity_filter=minimum_severity_filter",
458
462
  "scan_date=scan_date",
459
463
  "all_scans=all_scans",
460
464
  ]