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
@@ -84,6 +84,35 @@ class IssueStatus(str, Enum):
84
84
  return self.value
85
85
 
86
86
 
87
+ class IssueIdentification(str, Enum):
88
+ """Issue Identification"""
89
+
90
+ A123Review = "A-123 Review"
91
+ AssessmentAuditInternal = "Assessment/Audit (Internal)"
92
+ AssessmentAuditExternal = "Assessment/Audit (External)"
93
+ CriticalControlReview = "Critical Control Review"
94
+ FDCCUSGCB = "FDCC/USGCB"
95
+ GAOAudit = "GAO Audit"
96
+ IGAudit = "IG Audit"
97
+ IncidentResponseLessonsLearned = "Incident Response Lessons Learned"
98
+ ITAR = "ITAR"
99
+ PenetrationTest = "Penetration Test"
100
+ RiskAssessment = "Risk Assessment"
101
+ SecurityAuthorization = "Security Authorization"
102
+ SecurityControlAssessment = "Security Control Assessment"
103
+ VulnerabilityAssessment = "Vulnerability Assessment"
104
+ Other = "Other"
105
+
106
+ def __str__(self) -> str:
107
+ """
108
+ Return the value of the Enum as a string
109
+
110
+ :return: The value of the Enum as a string
111
+ :rtype: str
112
+ """
113
+ return self.value
114
+
115
+
87
116
  class Issue(RegScaleModel):
88
117
  """Issue Model"""
89
118
 
@@ -483,7 +512,6 @@ class Issue(RegScaleModel):
483
512
 
484
513
  if severity == IssueSeverity.Critical.value:
485
514
  days = cls._get_days_for_values(["critical"], config, key)
486
- start_date = start_date + datetime.timedelta(days=days)
487
515
  elif severity == IssueSeverity.High.value:
488
516
  days = cls._get_days_for_values(["high"], config, key)
489
517
  elif severity == IssueSeverity.Moderate.value:
@@ -917,103 +945,252 @@ class Issue(RegScaleModel):
917
945
  """
918
946
  import logging
919
947
 
920
- cache_disabled = cls._is_cache_disabled()
921
- use_cache: bool = not cache_disabled
922
-
923
948
  logger = logging.getLogger("regscale")
949
+
924
950
  # Check cache first
925
- if use_cache:
926
- cached_data = cls._get_from_cache(plan_id)
927
- if cached_data is not None:
928
- logger.info(f"Using cached open issues data for security plan {plan_id}")
929
- return cached_data
930
-
931
- # Performance optimization: Use larger batch size and optimize query
932
- take = 50 # Increased from 50 to reduce API roundtrips
933
- skip = 0
934
- control_issues: Dict[int, List[OpenIssueDict]] = defaultdict(list)
951
+ cached_data = cls._check_cache(plan_id, logger)
952
+ if cached_data is not None:
953
+ return cached_data
935
954
 
936
- start_time = time.time()
937
- logger.info(f"Fetching open issues for controls and for security plan {plan_id}...")
955
+ # Fetch open issues from API
956
+ control_issues = cls._fetch_open_issues_from_api(plan_id, is_component, logger)
957
+
958
+ # Cache the results if caching is enabled
959
+ if not cls._is_cache_disabled():
960
+ cls._cache_data(plan_id, control_issues)
961
+
962
+ return control_issues
963
+
964
+ @classmethod
965
+ def _check_cache(cls, plan_id: int, logger) -> Optional[Dict[int, List[OpenIssueDict]]]:
966
+ """
967
+ Check cache for open issues data
968
+
969
+ :param int plan_id: The ID of the parent
970
+ :param logger: Logger instance
971
+ :return: Cached data if available and valid, None otherwise
972
+ :rtype: Optional[Dict[int, List[OpenIssueDict]]]
973
+ """
974
+ if cls._is_cache_disabled():
975
+ return None
976
+
977
+ cached_data = cls._get_from_cache(plan_id)
978
+ if cached_data is not None:
979
+ logger.info(f"Using cached open issues data for security plan {plan_id}")
980
+ return cached_data
981
+
982
+ @classmethod
983
+ def _fetch_open_issues_from_api(cls, plan_id: int, is_component: bool, logger) -> Dict[int, List[OpenIssueDict]]:
984
+ """
985
+ Fetch open issues from API with pagination
938
986
 
987
+ :param int plan_id: The ID of the parent
988
+ :param bool is_component: Whether parent is a component
989
+ :param logger: Logger instance
990
+ :return: Dictionary of control IDs to open issues
991
+ :rtype: Dict[int, List[OpenIssueDict]]
992
+ """
993
+ start_time = time.time()
939
994
  module_str = "component" if is_component else "security plan"
940
- logger.info(
941
- f"Fetching open issues for controls and for {module_str} {plan_id}...",
942
- )
943
- supports_multiple_controls: bool = cls.is_multiple_controls_supported()
995
+ logger.info(f"Fetching open issues for controls and for {module_str} {plan_id}...")
944
996
 
945
- # Optimize field selection - only get what we need
946
- if supports_multiple_controls:
947
- fields = "id, otherIdentifier, integrationFindingId, controlImplementations { id }"
948
- else:
949
- fields = "id, controlId, otherIdentifier, integrationFindingId"
997
+ control_issues: Dict[int, List[OpenIssueDict]] = defaultdict(list)
998
+
999
+ try:
1000
+ total_fetched = cls._paginate_and_process_issues(plan_id, is_component, control_issues, logger)
1001
+ cls._log_completion(plan_id, total_fetched, len(control_issues), start_time, logger)
1002
+ except Exception as e:
1003
+ logger.error(f"Error fetching open issues for security plan {plan_id}: {e}")
1004
+ return defaultdict(list)
1005
+
1006
+ return control_issues
950
1007
 
1008
+ @classmethod
1009
+ def _paginate_and_process_issues(
1010
+ cls,
1011
+ plan_id: int,
1012
+ is_component: bool,
1013
+ control_issues: Dict[int, List[OpenIssueDict]],
1014
+ logger,
1015
+ ) -> int:
1016
+ """
1017
+ Paginate through API results and process issues
1018
+
1019
+ :param int plan_id: The ID of the parent
1020
+ :param bool is_component: Whether parent is a component
1021
+ :param Dict[int, List[OpenIssueDict]] control_issues: Dictionary to populate with results
1022
+ :param logger: Logger instance
1023
+ :return: Total number of items fetched
1024
+ :rtype: int
1025
+ """
1026
+ take = 50
1027
+ skip = 0
951
1028
  total_fetched = 0
1029
+ supports_multiple_controls = cls.is_multiple_controls_supported()
1030
+ fields = cls._get_query_fields(supports_multiple_controls)
952
1031
 
953
- try:
954
- while True:
955
- query = f"""
956
- query GetOpenIssuesByPlanOrComponent {{
957
- {cls.get_module_string()}(
958
- skip: {skip},
959
- take: {take},
960
- where: {{
961
- {"componentId" if is_component else "securityPlanId"}: {{eq: {plan_id}}},
962
- status: {{eq: "Open"}}
963
- }}
964
- ) {{
965
- items {{ {fields} }}
966
- pageInfo {{ hasNextPage }}
967
- totalCount
968
- }}
1032
+ while True:
1033
+ query = cls._build_query(plan_id, is_component, skip, take, fields)
1034
+ response = cls._get_api_handler().graph(query)
1035
+
1036
+ items = response.get(cls.get_module_string(), {}).get("items", [])
1037
+ total_count = response.get(cls.get_module_string(), {}).get("totalCount", 0)
1038
+
1039
+ cls._log_progress(skip, take, len(items), total_count, logger)
1040
+ cls._process_issue_items(items, supports_multiple_controls, control_issues)
1041
+
1042
+ total_fetched += len(items)
1043
+
1044
+ if not response.get(cls.get_module_string(), {}).get("pageInfo", {}).get("hasNextPage", False):
1045
+ break
1046
+
1047
+ skip += take
1048
+
1049
+ return total_fetched
1050
+
1051
+ @classmethod
1052
+ def _get_query_fields(cls, supports_multiple_controls: bool) -> str:
1053
+ """
1054
+ Get GraphQL query fields based on control support
1055
+
1056
+ :param bool supports_multiple_controls: Whether multiple controls are supported
1057
+ :return: GraphQL field selection string
1058
+ :rtype: str
1059
+ """
1060
+ if supports_multiple_controls:
1061
+ return "id, otherIdentifier, integrationFindingId, controlImplementations { id }"
1062
+ return "id, controlId, otherIdentifier, integrationFindingId"
1063
+
1064
+ @classmethod
1065
+ def _build_query(cls, plan_id: int, is_component: bool, skip: int, take: int, fields: str) -> str:
1066
+ """
1067
+ Build GraphQL query for fetching open issues
1068
+
1069
+ :param int plan_id: The ID of the parent
1070
+ :param bool is_component: Whether parent is a component
1071
+ :param int skip: Number of items to skip
1072
+ :param int take: Number of items to take
1073
+ :param str fields: GraphQL fields to select
1074
+ :return: GraphQL query string
1075
+ :rtype: str
1076
+ """
1077
+ parent_field = "componentId" if is_component else "securityPlanId"
1078
+ return f"""
1079
+ query GetOpenIssuesByPlanOrComponent {{
1080
+ {cls.get_module_string()}(
1081
+ skip: {skip},
1082
+ take: {take},
1083
+ where: {{
1084
+ {parent_field}: {{eq: {plan_id}}},
1085
+ status: {{eq: "Open"}}
969
1086
  }}
970
- """
1087
+ ) {{
1088
+ items {{ {fields} }}
1089
+ pageInfo {{ hasNextPage }}
1090
+ totalCount
1091
+ }}
1092
+ }}
1093
+ """
971
1094
 
972
- response = cls._get_api_handler().graph(query)
973
- items = response.get(cls.get_module_string(), {}).get("items", [])
974
- total_count = response.get(cls.get_module_string(), {}).get("totalCount", 0)
1095
+ @classmethod
1096
+ def _log_progress(cls, skip: int, take: int, items_count: int, total_count: int, logger) -> None:
1097
+ """
1098
+ Log progress for large datasets
975
1099
 
976
- # Log progress for large datasets
977
- if total_count > 1000:
978
- logger.info(
979
- f"Processing batch {skip // take + 1} - fetched {len(items)} items ({skip + len(items)}/{total_count})"
980
- )
1100
+ :param int skip: Number of items skipped
1101
+ :param int take: Batch size
1102
+ :param int items_count: Number of items in current batch
1103
+ :param int total_count: Total count of items
1104
+ :param logger: Logger instance
1105
+ :rtype: None
1106
+ """
1107
+ if total_count > 1000:
1108
+ logger.info(
1109
+ f"Processing batch {skip // take + 1} - fetched {items_count} items ({skip + items_count}/{total_count})"
1110
+ )
981
1111
 
982
- for item in items:
983
- issue_dict = OpenIssueDict(
984
- id=item["id"],
985
- otherIdentifier=item.get("otherIdentifier", ""),
986
- integrationFindingId=item.get("integrationFindingId", ""),
987
- )
1112
+ @classmethod
1113
+ def _process_issue_items(
1114
+ cls,
1115
+ items: List[Dict[str, Any]],
1116
+ supports_multiple_controls: bool,
1117
+ control_issues: Dict[int, List[OpenIssueDict]],
1118
+ ) -> None:
1119
+ """
1120
+ Process issue items and populate control_issues dictionary
988
1121
 
989
- if supports_multiple_controls and item.get("controlImplementations"):
990
- for control in item.get("controlImplementations", []):
991
- control_issues[control["id"]].append(issue_dict)
992
- elif item.get("controlId"):
993
- control_issues[item["controlId"]].append(issue_dict)
1122
+ :param List[Dict[str, Any]] items: List of issue items from API
1123
+ :param bool supports_multiple_controls: Whether multiple controls are supported
1124
+ :param Dict[int, List[OpenIssueDict]] control_issues: Dictionary to populate
1125
+ :rtype: None
1126
+ """
1127
+ for item in items:
1128
+ issue_dict = OpenIssueDict(
1129
+ id=item["id"],
1130
+ otherIdentifier=item.get("otherIdentifier", ""),
1131
+ integrationFindingId=item.get("integrationFindingId", ""),
1132
+ )
994
1133
 
995
- total_fetched += len(items)
996
- if not response.get(cls.get_module_string(), {}).get("pageInfo", {}).get("hasNextPage", False):
997
- break
998
- skip += take
1134
+ if supports_multiple_controls:
1135
+ cls._add_issue_to_multiple_controls(item, issue_dict, control_issues)
1136
+ else:
1137
+ cls._add_issue_to_single_control(item, issue_dict, control_issues)
999
1138
 
1000
- except Exception as e:
1001
- logger.error(f"Error fetching open issues for security plan {plan_id}: {e}")
1002
- # Return empty dict on error to prevent breaking the calling code
1003
- return defaultdict(list)
1139
+ @classmethod
1140
+ def _add_issue_to_multiple_controls(
1141
+ cls,
1142
+ item: Dict[str, Any],
1143
+ issue_dict: OpenIssueDict,
1144
+ control_issues: Dict[int, List[OpenIssueDict]],
1145
+ ) -> None:
1146
+ """
1147
+ Add issue to multiple control implementations
1148
+
1149
+ :param Dict[str, Any] item: Issue item from API
1150
+ :param OpenIssueDict issue_dict: Issue dictionary
1151
+ :param Dict[int, List[OpenIssueDict]] control_issues: Dictionary to populate
1152
+ :rtype: None
1153
+ """
1154
+ if item.get("controlImplementations"):
1155
+ for control in item.get("controlImplementations", []):
1156
+ control_issues[control["id"]].append(issue_dict)
1004
1157
 
1158
+ @classmethod
1159
+ def _add_issue_to_single_control(
1160
+ cls,
1161
+ item: Dict[str, Any],
1162
+ issue_dict: OpenIssueDict,
1163
+ control_issues: Dict[int, List[OpenIssueDict]],
1164
+ ) -> None:
1165
+ """
1166
+ Add issue to single control
1167
+
1168
+ :param Dict[str, Any] item: Issue item from API
1169
+ :param OpenIssueDict issue_dict: Issue dictionary
1170
+ :param Dict[int, List[OpenIssueDict]] control_issues: Dictionary to populate
1171
+ :rtype: None
1172
+ """
1173
+ if item.get("controlId"):
1174
+ control_issues[item["controlId"]].append(issue_dict)
1175
+
1176
+ @classmethod
1177
+ def _log_completion(cls, plan_id: int, total_fetched: int, control_count: int, start_time: float, logger) -> None:
1178
+ """
1179
+ Log completion statistics
1180
+
1181
+ :param int plan_id: The ID of the parent
1182
+ :param int total_fetched: Total number of items fetched
1183
+ :param int control_count: Number of controls with issues
1184
+ :param float start_time: Start time of the operation
1185
+ :param logger: Logger instance
1186
+ :rtype: None
1187
+ """
1005
1188
  elapsed_time = time.time() - start_time
1006
1189
  logger.info(
1007
- f"Finished fetching {total_fetched} open issue(s) for {len(control_issues)} control(s) "
1190
+ f"Finished fetching {total_fetched} open issue(s) for {control_count} control(s) "
1008
1191
  f"in security plan {plan_id} - took {elapsed_time:.2f} seconds"
1009
1192
  )
1010
1193
 
1011
- # Cache the results
1012
- if use_cache:
1013
- cls._cache_data(plan_id, control_issues)
1014
-
1015
- return control_issues
1016
-
1017
1194
  @classmethod
1018
1195
  def get_sort_position_dict(cls) -> Dict[str, int]:
1019
1196
  """
@@ -1103,36 +1280,20 @@ class Issue(RegScaleModel):
1103
1280
  }
1104
1281
 
1105
1282
  @classmethod
1106
- def get_enum_values(cls, field_name: str) -> List[Union[IssueSeverity, IssueStatus, str]]:
1283
+ def get_enum_values(cls, field_name: str) -> List[Union[IssueSeverity, IssueStatus, IssueIdentification, str]]:
1107
1284
  """
1108
1285
  Overrides the base method.
1109
1286
 
1110
1287
  :param str field_name: The property name to provide enum values for
1111
1288
  :return: List of enum values or strings
1112
- :rtype: List[Union[IssueSeverity, IssueStatus, str]]
1289
+ :rtype: List[Union[IssueSeverity, IssueStatus, IssueIdentification, str]]
1113
1290
  """
1114
1291
  if field_name == "severityLevel":
1115
1292
  return [severity.__str__() for severity in IssueSeverity]
1116
1293
  if field_name == "status":
1117
1294
  return [status.__str__() for status in IssueStatus]
1118
1295
  if field_name == "identification":
1119
- return [
1120
- "A-123 Review",
1121
- "Assessment/Audit (External)",
1122
- "Assessment/Audit (Internal)",
1123
- "Critical Control Review",
1124
- "FDCC/USGCB",
1125
- "GAO Audit",
1126
- "IG Audit",
1127
- "Incidnet Response Lessons Learned",
1128
- "ITAR",
1129
- "Other",
1130
- "Penetration Test",
1131
- "Risk Assessment",
1132
- "Security Authorization",
1133
- "Security Control Assessment",
1134
- "Vulnerability Assessment",
1135
- ]
1296
+ return [identification.__str__() for identification in IssueIdentification]
1136
1297
  return cls.get_bool_enums(field_name)
1137
1298
 
1138
1299
  @classmethod
@@ -15,7 +15,7 @@ class Milestone(RegScaleModel):
15
15
 
16
16
  _module_slug = "milestones"
17
17
  _module_string = "milestones"
18
- _unique_fields = ["title", "parentModule"]
18
+ _unique_fields = ["title", "parentModule", "parentID"]
19
19
  _parent_id_field = "parentID"
20
20
 
21
21
  title: str
@@ -60,6 +60,7 @@ class RegScaleModel(BaseModel, ABC):
60
60
 
61
61
  _pending_updates: ClassVar[Dict[str, Set[int]]] = {}
62
62
  _pending_creations: ClassVar[Dict[str, Set[str]]] = {}
63
+ _ignore_has_changed: bool = False
63
64
 
64
65
  id: int = 0
65
66
  extra_data: Dict[str, Any] = Field(default={}, exclude=True)
@@ -1309,7 +1310,11 @@ class RegScaleModel(BaseModel, ABC):
1309
1310
  """
1310
1311
  # Check if the model has change tracking and if there are changes
1311
1312
  has_change_tracking = hasattr(self, "has_changed") and callable(getattr(self, "has_changed", None))
1312
- should_save = not has_change_tracking or self.has_changed()
1313
+
1314
+ if hasattr(self, "_ignore_has_changed") and self._ignore_has_changed:
1315
+ should_save = True
1316
+ else:
1317
+ should_save = not has_change_tracking or self.has_changed()
1313
1318
 
1314
1319
  if should_save:
1315
1320
  if bulk:
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: regscale-cli
3
- Version: 6.25.1.0
3
+ Version: 6.27.0.0
4
4
  Summary: Command Line Interface (CLI) for bulk processing/loading data into RegScale
5
5
  Home-page: https://github.com/RegScale/regscale-cli
6
6
  Author: Travis Howerton
@@ -30,7 +30,6 @@ Requires-Dist: distro ==1.8.0
30
30
  Requires-Dist: docx ==0.2.4
31
31
  Requires-Dist: filelock ~=3.13.1
32
32
  Requires-Dist: frontend
33
- Requires-Dist: future ~=0.18.3
34
33
  Requires-Dist: google-api-python-client
35
34
  Requires-Dist: google-cloud-asset ~=3.22
36
35
  Requires-Dist: google-cloud-securitycenter ~=1.25
@@ -59,7 +58,6 @@ Requires-Dist: pytest
59
58
  Requires-Dist: python-dateutil ~=2.9.0
60
59
  Requires-Dist: python-docx
61
60
  Requires-Dist: python-jwt ==4.1.0
62
- Requires-Dist: pyxnat ==1.5.*
63
61
  Requires-Dist: rapidfuzz ~=3.7
64
62
  Requires-Dist: regscale-python-ssp
65
63
  Requires-Dist: requests >=2.32.0
@@ -96,7 +94,6 @@ Requires-Dist: filelock ~=3.13.1 ; extra == 'airflow'
96
94
  Requires-Dist: flask-appbuilder >=4.0.0 ; extra == 'airflow'
97
95
  Requires-Dist: flask >=2.3.2 ; extra == 'airflow'
98
96
  Requires-Dist: frontend ; extra == 'airflow'
99
- Requires-Dist: future ~=0.18.3 ; extra == 'airflow'
100
97
  Requires-Dist: google-api-python-client ; extra == 'airflow'
101
98
  Requires-Dist: google-cloud-asset ~=3.22 ; extra == 'airflow'
102
99
  Requires-Dist: google-cloud-securitycenter ~=1.25 ; extra == 'airflow'
@@ -140,7 +137,6 @@ Requires-Dist: pytest ; extra == 'airflow'
140
137
  Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'airflow'
141
138
  Requires-Dist: python-docx ; extra == 'airflow'
142
139
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'airflow'
143
- Requires-Dist: pyxnat ==1.5.* ; extra == 'airflow'
144
140
  Requires-Dist: rapidfuzz ~=3.7 ; extra == 'airflow'
145
141
  Requires-Dist: regscale-python-ssp ; extra == 'airflow'
146
142
  Requires-Dist: requests >=2.32.0 ; extra == 'airflow'
@@ -180,7 +176,6 @@ Requires-Dist: filelock ~=3.13.1 ; extra == 'airflow-azure'
180
176
  Requires-Dist: flask-appbuilder >=4.0.0 ; extra == 'airflow-azure'
181
177
  Requires-Dist: flask >=2.3.2 ; extra == 'airflow-azure'
182
178
  Requires-Dist: frontend ; extra == 'airflow-azure'
183
- Requires-Dist: future ~=0.18.3 ; extra == 'airflow-azure'
184
179
  Requires-Dist: google-api-python-client ; extra == 'airflow-azure'
185
180
  Requires-Dist: google-cloud-asset ~=3.22 ; extra == 'airflow-azure'
186
181
  Requires-Dist: google-cloud-securitycenter ~=1.25 ; extra == 'airflow-azure'
@@ -225,7 +220,6 @@ Requires-Dist: pytest ; extra == 'airflow-azure'
225
220
  Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'airflow-azure'
226
221
  Requires-Dist: python-docx ; extra == 'airflow-azure'
227
222
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'airflow-azure'
228
- Requires-Dist: pyxnat ==1.5.* ; extra == 'airflow-azure'
229
223
  Requires-Dist: rapidfuzz ~=3.7 ; extra == 'airflow-azure'
230
224
  Requires-Dist: regscale-python-ssp ; extra == 'airflow-azure'
231
225
  Requires-Dist: requests >=2.32.0 ; extra == 'airflow-azure'
@@ -264,7 +258,6 @@ Requires-Dist: filelock ~=3.13.1 ; extra == 'airflow-sqlserver'
264
258
  Requires-Dist: flask-appbuilder >=4.0.0 ; extra == 'airflow-sqlserver'
265
259
  Requires-Dist: flask >=2.3.2 ; extra == 'airflow-sqlserver'
266
260
  Requires-Dist: frontend ; extra == 'airflow-sqlserver'
267
- Requires-Dist: future ~=0.18.3 ; extra == 'airflow-sqlserver'
268
261
  Requires-Dist: google-api-python-client ; extra == 'airflow-sqlserver'
269
262
  Requires-Dist: google-cloud-asset ~=3.22 ; extra == 'airflow-sqlserver'
270
263
  Requires-Dist: google-cloud-securitycenter ~=1.25 ; extra == 'airflow-sqlserver'
@@ -310,7 +303,6 @@ Requires-Dist: pytest ; extra == 'airflow-sqlserver'
310
303
  Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'airflow-sqlserver'
311
304
  Requires-Dist: python-docx ; extra == 'airflow-sqlserver'
312
305
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'airflow-sqlserver'
313
- Requires-Dist: pyxnat ==1.5.* ; extra == 'airflow-sqlserver'
314
306
  Requires-Dist: rapidfuzz ~=3.7 ; extra == 'airflow-sqlserver'
315
307
  Requires-Dist: regscale-python-ssp ; extra == 'airflow-sqlserver'
316
308
  Requires-Dist: requests >=2.32.0 ; extra == 'airflow-sqlserver'
@@ -354,7 +346,6 @@ Requires-Dist: flask-rebar ; extra == 'all'
354
346
  Requires-Dist: flask-restx ; extra == 'all'
355
347
  Requires-Dist: flask >=2.3.2 ; extra == 'all'
356
348
  Requires-Dist: frontend ; extra == 'all'
357
- Requires-Dist: future ~=0.18.3 ; extra == 'all'
358
349
  Requires-Dist: google-api-python-client ; extra == 'all'
359
350
  Requires-Dist: google-cloud-asset ~=3.22 ; extra == 'all'
360
351
  Requires-Dist: google-cloud-securitycenter ~=1.25 ; extra == 'all'
@@ -400,7 +391,6 @@ Requires-Dist: pytest ; extra == 'all'
400
391
  Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'all'
401
392
  Requires-Dist: python-docx ; extra == 'all'
402
393
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'all'
403
- Requires-Dist: pyxnat ==1.5.* ; extra == 'all'
404
394
  Requires-Dist: rapidfuzz ~=3.7 ; extra == 'all'
405
395
  Requires-Dist: regscale-python-ssp ; extra == 'all'
406
396
  Requires-Dist: requests >=2.32.0 ; extra == 'all'
@@ -433,7 +423,6 @@ Requires-Dist: distro ==1.8.0 ; extra == 'ansible'
433
423
  Requires-Dist: docx ==0.2.4 ; extra == 'ansible'
434
424
  Requires-Dist: filelock ~=3.13.1 ; extra == 'ansible'
435
425
  Requires-Dist: frontend ; extra == 'ansible'
436
- Requires-Dist: future ~=0.18.3 ; extra == 'ansible'
437
426
  Requires-Dist: google-api-python-client ; extra == 'ansible'
438
427
  Requires-Dist: google-cloud-asset ~=3.22 ; extra == 'ansible'
439
428
  Requires-Dist: google-cloud-securitycenter ~=1.25 ; extra == 'ansible'
@@ -463,7 +452,6 @@ Requires-Dist: pytest ; extra == 'ansible'
463
452
  Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'ansible'
464
453
  Requires-Dist: python-docx ; extra == 'ansible'
465
454
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'ansible'
466
- Requires-Dist: pyxnat ==1.5.* ; extra == 'ansible'
467
455
  Requires-Dist: rapidfuzz ~=3.7 ; extra == 'ansible'
468
456
  Requires-Dist: regscale-python-ssp ; extra == 'ansible'
469
457
  Requires-Dist: requests >=2.32.0 ; extra == 'ansible'
@@ -499,7 +487,6 @@ Requires-Dist: filelock ~=3.13.1 ; extra == 'dev'
499
487
  Requires-Dist: flake8 ; extra == 'dev'
500
488
  Requires-Dist: freezegun ; extra == 'dev'
501
489
  Requires-Dist: frontend ; extra == 'dev'
502
- Requires-Dist: future ~=0.18.3 ; extra == 'dev'
503
490
  Requires-Dist: google-api-python-client ; extra == 'dev'
504
491
  Requires-Dist: google-cloud-asset ~=3.22 ; extra == 'dev'
505
492
  Requires-Dist: google-cloud-securitycenter ~=1.25 ; extra == 'dev'
@@ -542,7 +529,6 @@ Requires-Dist: pytest >=5 ; extra == 'dev'
542
529
  Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'dev'
543
530
  Requires-Dist: python-docx ; extra == 'dev'
544
531
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'dev'
545
- Requires-Dist: pyxnat ==1.5.* ; extra == 'dev'
546
532
  Requires-Dist: radon ; extra == 'dev'
547
533
  Requires-Dist: rapidfuzz ~=3.7 ; extra == 'dev'
548
534
  Requires-Dist: regscale-python-ssp ; extra == 'dev'
@@ -585,7 +571,6 @@ Requires-Dist: flask-appbuilder ; extra == 'server'
585
571
  Requires-Dist: flask-rebar ; extra == 'server'
586
572
  Requires-Dist: flask-restx ; extra == 'server'
587
573
  Requires-Dist: frontend ; extra == 'server'
588
- Requires-Dist: future ~=0.18.3 ; extra == 'server'
589
574
  Requires-Dist: google-api-python-client ; extra == 'server'
590
575
  Requires-Dist: google-cloud-asset ~=3.22 ; extra == 'server'
591
576
  Requires-Dist: google-cloud-securitycenter ~=1.25 ; extra == 'server'
@@ -615,7 +600,6 @@ Requires-Dist: pytest ; extra == 'server'
615
600
  Requires-Dist: python-dateutil ~=2.9.0 ; extra == 'server'
616
601
  Requires-Dist: python-docx ; extra == 'server'
617
602
  Requires-Dist: python-jwt ==4.1.0 ; extra == 'server'
618
- Requires-Dist: pyxnat ==1.5.* ; extra == 'server'
619
603
  Requires-Dist: rapidfuzz ~=3.7 ; extra == 'server'
620
604
  Requires-Dist: regscale-python-ssp ; extra == 'server'
621
605
  Requires-Dist: requests >=2.32.0 ; extra == 'server'