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
@@ -13,6 +13,7 @@ from collections import defaultdict
13
13
  from typing import Dict, List, Optional, Any, Iterator
14
14
 
15
15
  from regscale.core.app.utils.app_utils import get_current_datetime, regscale_string_to_datetime
16
+ from regscale.integrations.control_matcher import ControlMatcher
16
17
  from regscale.integrations.scanner_integration import (
17
18
  ScannerIntegration,
18
19
  IntegrationAsset,
@@ -166,6 +167,9 @@ class ComplianceIntegration(ScannerIntegration, ABC):
166
167
  self._compliance_settings = None
167
168
  self._security_plan = None
168
169
 
170
+ # Initialize control matcher for robust control ID matching
171
+ self._control_matcher = ControlMatcher()
172
+
169
173
  def is_poam(self, finding: IntegrationFinding) -> bool: # type: ignore[override]
170
174
  """
171
175
  Determines if an issue should be considered a POAM for compliance integrations.
@@ -1230,7 +1234,6 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1230
1234
  processed_impl_today.add(impl.id)
1231
1235
 
1232
1236
  self._record_control_mapping(control_id, impl.id)
1233
- self._map_assets_to_control_component(sec_control, items)
1234
1237
  return 1
1235
1238
  except Exception as e: # noqa: BLE001
1236
1239
  logger.error(f"Error processing control assessment for '{control_id}': {e}")
@@ -1245,13 +1248,22 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1245
1248
  """
1246
1249
  Find matching implementation and security control for a control ID.
1247
1250
 
1251
+ Uses ControlMatcher for robust control ID matching with leading zero normalization.
1252
+
1248
1253
  :param str control_id: Control identifier to match
1249
1254
  :param List[ControlImplementation] implementations: Available implementations
1250
1255
  :return: Tuple of matching implementation and security control, or (None, None)
1251
1256
  :rtype: tuple[Optional[ControlImplementation], Optional[SecurityControl]]
1252
1257
  """
1258
+ # Generate all variations of the search control ID for matching
1259
+ search_variations = self._control_matcher._get_control_id_variations(control_id)
1260
+ if not search_variations:
1261
+ logger.debug(f"Could not generate control ID variations for: {control_id}")
1262
+ return None, None
1263
+
1253
1264
  matching_implementation = None
1254
1265
  matching_security_control = None
1266
+
1255
1267
  for implementation in implementations:
1256
1268
  try:
1257
1269
  security_control = SecurityControl.get_object(object_id=implementation.controlID)
@@ -1264,10 +1276,16 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1264
1276
  if not security_control_id:
1265
1277
  logger.debug(f"Security control {security_control.id} has no controlId")
1266
1278
  continue
1279
+
1280
+ # Get variations of the security control ID
1281
+ control_variations = self._control_matcher._get_control_id_variations(security_control_id)
1282
+
1267
1283
  logger.debug(
1268
- f"Comparing extracted '{control_id}' with RegScale control '{security_control_id}' (impl: {implementation.id})"
1284
+ f"Comparing control '{control_id}' variations {list(search_variations)[:3]} with RegScale control '{security_control_id}' variations {list(control_variations)[:3]} (impl: {implementation.id})"
1269
1285
  )
1270
- if self._control_ids_match(control_id, security_control_id):
1286
+
1287
+ # Check if any variation matches (set intersection)
1288
+ if search_variations & control_variations:
1271
1289
  matching_implementation = implementation
1272
1290
  matching_security_control = security_control
1273
1291
  logger.info(
@@ -1351,44 +1369,6 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1351
1369
  except Exception:
1352
1370
  pass
1353
1371
 
1354
- def _map_assets_to_control_component(self, sec_control: SecurityControl, items: List[ComplianceItem]) -> None:
1355
- """
1356
- Map assets to control-specific components for organization.
1357
-
1358
- :param SecurityControl sec_control: Security control to create component for
1359
- :param List[ComplianceItem] items: Compliance items with asset references
1360
- :return: None
1361
- :rtype: None
1362
- """
1363
- try:
1364
- component_title = f"Control {sec_control.controlId}"
1365
- component = self.components_by_title.get(component_title) if hasattr(self, "components_by_title") else None
1366
- if not component:
1367
- component = regscale_models.Component(
1368
- title=component_title,
1369
- componentType=regscale_models.ComponentType.Hardware,
1370
- securityPlansId=self.plan_id,
1371
- description=component_title,
1372
- componentOwnerId=self.get_assessor_id(),
1373
- ).get_or_create()
1374
- regscale_models.ComponentMapping(
1375
- componentId=component.id,
1376
- securityPlanId=self.plan_id,
1377
- ).get_or_create()
1378
- if hasattr(self, "components_by_title"):
1379
- self.components_by_title[component_title] = component
1380
-
1381
- for item in items:
1382
- asset = self.get_asset_by_identifier(getattr(item, "resource_id", ""))
1383
- if not asset:
1384
- continue
1385
- regscale_models.AssetMapping(
1386
- assetId=asset.id,
1387
- componentId=component.id,
1388
- ).get_or_create_with_status()
1389
- except Exception as map_exc: # noqa: BLE001
1390
- logger.debug(f"Control-to-asset mapping skipped due to: {map_exc}")
1391
-
1392
1372
  @staticmethod
1393
1373
  def _parse_control_id(control_id: str) -> tuple[str, Optional[str]]:
1394
1374
  """
@@ -2050,6 +2030,8 @@ class ComplianceIntegration(ScannerIntegration, ABC):
2050
2030
  """
2051
2031
  Create or update an issue from a finding, using cache to prevent duplicates.
2052
2032
 
2033
+ Properly handles milestone creation for compliance integrations.
2034
+
2053
2035
  :param str title: Issue title
2054
2036
  :param IntegrationFinding finding: The finding to create issue from
2055
2037
  :return: Created or updated issue
@@ -2068,6 +2050,9 @@ class ComplianceIntegration(ScannerIntegration, ABC):
2068
2050
  f"Found existing issue {existing_issue.id} (other_identifier: '{existing_issue.otherIdentifier}') for lookup external_id '{external_id}', updating instead of creating"
2069
2051
  )
2070
2052
 
2053
+ # Store original status for milestone comparison
2054
+ original_status = existing_issue.status
2055
+
2071
2056
  # Update existing issue with new finding data
2072
2057
  existing_issue.title = title
2073
2058
  existing_issue.description = finding.description
@@ -2095,8 +2080,42 @@ class ComplianceIntegration(ScannerIntegration, ABC):
2095
2080
  existing_issue.orgId = self.determine_issue_organization_id(existing_issue.issueOwnerId)
2096
2081
  existing_issue.save()
2097
2082
 
2083
+ # Create milestone if status changed
2084
+ # Reconstruct original issue state for comparison
2085
+ original_issue = regscale_models.Issue()
2086
+ original_issue.status = original_status
2087
+ self._create_milestones_for_updated_issue(existing_issue, finding, original_issue)
2088
+
2098
2089
  return existing_issue
2099
2090
  else:
2100
2091
  # No existing issue found, create new one using parent method
2101
2092
  logger.debug(f"No existing issue found for external_id {external_id}, creating new issue")
2102
2093
  return super().create_or_update_issue_from_finding(title, finding)
2094
+
2095
+ def _create_milestones_for_updated_issue(
2096
+ self,
2097
+ issue: regscale_models.Issue,
2098
+ finding: IntegrationFinding,
2099
+ original_issue: regscale_models.Issue,
2100
+ ) -> None:
2101
+ """
2102
+ Create milestones for an updated issue in compliance integration.
2103
+
2104
+ This method handles both status transition milestones and backfilling of missing
2105
+ creation milestones for existing issues.
2106
+
2107
+ :param regscale_models.Issue issue: The updated issue
2108
+ :param IntegrationFinding finding: The finding data
2109
+ :param regscale_models.Issue original_issue: Original state for comparison
2110
+ """
2111
+ milestone_manager = self.get_milestone_manager()
2112
+
2113
+ # First, ensure the issue has a creation milestone (backfill if missing)
2114
+ milestone_manager.ensure_creation_milestone_exists(issue=issue, finding=finding)
2115
+
2116
+ # Then, handle status transition milestones
2117
+ milestone_manager.create_milestones_for_issue(
2118
+ issue=issue,
2119
+ finding=finding,
2120
+ existing_issue=original_issue,
2121
+ )
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Control Matcher - A utility class for identifying and matching control implementations
5
+ across different RegScale entities based on control ID strings.
6
+ """
7
+
8
+ import logging
9
+ import re
10
+ from typing import Dict, List, Optional, Tuple, Union
11
+
12
+ from regscale.core.app.api import Api
13
+ from regscale.core.app.application import Application
14
+ from regscale.models.regscale_models.control_implementation import ControlImplementation
15
+ from regscale.models.regscale_models.security_control import SecurityControl
16
+
17
+ logger = logging.getLogger("regscale")
18
+
19
+
20
+ class ControlMatcher:
21
+ """
22
+ A class to identify control IDs and match them across different RegScale entities.
23
+
24
+ This class provides control matching capabilities:
25
+ - Parse control ID strings to extract NIST control identifiers
26
+ - Match controls from catalogs to security plans
27
+ - Find control implementations based on control IDs
28
+ - Support multiple control ID formats (e.g., AC-1, AC-1(1), AC-1.1)
29
+
30
+ Note: This class is focused on finding and matching existing controls only.
31
+ Control creation/modification should be handled by calling code.
32
+ """
33
+
34
+ def __init__(self, app: Optional[Application] = None):
35
+ """
36
+ Initialize the ControlMatcher.
37
+
38
+ :param Optional[Application] app: RegScale Application instance
39
+ """
40
+ self.app = app or Application()
41
+ self.api = Api()
42
+ self._catalog_cache: Dict[int, List[SecurityControl]] = {}
43
+ self._control_impl_cache: Dict[Tuple[int, str], Dict[str, ControlImplementation]] = {}
44
+
45
+ def parse_control_id(self, control_string: str) -> Optional[str]:
46
+ """
47
+ Parse a control ID string and extract the standardized control identifier.
48
+
49
+ Handles various formats:
50
+ - NIST format: AC-1, AC-1(1), AC-1.1
51
+ - With leading zeros: AC-01, AC-17(02)
52
+ - With spaces: AC-1 (1), AC-02 (04)
53
+ - With text: "Access Control AC-1"
54
+ - Multiple controls: "AC-1, AC-2"
55
+
56
+ :param str control_string: Raw control ID string
57
+ :return: Standardized control ID or None if not found
58
+ :rtype: Optional[str]
59
+ """
60
+ if not control_string:
61
+ return None
62
+
63
+ # Clean the string
64
+ control_string = control_string.strip().upper()
65
+
66
+ # Common NIST control ID pattern
67
+ # Matches: AC-1, AC-01, AC-1(1), AC-1(01), AC-1 (1), AC-1.1, AC-1.01, etc.
68
+ # Allows optional whitespace before and inside parentheses
69
+ pattern = r"([A-Z]{2,3}-\d+(?:\s*\(\s*\d+\s*\)|\.\d+)?)"
70
+
71
+ matches = re.findall(pattern, control_string)
72
+ if matches:
73
+ # Normalize parentheses to dots for consistency and remove spaces
74
+ control_id = matches[0]
75
+ control_id = control_id.replace(" ", "") # Remove all spaces
76
+ control_id = control_id.replace("(", ".").replace(")", "")
77
+
78
+ # Normalize leading zeros (e.g., AC-01.02 -> AC-1.2)
79
+ parts = control_id.split("-")
80
+ if len(parts) == 2:
81
+ family = parts[0]
82
+ number_part = parts[1]
83
+
84
+ if "." in number_part:
85
+ main_num, enhancement = number_part.split(".", 1)
86
+ main_num = str(int(main_num))
87
+ enhancement = str(int(enhancement))
88
+ control_id = f"{family}-{main_num}.{enhancement}"
89
+ else:
90
+ main_num = str(int(number_part))
91
+ control_id = f"{family}-{main_num}"
92
+
93
+ return control_id
94
+
95
+ return None
96
+
97
+ def find_control_in_catalog(self, control_id: str, catalog_id: int) -> Optional[SecurityControl]:
98
+ """
99
+ Find a security control in a specific catalog by control ID.
100
+
101
+ :param str control_id: The control ID to search for
102
+ :param int catalog_id: The catalog ID to search in
103
+ :return: SecurityControl object if found, None otherwise
104
+ :rtype: Optional[SecurityControl]
105
+ """
106
+ controls = self._get_catalog_controls(catalog_id)
107
+
108
+ # Generate all possible variations of the control ID
109
+ search_ids = self._get_control_id_variations(control_id)
110
+
111
+ # Try exact match with any variation
112
+ for control in controls:
113
+ if control.controlId in search_ids:
114
+ return control
115
+
116
+ # Try matching control variations against search variations
117
+ for control in controls:
118
+ control_variations = self._get_control_id_variations(control.controlId)
119
+ if control_variations & search_ids: # Set intersection
120
+ return control
121
+
122
+ return None
123
+
124
+ def find_control_implementation(
125
+ self, control_id: str, parent_id: int, parent_module: str = "securityplans", catalog_id: Optional[int] = None
126
+ ) -> Optional[ControlImplementation]:
127
+ """
128
+ Find a control implementation based on control ID and parent context.
129
+
130
+ :param str control_id: The control ID to match
131
+ :param int parent_id: Parent entity ID (e.g., security plan ID)
132
+ :param str parent_module: Parent module type (default: securityplans)
133
+ :param Optional[int] catalog_id: Optional catalog ID for better matching
134
+ :return: ControlImplementation if found, None otherwise
135
+ :rtype: Optional[ControlImplementation]
136
+ """
137
+ # Get control implementations for the parent
138
+ implementations = self._get_control_implementations(parent_id, parent_module)
139
+
140
+ # Get all variations of the control ID for matching
141
+ search_variations = self._get_control_id_variations(control_id)
142
+ if not search_variations:
143
+ logger.warning(f"Could not parse control ID: {control_id}")
144
+ return None
145
+
146
+ # Try to find matching implementation with variation matching
147
+ for impl_key, impl in implementations.items():
148
+ impl_variations = self._get_control_id_variations(impl_key)
149
+ if impl_variations & search_variations: # Set intersection
150
+ return impl
151
+
152
+ # If catalog ID provided, try to find via security control
153
+ if catalog_id:
154
+ control = self.find_control_in_catalog(control_id, catalog_id)
155
+ if control:
156
+ # Search implementations by control ID
157
+ for impl in implementations.values():
158
+ if impl.controlID == control.id:
159
+ return impl
160
+
161
+ return None
162
+
163
+ def match_controls_to_implementations(
164
+ self,
165
+ control_ids: List[str],
166
+ parent_id: int,
167
+ parent_module: str = "securityplans",
168
+ catalog_id: Optional[int] = None,
169
+ ) -> Dict[str, Optional[ControlImplementation]]:
170
+ """
171
+ Match multiple control IDs to their implementations.
172
+
173
+ :param List[str] control_ids: List of control ID strings
174
+ :param int parent_id: Parent entity ID
175
+ :param str parent_module: Parent module type
176
+ :param Optional[int] catalog_id: Optional catalog ID
177
+ :return: Dictionary mapping control IDs to implementations
178
+ :rtype: Dict[str, Optional[ControlImplementation]]
179
+ """
180
+ results = {}
181
+
182
+ for control_id in control_ids:
183
+ impl = self.find_control_implementation(control_id, parent_id, parent_module, catalog_id)
184
+ results[control_id] = impl
185
+
186
+ return results
187
+
188
+ def get_security_plan_controls(self, security_plan_id: int) -> Dict[str, ControlImplementation]:
189
+ """
190
+ Get all control implementations for a security plan.
191
+
192
+ :param int security_plan_id: The security plan ID
193
+ :return: Dictionary of control implementations keyed by control ID
194
+ :rtype: Dict[str, ControlImplementation]
195
+ """
196
+ return self._get_control_implementations(security_plan_id, "securityplans")
197
+
198
+ def find_controls_by_pattern(self, pattern: str, catalog_id: int) -> List[SecurityControl]:
199
+ """
200
+ Find all controls in a catalog matching a pattern.
201
+
202
+ :param str pattern: Regex pattern or substring to match
203
+ :param int catalog_id: Catalog ID to search in
204
+ :return: List of matching SecurityControl objects
205
+ :rtype: List[SecurityControl]
206
+ """
207
+ controls = self._get_catalog_controls(catalog_id)
208
+ matched = []
209
+
210
+ for control in controls:
211
+ if (re.search(pattern, control.controlId, re.IGNORECASE)) or (
212
+ control.title and re.search(pattern, control.title, re.IGNORECASE)
213
+ ):
214
+ matched.append(control)
215
+
216
+ return matched
217
+
218
+ def bulk_match_controls(
219
+ self,
220
+ control_mappings: Dict[str, str],
221
+ parent_id: int,
222
+ parent_module: str = "securityplans",
223
+ catalog_id: Optional[int] = None,
224
+ ) -> Dict[str, Optional[ControlImplementation]]:
225
+ """
226
+ Bulk match control IDs to their implementations.
227
+
228
+ :param Dict[str, str] control_mappings: Dict of {external_id: control_id}
229
+ :param int parent_id: Parent entity ID
230
+ :param str parent_module: Parent module type
231
+ :param Optional[int] catalog_id: Catalog ID for controls
232
+ :return: Dictionary mapping external IDs to ControlImplementations (None if not found)
233
+ :rtype: Dict[str, Optional[ControlImplementation]]
234
+ """
235
+ results = {}
236
+
237
+ for external_id, control_id in control_mappings.items():
238
+ impl = self.find_control_implementation(control_id, parent_id, parent_module, catalog_id)
239
+ results[external_id] = impl
240
+
241
+ return results
242
+
243
+ def _get_catalog_controls(self, catalog_id: int) -> List[SecurityControl]:
244
+ """
245
+ Get all controls for a catalog (with caching).
246
+
247
+ :param int catalog_id: Catalog ID
248
+ :return: List of SecurityControl objects
249
+ :rtype: List[SecurityControl]
250
+ """
251
+ if catalog_id not in self._catalog_cache:
252
+ try:
253
+ controls = SecurityControl.get_list_by_catalog(catalog_id)
254
+ self._catalog_cache[catalog_id] = controls
255
+ except Exception as e:
256
+ logger.error(f"Failed to get controls for catalog {catalog_id}: {e}")
257
+ return []
258
+
259
+ return self._catalog_cache.get(catalog_id, [])
260
+
261
+ def _normalize_control_id(self, control_id: str) -> Optional[str]:
262
+ """
263
+ Normalize a control ID by removing leading zeros from all numeric parts.
264
+
265
+ Examples:
266
+ - AC-01 -> AC-1
267
+ - AC-17(02) -> AC-17.2
268
+ - AC-1.01 -> AC-1.1
269
+
270
+ :param str control_id: The control ID to normalize
271
+ :return: Normalized control ID or None if invalid
272
+ :rtype: Optional[str]
273
+ """
274
+ parsed = self.parse_control_id(control_id)
275
+ if not parsed:
276
+ return None
277
+
278
+ # Split by '-' to get family and number parts
279
+ parts = parsed.split("-")
280
+ if len(parts) != 2:
281
+ return None
282
+
283
+ family = parts[0]
284
+ number_part = parts[1]
285
+
286
+ # Handle enhancement notation (both . and parentheses are normalized to .)
287
+ if "." in number_part:
288
+ main_num, enhancement = number_part.split(".", 1)
289
+ # Remove leading zeros from both parts
290
+ main_num = str(int(main_num))
291
+ enhancement = str(int(enhancement))
292
+ return f"{family}-{main_num}.{enhancement}"
293
+ else:
294
+ # Just main control number
295
+ main_num = str(int(number_part))
296
+ return f"{family}-{main_num}"
297
+
298
+ def _get_control_id_variations(self, control_id: str) -> set:
299
+ """
300
+ Generate all valid variations of a control ID (with and without leading zeros).
301
+
302
+ Examples:
303
+ - AC-1 -> {AC-1, AC-01}
304
+ - AC-17.2 -> {AC-17.2, AC-17.02, AC-17(2), AC-17(02)}
305
+
306
+ :param str control_id: The control ID to generate variations for
307
+ :return: Set of all valid variations
308
+ :rtype: set
309
+ """
310
+ parsed = self.parse_control_id(control_id)
311
+ if not parsed:
312
+ return set()
313
+
314
+ variations = set()
315
+
316
+ # Split by '-' to get family and number parts
317
+ parts = parsed.split("-")
318
+ if len(parts) != 2:
319
+ return set()
320
+
321
+ family = parts[0]
322
+ number_part = parts[1]
323
+
324
+ # Handle enhancement notation
325
+ if "." in number_part:
326
+ main_num, enhancement = number_part.split(".", 1)
327
+ main_int = int(main_num)
328
+ enh_int = int(enhancement)
329
+
330
+ # Generate all combinations: with/without leading zeros, dot/parenthesis notation
331
+ for main_fmt in [str(main_int), f"{main_int:02d}"]:
332
+ for enh_fmt in [str(enh_int), f"{enh_int:02d}"]:
333
+ variations.add(f"{family}-{main_fmt}.{enh_fmt}")
334
+ variations.add(f"{family}-{main_fmt}({enh_fmt})")
335
+ else:
336
+ # Just main control number
337
+ main_int = int(number_part)
338
+ variations.add(f"{family}-{main_int}")
339
+ variations.add(f"{family}-{main_int:02d}")
340
+
341
+ # Add uppercase versions to ensure consistency
342
+ return {v.upper() for v in variations}
343
+
344
+ def _get_control_implementations(self, parent_id: int, parent_module: str) -> Dict[str, ControlImplementation]:
345
+ """
346
+ Get control implementations for a parent (with caching).
347
+
348
+ :param int parent_id: Parent ID
349
+ :param str parent_module: Parent module
350
+ :return: Dict of implementations keyed by control ID
351
+ :rtype: Dict[str, ControlImplementation]
352
+ """
353
+ cache_key = (parent_id, parent_module)
354
+
355
+ if cache_key not in self._control_impl_cache:
356
+ try:
357
+ # Get the label map which maps control IDs to implementation IDs
358
+ label_map = ControlImplementation.get_control_label_map_by_parent(parent_id, parent_module)
359
+
360
+ implementations = {}
361
+ for control_label, impl_id in label_map.items():
362
+ impl = ControlImplementation.get_object(impl_id)
363
+ if impl:
364
+ implementations[control_label] = impl
365
+
366
+ self._control_impl_cache[cache_key] = implementations
367
+ except Exception as e:
368
+ logger.error(f"Failed to get implementations for {parent_module}/{parent_id}: {e}")
369
+ return {}
370
+
371
+ return self._control_impl_cache.get(cache_key, {})
372
+
373
+ def clear_cache(self):
374
+ """Clear all cached data."""
375
+ self._catalog_cache.clear()
376
+ self._control_impl_cache.clear()
377
+ logger.info("Cleared control matcher cache")
@@ -4,7 +4,7 @@
4
4
 
5
5
  import logging
6
6
  from datetime import datetime
7
- from typing import Any, Dict, Optional
7
+ from typing import Any, Dict, Optional, Union
8
8
 
9
9
  from regscale.core.app.application import Application
10
10
  from regscale.core.utils.date import get_day_increment
@@ -53,9 +53,13 @@ class DueDateHandler:
53
53
  # Default due date timelines (days)
54
54
  self.default_timelines = {
55
55
  regscale_models.IssueSeverity.Critical: 30,
56
+ "critical": 30,
56
57
  regscale_models.IssueSeverity.High: 60,
58
+ "high": 60,
57
59
  regscale_models.IssueSeverity.Moderate: 120,
60
+ "moderate": 120,
58
61
  regscale_models.IssueSeverity.Low: 364,
62
+ "low": 364,
59
63
  }
60
64
 
61
65
  # Load integration-specific timelines from config
@@ -66,10 +70,10 @@ class DueDateHandler:
66
70
 
67
71
  def _load_integration_timelines(self) -> Dict[regscale_models.IssueSeverity, int]:
68
72
  """
69
- Load timeline configurations for this integration from init.yaml
70
-
71
- :return: Dictionary mapping severity to days
72
- :rtype: Dict[regscale_models.IssueSeverity, int]
73
+ Load timeline configurations for this integration from init.yaml
74
+ mv
75
+ :return: Dictionary mapping severity to days
76
+ :rtype: Dict[regscale_models.IssueSeverity, int]
73
77
  """
74
78
  timelines = self.default_timelines.copy()
75
79
 
@@ -184,7 +188,7 @@ class DueDateHandler:
184
188
 
185
189
  def calculate_due_date(
186
190
  self,
187
- severity: regscale_models.IssueSeverity,
191
+ severity: Union[regscale_models.IssueSeverity, str],
188
192
  created_date: str,
189
193
  cve: Optional[str] = None,
190
194
  title: Optional[str] = None,
@@ -192,7 +196,7 @@ class DueDateHandler:
192
196
  """
193
197
  Calculate the due date for an issue based on severity, KEV status, and integration config
194
198
 
195
- :param regscale_models.IssueSeverity severity: The severity of the issue
199
+ :param Union[regscale_models.IssueSeverity, str] severity: The severity of the issue
196
200
  :param str created_date: The creation date of the issue
197
201
  :param Optional[str] cve: The CVE identifier (if applicable)
198
202
  :param Optional[str] title: The title of the issue (for additional context)
@@ -244,7 +248,9 @@ class DueDateHandler:
244
248
  # Ensure due date is never in the past (allow yesterday for timezone differences)
245
249
  due_date = self._ensure_future_due_date(calculated_due_date, days)
246
250
 
247
- logger.debug(f"Using {self.integration_name} timeline: {severity.name} = {days} days, due date = {due_date}")
251
+ logger.debug(
252
+ f"Using {self.integration_name} timeline: {severity.name if isinstance(severity, regscale_models.IssueSeverity) else severity} = {days} days, due date = {due_date}"
253
+ )
248
254
 
249
255
  return due_date
250
256