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.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +1 -0
- regscale/core/app/internal/control_editor.py +73 -21
- regscale/core/app/internal/login.py +4 -1
- regscale/core/app/internal/model_editor.py +219 -64
- regscale/core/app/utils/app_utils.py +41 -7
- regscale/core/login.py +21 -4
- regscale/core/utils/date.py +77 -1
- regscale/integrations/commercial/aws/scanner.py +7 -3
- regscale/integrations/commercial/microsoft_defender/defender_api.py +1 -1
- regscale/integrations/commercial/sicura/api.py +65 -29
- regscale/integrations/commercial/sicura/scanner.py +36 -7
- regscale/integrations/commercial/synqly/query_builder.py +4 -1
- regscale/integrations/commercial/tenablev2/commands.py +4 -4
- regscale/integrations/commercial/tenablev2/scanner.py +1 -2
- regscale/integrations/commercial/wizv2/scanner.py +40 -16
- regscale/integrations/control_matcher.py +78 -23
- regscale/integrations/public/cci_importer.py +400 -9
- regscale/integrations/public/csam/csam.py +572 -763
- regscale/integrations/public/csam/csam_agency_defined.py +179 -0
- regscale/integrations/public/csam/csam_common.py +154 -0
- regscale/integrations/public/csam/csam_controls.py +432 -0
- regscale/integrations/public/csam/csam_poam.py +124 -0
- regscale/integrations/public/fedramp/click.py +17 -4
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
- regscale/integrations/public/fedramp/poam/scanner.py +74 -7
- regscale/integrations/scanner_integration.py +16 -1
- regscale/models/integration_models/aqua.py +2 -2
- regscale/models/integration_models/cisa_kev_data.json +121 -18
- regscale/models/integration_models/flat_file_importer/__init__.py +4 -6
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +35 -2
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
- regscale/models/platform.py +3 -0
- regscale/models/regscale_models/__init__.py +5 -0
- regscale/models/regscale_models/component.py +1 -1
- regscale/models/regscale_models/control_implementation.py +55 -24
- regscale/models/regscale_models/organization.py +3 -0
- regscale/models/regscale_models/regscale_model.py +17 -5
- regscale/models/regscale_models/security_plan.py +1 -0
- regscale/regscale.py +11 -1
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/RECORD +53 -49
- tests/regscale/core/test_login.py +171 -4
- tests/regscale/integrations/commercial/test_sicura.py +0 -1
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +86 -0
- tests/regscale/integrations/public/test_cci.py +596 -1
- tests/regscale/integrations/test_control_matcher.py +24 -0
- tests/regscale/models/test_control_implementation.py +118 -3
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
736
|
-
f"
|
|
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"
|
|
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
|
-
"
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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}
|