regscale-cli 6.27.1.0__py3-none-any.whl → 6.27.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (53) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -0
  3. regscale/core/app/internal/control_editor.py +73 -21
  4. regscale/core/app/internal/login.py +4 -1
  5. regscale/core/app/internal/model_editor.py +219 -64
  6. regscale/core/app/utils/app_utils.py +41 -7
  7. regscale/core/login.py +21 -4
  8. regscale/core/utils/date.py +77 -1
  9. regscale/integrations/commercial/aws/scanner.py +7 -3
  10. regscale/integrations/commercial/microsoft_defender/defender_api.py +1 -1
  11. regscale/integrations/commercial/sicura/api.py +65 -29
  12. regscale/integrations/commercial/sicura/scanner.py +36 -7
  13. regscale/integrations/commercial/synqly/query_builder.py +4 -1
  14. regscale/integrations/commercial/tenablev2/commands.py +4 -4
  15. regscale/integrations/commercial/tenablev2/scanner.py +1 -2
  16. regscale/integrations/commercial/wizv2/scanner.py +40 -16
  17. regscale/integrations/control_matcher.py +78 -23
  18. regscale/integrations/public/cci_importer.py +400 -9
  19. regscale/integrations/public/csam/csam.py +572 -763
  20. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  21. regscale/integrations/public/csam/csam_common.py +154 -0
  22. regscale/integrations/public/csam/csam_controls.py +432 -0
  23. regscale/integrations/public/csam/csam_poam.py +124 -0
  24. regscale/integrations/public/fedramp/click.py +17 -4
  25. regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
  26. regscale/integrations/public/fedramp/poam/scanner.py +74 -7
  27. regscale/integrations/scanner_integration.py +16 -1
  28. regscale/models/integration_models/aqua.py +2 -2
  29. regscale/models/integration_models/cisa_kev_data.json +121 -18
  30. regscale/models/integration_models/flat_file_importer/__init__.py +4 -6
  31. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  32. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +35 -2
  33. regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
  34. regscale/models/platform.py +3 -0
  35. regscale/models/regscale_models/__init__.py +5 -0
  36. regscale/models/regscale_models/component.py +1 -1
  37. regscale/models/regscale_models/control_implementation.py +55 -24
  38. regscale/models/regscale_models/organization.py +3 -0
  39. regscale/models/regscale_models/regscale_model.py +17 -5
  40. regscale/models/regscale_models/security_plan.py +1 -0
  41. regscale/regscale.py +11 -1
  42. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/METADATA +1 -1
  43. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/RECORD +53 -49
  44. tests/regscale/core/test_login.py +171 -4
  45. tests/regscale/integrations/commercial/test_sicura.py +0 -1
  46. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +86 -0
  47. tests/regscale/integrations/public/test_cci.py +596 -1
  48. tests/regscale/integrations/test_control_matcher.py +24 -0
  49. tests/regscale/models/test_control_implementation.py +118 -3
  50. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/LICENSE +0 -0
  51. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/WHEEL +0 -0
  52. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/entry_points.txt +0 -0
  53. {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/top_level.txt +0 -0
@@ -70,6 +70,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
70
70
  "Medium": regscale_models.IssueSeverity.Moderate,
71
71
  "Low": regscale_models.IssueSeverity.Low,
72
72
  "INFORMATIONAL": regscale_models.IssueSeverity.NotAssigned,
73
+ "INFO": regscale_models.IssueSeverity.NotAssigned, # Wiz uses "INFO" for informational data findings
74
+ "None": regscale_models.IssueSeverity.NotAssigned, # Wiz uses "NONE" for findings without severity
73
75
  }
74
76
  asset_lookup = "vulnerableAsset"
75
77
  wiz_token = None
@@ -706,13 +708,11 @@ class WizVulnerabilityIntegration(ScannerIntegration):
706
708
  :yield: IntegrationFinding objects
707
709
  :rtype: Iterator[IntegrationFinding]
708
710
  """
709
- logger.info(
710
- f"🔍 VULNERABILITY PROCESSING ANALYSIS: Received {len(nodes)} raw Wiz vulnerabilities for processing"
711
- )
711
+ logger.debug(f"VULNERABILITY PROCESSING ANALYSIS: Received {len(nodes)} raw Wiz vulnerabilities for processing")
712
712
 
713
713
  # Count issues by severity for analysis
714
- severity_counts = {}
715
- status_counts = {}
714
+ severity_counts: dict[str, int] = {}
715
+ status_counts: dict[str, int] = {}
716
716
  for node in nodes:
717
717
  severity = node.get("severity", "Low")
718
718
  status = node.get("status", "OPEN")
@@ -728,22 +728,35 @@ class WizVulnerabilityIntegration(ScannerIntegration):
728
728
  for node in nodes:
729
729
  wiz_severity = node.get("severity", "Low")
730
730
  wiz_id = node.get("id", "unknown")
731
+
732
+ # Log sample record for NONE severity (only first occurrence per session)
733
+ if wiz_severity and wiz_severity.upper() == "NONE":
734
+ if not hasattr(self, "_none_severity_sample_logged"):
735
+ logger.info(
736
+ f"SAMPLE RECORD - Vulnerability with NONE severity (treating as informational): "
737
+ f"ID={node.get('id', 'Unknown')}, "
738
+ f"Name={node.get('name', 'Unknown')}, "
739
+ f"Type={node.get('type', 'Unknown')}, "
740
+ f"Severity={wiz_severity}"
741
+ )
742
+ self._none_severity_sample_logged = True
743
+
731
744
  if self.should_process_finding_by_severity(wiz_severity):
732
745
  filtered_nodes.append(node)
733
746
  else:
734
747
  filtered_out_count += 1
735
- logger.info(
736
- f"🚫 FILTERED BY SEVERITY: Vulnerability {wiz_id} with severity '{wiz_severity}' "
748
+ logger.debug(
749
+ f"FILTERED BY SEVERITY: Vulnerability {wiz_id} with severity '{wiz_severity}' "
737
750
  f"filtered due to minimumSeverity configuration"
738
751
  )
739
752
 
740
753
  logger.info(
741
- f"After severity filtering: {len(filtered_nodes)} vulnerabilities kept, {filtered_out_count} filtered out"
754
+ f"After severity filtering: {len(filtered_nodes)} vulnerabilities kept, {filtered_out_count} filtered out"
742
755
  )
743
756
 
744
757
  if not filtered_nodes:
745
758
  logger.warning(
746
- "⚠️ All vulnerabilities filtered out by severity configuration - check your minimumSeverity setting"
759
+ "All vulnerabilities filtered out by severity configuration - check your minimumSeverity setting"
747
760
  )
748
761
  return
749
762
 
@@ -944,10 +957,17 @@ class WizVulnerabilityIntegration(ScannerIntegration):
944
957
  self._severity_config_logged = True
945
958
 
946
959
  # Define severity hierarchy (lower index = higher severity)
947
- severity_hierarchy = ["critical", "high", "medium", "low", "informational"]
960
+ # Note: "info", "informational", and "none" are all treated as informational
961
+ severity_hierarchy = ["critical", "high", "medium", "low", "informational", "info", "none"]
948
962
 
949
963
  try:
950
964
  wiz_severity_lower = wiz_severity.lower()
965
+
966
+ # Handle empty or None severity values - treat as informational
967
+ # Normalize "info" to "informational" for consistent processing
968
+ if not wiz_severity_lower or wiz_severity_lower == "none" or wiz_severity_lower == "info":
969
+ wiz_severity_lower = "informational"
970
+
951
971
  min_severity_index = severity_hierarchy.index(min_severity)
952
972
  finding_severity_index = severity_hierarchy.index(wiz_severity_lower)
953
973
 
@@ -1010,8 +1030,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
1010
1030
  return graph_entity.get("id")
1011
1031
 
1012
1032
  # Standard case - direct id access
1013
- asset_container = node.get(asset_lookup_key, {})
1014
- asset_id = asset_container.get("id")
1033
+ asset_container = node.get(asset_lookup_key) or {}
1034
+ asset_id = asset_container.get("id") if isinstance(asset_container, dict) else None
1015
1035
 
1016
1036
  # Add debug logging to help diagnose missing assets
1017
1037
  if not asset_id:
@@ -1023,8 +1043,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
1023
1043
  fallback_keys = ["vulnerableAsset", "resource", "exposedEntity", "entitySnapshot"]
1024
1044
  for fallback_key in fallback_keys:
1025
1045
  if fallback_key != asset_lookup_key and fallback_key in node:
1026
- fallback_asset = node.get(fallback_key, {})
1027
- if fallback_id := fallback_asset.get("id"):
1046
+ fallback_asset = node.get(fallback_key) or {}
1047
+ if isinstance(fallback_asset, dict) and (fallback_id := fallback_asset.get("id")):
1028
1048
  logger.debug(
1029
1049
  f"Found asset ID using fallback key '{fallback_key}' for {vulnerability_type.value}"
1030
1050
  )
@@ -1068,7 +1088,11 @@ class WizVulnerabilityIntegration(ScannerIntegration):
1068
1088
  return graph_entity.get("providerUniqueId") or graph_entity.get("name") or graph_entity.get("id")
1069
1089
 
1070
1090
  # Standard case - get asset container and extract provider identifier
1071
- asset_container = node.get(asset_lookup_key, {})
1091
+ asset_container = node.get(asset_lookup_key) or {}
1092
+
1093
+ # Ensure asset_container is a dict before accessing
1094
+ if not isinstance(asset_container, dict):
1095
+ return None
1072
1096
 
1073
1097
  # For Issue queries, the field is called 'providerId' instead of 'providerUniqueId'
1074
1098
  if vulnerability_type == WizVulnerabilityType.ISSUE:
@@ -1167,7 +1191,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
1167
1191
  # Get asset identifier
1168
1192
  asset_id = self.get_asset_id_from_node(node, vulnerability_type)
1169
1193
  if not asset_id:
1170
- logger.warning(
1194
+ logger.debug(
1171
1195
  f"Skipping {vulnerability_type.value} finding '{node.get('name', 'Unknown')}' "
1172
1196
  f"(ID: {node.get('id', 'Unknown')}) - no asset identifier found"
1173
1197
  )
@@ -7,7 +7,7 @@ across different RegScale entities based on control ID strings.
7
7
 
8
8
  import logging
9
9
  import re
10
- from typing import Dict, List, Optional, Tuple, Union
10
+ from typing import Dict, List, Optional, Tuple
11
11
 
12
12
  from regscale.core.app.api import Api
13
13
  from regscale.core.app.application import Application
@@ -42,14 +42,15 @@ class ControlMatcher:
42
42
  self._catalog_cache: Dict[int, List[SecurityControl]] = {}
43
43
  self._control_impl_cache: Dict[Tuple[int, str], Dict[str, ControlImplementation]] = {}
44
44
 
45
- def parse_control_id(self, control_string: str) -> Optional[str]:
45
+ @staticmethod
46
+ def parse_control_id(control_string: str) -> Optional[str]:
46
47
  """
47
48
  Parse a control ID string and extract the standardized control identifier.
48
49
 
49
50
  Handles various formats:
50
- - NIST format: AC-1, AC-1(1), AC-1.1
51
+ - NIST format: AC-1, AC-1(1), AC-1.1, AC-1(a), AC-1.a
51
52
  - With leading zeros: AC-01, AC-17(02)
52
- - With spaces: AC-1 (1), AC-02 (04)
53
+ - With spaces: AC-1 (1), AC-02 (04), AC-1 (a)
53
54
  - With text: "Access Control AC-1"
54
55
  - Multiple controls: "AC-1, AC-2"
55
56
 
@@ -64,12 +65,12 @@ class ControlMatcher:
64
65
  control_string = control_string.strip().upper()
65
66
 
66
67
  # 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
+ # Matches: AC-1, AC-01, AC-1(1), AC-1(01), AC-1 (1), AC-1.1, AC-1.01, AC-1(a), AC-1.a, etc.
68
69
  # Allows optional whitespace before and inside parentheses
69
- pattern = r"([A-Z]{2,3}-\d+(?:\s*\(\s*\d+\s*\)|\.\d+)?)"
70
+ # Now includes letter parts (a, b, c) in addition to numeric parts
71
+ pattern = r"([A-Z]{2,3}-\d+(?:\s*\(\s*(?:\d+|[A-Z])\s*\)|\.(?:\d+|[A-Z]))?)"
70
72
 
71
- matches = re.findall(pattern, control_string)
72
- if matches:
73
+ if matches := re.findall(pattern, control_string):
73
74
  # Normalize parentheses to dots for consistency and remove spaces
74
75
  control_id = matches[0]
75
76
  control_id = control_id.replace(" ", "") # Remove all spaces
@@ -84,7 +85,9 @@ class ControlMatcher:
84
85
  if "." in number_part:
85
86
  main_num, enhancement = number_part.split(".", 1)
86
87
  main_num = str(int(main_num))
87
- enhancement = str(int(enhancement))
88
+ # Only normalize if enhancement is numeric, preserve letters as-is
89
+ if enhancement.isdigit():
90
+ enhancement = str(int(enhancement))
88
91
  control_id = f"{family}-{main_num}.{enhancement}"
89
92
  else:
90
93
  main_num = str(int(number_part))
@@ -295,6 +298,64 @@ class ControlMatcher:
295
298
  main_num = str(int(number_part))
296
299
  return f"{family}-{main_num}"
297
300
 
301
+ @staticmethod
302
+ def _generate_simple_variations(family: str, main_num: str) -> set:
303
+ """
304
+ Generate variations for simple control IDs without enhancements.
305
+
306
+ :param str family: Control family (e.g., AC, SI)
307
+ :param str main_num: Main control number
308
+ :return: Set of variations
309
+ :rtype: set
310
+ """
311
+ main_int = int(main_num)
312
+ return {
313
+ f"{family}-{main_int}",
314
+ f"{family}-{main_int:02d}",
315
+ }
316
+
317
+ @staticmethod
318
+ def _generate_letter_enhancement_variations(family: str, main_num: str, enhancement: str) -> set:
319
+ """
320
+ Generate variations for control IDs with letter-based enhancements.
321
+
322
+ :param str family: Control family (e.g., AC, SI)
323
+ :param str main_num: Main control number
324
+ :param str enhancement: Letter enhancement (e.g., a, b)
325
+ :return: Set of variations
326
+ :rtype: set
327
+ """
328
+ main_int = int(main_num)
329
+ variations = set()
330
+
331
+ for main_fmt in [str(main_int), f"{main_int:02d}"]:
332
+ variations.add(f"{family}-{main_fmt}.{enhancement}")
333
+ variations.add(f"{family}-{main_fmt}({enhancement})")
334
+
335
+ return variations
336
+
337
+ @staticmethod
338
+ def _generate_numeric_enhancement_variations(family: str, main_num: str, enhancement: str) -> set:
339
+ """
340
+ Generate variations for control IDs with numeric enhancements.
341
+
342
+ :param str family: Control family (e.g., AC, SI)
343
+ :param str main_num: Main control number
344
+ :param str enhancement: Numeric enhancement
345
+ :return: Set of variations
346
+ :rtype: set
347
+ """
348
+ main_int = int(main_num)
349
+ enh_int = int(enhancement)
350
+ variations = set()
351
+
352
+ for main_fmt in [str(main_int), f"{main_int:02d}"]:
353
+ for enh_fmt in [str(enh_int), f"{enh_int:02d}"]:
354
+ variations.add(f"{family}-{main_fmt}.{enh_fmt}")
355
+ variations.add(f"{family}-{main_fmt}({enh_fmt})")
356
+
357
+ return variations
358
+
298
359
  def _get_control_id_variations(self, control_id: str) -> set:
299
360
  """
300
361
  Generate all valid variations of a control ID (with and without leading zeros).
@@ -302,6 +363,7 @@ class ControlMatcher:
302
363
  Examples:
303
364
  - AC-1 -> {AC-1, AC-01}
304
365
  - AC-17.2 -> {AC-17.2, AC-17.02, AC-17(2), AC-17(02)}
366
+ - AC-1.a -> {AC-1.a, AC-01.a, AC-1(a), AC-01(a)}
305
367
 
306
368
  :param str control_id: The control ID to generate variations for
307
369
  :return: Set of all valid variations
@@ -311,8 +373,6 @@ class ControlMatcher:
311
373
  if not parsed:
312
374
  return set()
313
375
 
314
- variations = set()
315
-
316
376
  # Split by '-' to get family and number parts
317
377
  parts = parsed.split("-")
318
378
  if len(parts) != 2:
@@ -324,19 +384,14 @@ class ControlMatcher:
324
384
  # Handle enhancement notation
325
385
  if "." in number_part:
326
386
  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})")
387
+
388
+ # Check if enhancement is a letter (a, b, c, etc.) or a number
389
+ if enhancement.isalpha():
390
+ variations = self._generate_letter_enhancement_variations(family, main_num, enhancement)
391
+ else:
392
+ variations = self._generate_numeric_enhancement_variations(family, main_num, enhancement)
335
393
  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}")
394
+ variations = self._generate_simple_variations(family, number_part)
340
395
 
341
396
  # Add uppercase versions to ensure consistency
342
397
  return {v.upper() for v in variations}