regscale-cli 6.26.0.0__py3-none-any.whl → 6.27.0.1__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 (96) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -1
  3. regscale/core/app/internal/evidence.py +419 -2
  4. regscale/dev/code_gen.py +24 -20
  5. regscale/integrations/commercial/__init__.py +0 -1
  6. regscale/integrations/commercial/jira.py +367 -126
  7. regscale/integrations/commercial/qualys/__init__.py +7 -8
  8. regscale/integrations/commercial/qualys/scanner.py +8 -3
  9. regscale/integrations/commercial/synqly/assets.py +17 -0
  10. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  11. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  12. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  13. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  14. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  15. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  16. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  17. regscale/integrations/commercial/wizv2/click.py +44 -59
  18. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  19. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  20. regscale/integrations/commercial/wizv2/compliance_report.py +10 -9
  21. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  22. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  23. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  24. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  25. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  26. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  27. regscale/integrations/commercial/wizv2/issue.py +1 -1
  28. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  29. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  30. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  31. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  33. regscale/integrations/commercial/wizv2/reports.py +1 -1
  34. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  35. regscale/integrations/commercial/wizv2/scanner.py +40 -100
  36. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  37. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  38. regscale/integrations/commercial/wizv2/variables.py +89 -3
  39. regscale/integrations/compliance_integration.py +0 -46
  40. regscale/integrations/control_matcher.py +22 -3
  41. regscale/integrations/due_date_handler.py +14 -8
  42. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  43. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  44. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  45. regscale/integrations/scanner_integration.py +127 -57
  46. regscale/models/integration_models/cisa_kev_data.json +132 -9
  47. regscale/models/integration_models/qualys.py +3 -4
  48. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  49. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  50. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  51. regscale/models/regscale_models/control_implementation.py +1 -1
  52. regscale/models/regscale_models/issue.py +0 -1
  53. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/METADATA +1 -17
  54. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/RECORD +94 -61
  55. tests/regscale/integrations/commercial/test_jira.py +481 -91
  56. tests/regscale/integrations/commercial/test_wiz.py +96 -200
  57. tests/regscale/integrations/commercial/wizv2/__init__.py +1 -1
  58. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  59. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  60. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  61. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  62. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  63. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  64. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  65. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  66. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  67. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  68. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  69. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  70. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  71. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  72. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  73. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  74. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  75. tests/regscale/integrations/commercial/wizv2/test_issue.py +1 -1
  76. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  77. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  78. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  79. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  80. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +1 -1
  81. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +72 -29
  82. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  83. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  84. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +946 -78
  85. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +97 -202
  86. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  87. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  88. tests/regscale/integrations/public/test_fedramp.py +301 -0
  89. tests/regscale/integrations/test_control_matcher.py +83 -0
  90. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  91. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +0 -750
  92. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  93. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/LICENSE +0 -0
  94. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/WHEEL +0 -0
  95. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/entry_points.txt +0 -0
  96. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.1.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,89 @@ from regscale.core.app.utils.app_utils import get_current_datetime
8
8
  from regscale.integrations.scanner_integration import IntegrationFinding, issue_due_date
9
9
  from regscale.models import regscale_models
10
10
 
11
- logger = logging.getLogger(__name__)
11
+ logger = logging.getLogger("regscale")
12
+
13
+ # Constants
14
+ DEFAULT_CCI_REF = "CCI-000366" # Default CCI reference for STIG findings
15
+ UNKNOWN_VULN_NUM = "unknown"
16
+ _SOLUTION_KEYWORD = "Solution:"
17
+ _REFERENCE_INFO_KEYWORD = "Reference Information:"
18
+
19
+ # Status mapping for STIG results
20
+ _RESULT_STATUS_MAP = {
21
+ "PASSED": regscale_models.ChecklistStatus.PASS,
22
+ "FAILED": regscale_models.ChecklistStatus.FAIL,
23
+ "ERROR": regscale_models.ChecklistStatus.FAIL,
24
+ "NOT APPLICABLE": regscale_models.ChecklistStatus.NOT_APPLICABLE,
25
+ "NOT_APPLICABLE": regscale_models.ChecklistStatus.NOT_APPLICABLE,
26
+ }
27
+
28
+ # Severity mapping for STIG findings
29
+ _SEVERITY_MAP = {
30
+ "critical": regscale_models.IssueSeverity.Critical,
31
+ "high": regscale_models.IssueSeverity.High,
32
+ "medium": regscale_models.IssueSeverity.Moderate,
33
+ "low": regscale_models.IssueSeverity.Low,
34
+ }
35
+
36
+
37
+ def _extract_field(pattern: str, text: str, flags: Union[int, re.RegexFlag] = 0, group: int = 1) -> str:
38
+ """
39
+ Extracts a field from a string using a regular expression.
40
+
41
+ :param str pattern: The regular expression pattern to search for
42
+ :param str text: The string to search in
43
+ :param int flags: Optional regular expression flags, defaults to 0
44
+ :param int group: The group number to return from the match, defaults to 1
45
+ :return: The extracted field as a string. Empty string if no match was found
46
+ :rtype: str
47
+ """
48
+ match = re.search(pattern, text, flags)
49
+ return match.group(group).strip() if match else ""
50
+
51
+
52
+ def _extract_reference_dict(output: str) -> dict:
53
+ """
54
+ Extract and parse reference information into dictionary.
55
+
56
+ :param str output: The STIG output string
57
+ :return: Dictionary of reference information
58
+ :rtype: dict
59
+ """
60
+ ref_info = _extract_field(r"Reference Information:\s*(.+)", output, flags=re.DOTALL)
61
+ ref_dict = {}
62
+ for item in ref_info.split(","):
63
+ if "|" in item:
64
+ parts = item.split("|", 1)
65
+ if len(parts) == 2:
66
+ ref_dict[parts[0].strip()] = parts[1].strip()
67
+ return ref_dict
68
+
69
+
70
+ def _map_result_to_status(result: str) -> regscale_models.ChecklistStatus:
71
+ """
72
+ Map result string to ChecklistStatus enum.
73
+
74
+ :param str result: The result string from STIG output
75
+ :return: ChecklistStatus enum value
76
+ :rtype: regscale_models.ChecklistStatus
77
+ """
78
+ result_key = result.upper().replace("_", " ").strip()
79
+ if result_key not in _RESULT_STATUS_MAP:
80
+ logger.warning("Result '%s' not found in status map", result)
81
+ return regscale_models.ChecklistStatus.NOT_REVIEWED
82
+ return _RESULT_STATUS_MAP[result_key]
83
+
84
+
85
+ def _map_severity_to_enum(severity: str) -> regscale_models.IssueSeverity:
86
+ """
87
+ Map severity string to IssueSeverity enum.
88
+
89
+ :param str severity: The severity string
90
+ :return: IssueSeverity enum value
91
+ :rtype: regscale_models.IssueSeverity
92
+ """
93
+ return _SEVERITY_MAP.get(severity.lower(), regscale_models.IssueSeverity.NotAssigned)
12
94
 
13
95
 
14
96
  def parse_stig_output(output: str, finding: IntegrationFinding) -> IntegrationFinding:
@@ -20,43 +102,39 @@ def parse_stig_output(output: str, finding: IntegrationFinding) -> IntegrationFi
20
102
  :return: An IntegrationFinding object containing the parsed finding information.
21
103
  :rtype: IntegrationFinding
22
104
  """
23
-
24
- def _extract_field(pattern: str, text: str, flags: Union[int, re.RegexFlag] = 0, group: int = 1) -> str:
25
- """
26
- Extracts a field from a string using a regular expression
27
-
28
- :param str pattern: The regular expression pattern to search for
29
- :param str text: The string to search in
30
- :param int flags: Optional regular expression flags, defaults to 0
31
- :param int group: The group number to return from the match, defaults to 1
32
- :return: The extracted field as a string. Empty string if no match was found
33
- :rtype: str
34
- """
35
- match = re.search(pattern, text, flags)
36
- return match.group(group).strip() if match else ""
37
-
38
- # Extract fields
39
- check_name_full = _extract_field(r"Check Name:\s*(.*?)\n", output, flags=re.DOTALL | re.MULTILINE)
105
+ # Extract fields using optimized regex patterns (preventing catastrophic backtracking)
106
+ check_name_full = _extract_field(r"Check Name:\s*([^\n]+)", output)
40
107
  check_name_parts = check_name_full.split(":", 1)
41
108
  rule_id = check_name_parts[0].strip()
42
109
  check_description = check_name_parts[1].strip() if len(check_name_parts) > 1 else ""
43
110
 
111
+ # Extract baseline from Target keyword
44
112
  baseline = _extract_field(r"(.*?)\s+Target\s+(.*)", check_description, group=2)
45
- if target_match := _extract_field(r"(.*?)\s+Target\s+(.*)", check_description):
113
+ target_match = _extract_field(r"(.*?)\s+Target\s+(.*)", check_description)
114
+ if target_match:
46
115
  check_description = check_description[: check_description.find(target_match) + len(target_match)].strip()
47
116
 
48
- information = _extract_field(r"Information:\s*(.*?)\n", output, flags=re.DOTALL | re.MULTILINE)
49
- vuln_discuss = _extract_field(r"VulnDiscussion='(.*?)'\s", output, flags=re.DOTALL | re.MULTILINE)
50
- result = _extract_field(r"Result:\s*(.*?)(?:\n|$)", output, flags=re.IGNORECASE | re.DOTALL)
51
- solution = _extract_field(r"Solution:\s*(.*?)\n\nReference Information:", output, flags=re.DOTALL | re.MULTILINE)
117
+ information = _extract_field(r"Information:\s*([^\n]+)", output)
118
+ vuln_discuss = _extract_field(r"VulnDiscussion='([^']+)'\s", output)
119
+ result = _extract_field(r"Result:\s*([^\n]+)", output, flags=re.IGNORECASE)
120
+ # Extract solution using a safe pattern that avoids backtracking
121
+ # Split on reference keyword and take the solution section
122
+ if _SOLUTION_KEYWORD in output and _REFERENCE_INFO_KEYWORD in output:
123
+ solution_start = output.find(_SOLUTION_KEYWORD) + len(_SOLUTION_KEYWORD)
124
+ ref_start = output.find(_REFERENCE_INFO_KEYWORD, solution_start)
125
+ solution = output[solution_start:ref_start].strip()
126
+ elif _SOLUTION_KEYWORD in output:
127
+ solution_start = output.find(_SOLUTION_KEYWORD) + len(_SOLUTION_KEYWORD)
128
+ solution = output[solution_start:].strip()
129
+ else:
130
+ solution = ""
52
131
 
53
- # Extract reference information
54
- ref_info = _extract_field(r"Reference Information:\s*(.*)", output, flags=re.DOTALL | re.MULTILINE)
55
- ref_dict = dict(item.split("|", 1) for item in ref_info.split(",") if "|" in item)
132
+ # Extract reference information using helper function
133
+ ref_dict = _extract_reference_dict(output)
56
134
 
57
135
  # Extract specific references
58
- cci_ref = ref_dict.get("CCI", "CCI-000366")
59
- severity = ref_dict.get("SEVERITY", "").lower()
136
+ cci_ref = ref_dict.get("CCI", DEFAULT_CCI_REF)
137
+ severity_str = ref_dict.get("SEVERITY", "").lower()
60
138
  oval_def = ref_dict.get("OVAL-DEF", "")
61
139
  generated_date = ref_dict.get("GENERATED-DATE", "")
62
140
  updated_date = ref_dict.get("UPDATED-DATE", "")
@@ -64,36 +142,19 @@ def parse_stig_output(output: str, finding: IntegrationFinding) -> IntegrationFi
64
142
  rule_id_full = ref_dict.get("RULE-ID", "")
65
143
  group_id = ref_dict.get("GROUP-ID", "")
66
144
 
145
+ # Extract vulnerability number
67
146
  vuln_num_match = re.search(r"SV-\d+r\d+_rule", rule_id)
68
- vuln_num = vuln_num_match.group(0) if vuln_num_match else "unknown"
147
+ vuln_num = vuln_num_match.group(0) if vuln_num_match else UNKNOWN_VULN_NUM
69
148
 
70
149
  title = f"{vuln_num}: {check_description}"
71
150
  issue_title = title
72
151
 
73
- status_map = {
74
- "PASSED": regscale_models.ChecklistStatus.PASS,
75
- "FAILED": regscale_models.ChecklistStatus.FAIL,
76
- "ERROR": regscale_models.ChecklistStatus.FAIL,
77
- "NOT APPLICABLE": regscale_models.ChecklistStatus.NOT_APPLICABLE,
78
- "NOT_APPLICABLE": regscale_models.ChecklistStatus.NOT_APPLICABLE,
79
- }
152
+ # Map result to status using helper function
153
+ status = _map_result_to_status(result)
80
154
 
81
- result_key = result.upper().replace("_", " ").strip()
82
- if result_key not in status_map:
83
- logger.warning(f"Result '{result}' not found in status map")
84
- status = regscale_models.ChecklistStatus.NOT_REVIEWED
85
- else:
86
- status = status_map[result_key]
87
-
88
- # Map severity to IssueSeverity enum
89
- priority = (severity or "").title()
90
- severity_map = {
91
- "critical": regscale_models.IssueSeverity.Critical,
92
- "high": regscale_models.IssueSeverity.High,
93
- "medium": regscale_models.IssueSeverity.Moderate,
94
- "low": regscale_models.IssueSeverity.Low,
95
- }
96
- severity = severity_map.get(severity.lower(), regscale_models.IssueSeverity.NotAssigned)
155
+ # Map severity to IssueSeverity enum using helper function
156
+ priority = (severity_str or "").title()
157
+ severity = _map_severity_to_enum(severity_str)
97
158
 
98
159
  results = (
99
160
  f"Vulnerability Number: {vuln_num}, Severity: {severity.value}, "
@@ -132,9 +193,4 @@ def parse_stig_output(output: str, finding: IntegrationFinding) -> IntegrationFi
132
193
  finding.rule_id_full = rule_id_full
133
194
  finding.group_id = group_id
134
195
 
135
- # Future values
136
- # finding.comments = ""
137
- # finding.poam_comments = ""
138
- # finding.rule_version = ""
139
-
140
196
  return finding
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta
5
5
  from typing import Optional, Dict, Any, List
6
6
 
7
7
  from regscale.core.app.utils.app_utils import error_and_exit, check_file_path
8
- from regscale.integrations.commercial.wizv2.constants import CONTENT_TYPE
8
+ from regscale.integrations.commercial.wizv2.core.constants import CONTENT_TYPE
9
9
  from regscale.core.app.application import Application
10
10
  from regscale.utils import PaginatedGraphQLClient
11
11
 
@@ -25,7 +25,7 @@ def wiz():
25
25
  @click.option("--client_secret", default=None, hide_input=True, required=False) # type: ignore
26
26
  def authenticate(client_id, client_secret):
27
27
  """Authenticate to Wiz."""
28
- from regscale.integrations.commercial.wizv2.wiz_auth import wiz_authenticate
28
+ from regscale.integrations.commercial.wizv2.core.auth import wiz_authenticate
29
29
 
30
30
  wiz_authenticate(client_id, client_secret)
31
31
 
@@ -136,7 +136,7 @@ def issues(
136
136
  Process Issues from Wiz into RegScale
137
137
  """
138
138
  from regscale.core.app.utils.app_utils import check_license
139
- from regscale.integrations.commercial.wizv2.wiz_auth import wiz_authenticate
139
+ from regscale.integrations.commercial.wizv2.core.auth import wiz_authenticate
140
140
  from regscale.integrations.commercial.wizv2.issue import WizIssue
141
141
  import json
142
142
 
@@ -191,7 +191,7 @@ def attach_sbom(
191
191
  standard="CycloneDX",
192
192
  ):
193
193
  """Download SBOMs from a Wiz report by ID and add them to the corresponding RegScale assets."""
194
- from regscale.integrations.commercial.wizv2.wiz_auth import wiz_authenticate
194
+ from regscale.integrations.commercial.wizv2.core.auth import wiz_authenticate
195
195
  from regscale.integrations.commercial.wizv2.utils import fetch_sbom_report
196
196
 
197
197
  if client_secret is None:
@@ -212,15 +212,6 @@ def attach_sbom(
212
212
  )
213
213
 
214
214
 
215
- @wiz.command()
216
- def threats():
217
- """Process threats from Wiz -> Coming soon"""
218
- from regscale.core.app.utils.app_utils import check_license
219
-
220
- check_license()
221
- logger.info("Threats - COMING SOON")
222
-
223
-
224
215
  @wiz.command()
225
216
  @click.option( # type: ignore
226
217
  "--wiz_project_id",
@@ -300,8 +291,12 @@ def vulnerabilities(
300
291
  )
301
292
  @click.option("--evidence_id", "-e", help="Wiz Evidence ID", required=True, type=int) # type: ignore
302
293
  @click.option("--report_id", "-r", help="Wiz Report ID", required=True) # type: ignore
303
- @click.option("--report_file_name", "-n", help="Report file name", default="evidence_report", required=False) # type: ignore
304
- @click.option("--report_file_extension", "-e", help="Report file extension", default="csv", required=False) # type: ignore
294
+ @click.option(
295
+ "--report_file_name", "-n", help="Report file name", default="evidence_report", required=False
296
+ ) # type: ignore
297
+ @click.option(
298
+ "--report_file_extension", "-e", help="Report file extension", default="csv", required=False
299
+ ) # type: ignore
305
300
  def add_report_evidence(
306
301
  client_id,
307
302
  client_secret,
@@ -311,7 +306,7 @@ def add_report_evidence(
311
306
  report_file_extension: str = "csv",
312
307
  ):
313
308
  """Download a Wiz report by ID and Attach to Evidence locker"""
314
- from regscale.integrations.commercial.wizv2.wiz_auth import wiz_authenticate
309
+ from regscale.integrations.commercial.wizv2.core.auth import wiz_authenticate
315
310
  from regscale.integrations.commercial.wizv2.utils import fetch_report_by_id
316
311
 
317
312
  if client_secret is None:
@@ -324,7 +319,10 @@ def add_report_evidence(
324
319
  client_secret=client_secret,
325
320
  )
326
321
  fetch_report_by_id(
327
- report_id, parent_id=evidence_id, report_file_name=report_file_name, report_file_extension=report_file_extension
322
+ report_id,
323
+ parent_id=evidence_id,
324
+ report_file_name=report_file_name,
325
+ report_file_extension=report_file_extension,
328
326
  )
329
327
 
330
328
 
@@ -361,7 +359,10 @@ def add_report_evidence(
361
359
  @click.option( # type: ignore
362
360
  "--framework_id",
363
361
  "-f",
364
- help="Wiz framework ID or shorthand (e.g., 'nist', 'aws', 'wf-id-4'). Use --list-frameworks to see options. Default: wf-id-4 (NIST SP 800-53 Rev 5)",
362
+ help=(
363
+ "Wiz framework ID or shorthand (e.g., 'nist', 'aws', 'wf-id-4'). "
364
+ "Use --list-frameworks to see options. Default: wf-id-4 (NIST SP 800-53 Rev 5)"
365
+ ),
365
366
  default="wf-id-4",
366
367
  required=False,
367
368
  )
@@ -420,67 +421,51 @@ def sync_compliance(
420
421
  """
421
422
  Sync policy compliance assessments from Wiz to RegScale.
422
423
 
423
- This command fetches policy assessment data from Wiz and creates:
424
+ This command now uses CSV compliance reports from Wiz instead of GraphQL API.
425
+ It creates:
424
426
  - Control assessments based on policy compliance results
425
427
  - Issues for failed policy assessments (if --create-issues enabled)
426
428
  - Updates to control implementation status (if --update-control-status enabled)
427
- - JSON output file with policy compliance data in artifacts/wiz/ directory
428
- - Cached framework mapping for improved performance
429
429
 
430
- CACHING:
431
- By default, the command will reuse cached policy data if it's newer than the cache
432
- duration (default: 60 minutes). Use --refresh to force fresh data from Wiz.
433
- Use --cache-duration to adjust how long cached data is considered valid.
430
+ NOTE: This command is deprecated. Use 'compliance_report' command instead.
434
431
  """
435
- from regscale.integrations.commercial.wizv2.policy_compliance import (
436
- WizPolicyComplianceIntegration,
437
- list_available_frameworks,
438
- resolve_framework_id,
439
- )
432
+ click.echo("⚠️ sync_compliance is deprecated and now uses compliance_report implementation.")
433
+ click.echo(" Consider using 'regscale wiz compliance_report' directly for future use.\n")
440
434
 
441
- # Handle --list-frameworks flag
435
+ # Handle --list-frameworks flag (no longer supported, inform user)
442
436
  if list_frameworks:
443
- click.echo(list_available_frameworks())
437
+ click.echo("❌ --list-frameworks is no longer supported in the deprecated sync_compliance command.")
438
+ click.echo(" Please use 'regscale wiz compliance_report' instead.\n")
444
439
  return
445
440
 
446
441
  # Use environment variables if not provided
447
- if client_secret is None:
442
+ if client_secret is None or client_secret == "":
448
443
  client_secret = WizVariables.wizClientSecret
449
- if client_id is None:
444
+ if client_id is None or client_id == "":
450
445
  client_id = WizVariables.wizClientId
451
446
 
452
- # Resolve framework ID using the enhanced framework resolution
453
- try:
454
- resolved_framework_id = resolve_framework_id(framework_id.lower())
455
- if resolved_framework_id != framework_id:
456
- from regscale.integrations.commercial.wizv2.policy_compliance import FRAMEWORK_MAPPINGS
457
-
458
- framework_name = FRAMEWORK_MAPPINGS.get(resolved_framework_id, resolved_framework_id)
459
- click.echo(f"🔍 Resolved '{framework_id}' to '{framework_name}' ({resolved_framework_id})")
460
- except ValueError as e:
461
- click.echo(f"❌ {str(e)}")
462
- click.echo("\nUse --list-frameworks to see all available options.")
463
- return
447
+ # Import compliance_report processor instead of GraphQL-based integration
448
+ from regscale.integrations.commercial.wizv2.compliance_report import WizComplianceReportProcessor
464
449
 
465
- # Create and run the policy compliance integration
466
- policy_integration = WizPolicyComplianceIntegration(
450
+ # Create processor with similar options to compliance_report command
451
+ processor = WizComplianceReportProcessor(
467
452
  plan_id=regscale_id,
468
453
  wiz_project_id=wiz_project_id,
469
454
  client_id=client_id,
470
455
  client_secret=client_secret,
471
- framework_id=resolved_framework_id,
472
456
  regscale_module=regscale_module,
473
457
  create_poams=create_poams,
474
- cache_duration_minutes=cache_duration,
475
- force_refresh=refresh,
476
- )
477
-
478
- # Run the policy compliance sync
479
- policy_integration.sync_policy_compliance(
480
458
  create_issues=create_issues,
481
459
  update_control_status=update_control_status,
460
+ report_file_path=None, # Will create new report
461
+ force_fresh_report=refresh, # Map --refresh to force_fresh_report
462
+ reuse_existing_reports=not refresh, # Inverse of refresh
463
+ bypass_control_filtering=True, # Enable for performance
482
464
  )
483
465
 
466
+ # Process the compliance report using new ComplianceIntegration pattern
467
+ processor.process_compliance_sync()
468
+
484
469
 
485
470
  @wiz.command(name="compliance_report")
486
471
  @click.option(
@@ -496,7 +481,7 @@ def sync_compliance(
496
481
  "--client_id",
497
482
  "-i",
498
483
  help="Wiz Client ID, or can be set as environment variable wizClientId",
499
- default="",
484
+ default=None,
500
485
  hide_input=False,
501
486
  required=False,
502
487
  )
@@ -504,7 +489,7 @@ def sync_compliance(
504
489
  "--client_secret",
505
490
  "-s",
506
491
  help="Wiz Client Secret, or can be set as environment variable wizClientSecret",
507
- default="",
492
+ default=None,
508
493
  hide_input=True,
509
494
  required=False,
510
495
  )
@@ -581,10 +566,10 @@ def compliance_report(
581
566
  """
582
567
  from regscale.integrations.commercial.wizv2.compliance_report import WizComplianceReportProcessor
583
568
 
584
- # Use environment variables if not provided
585
- if client_secret is None:
569
+ # Use environment variables if not provided or empty
570
+ if not client_secret:
586
571
  client_secret = WizVariables.wizClientSecret
587
- if client_id is None:
572
+ if not client_id:
588
573
  client_id = WizVariables.wizClientId
589
574
 
590
575
  # Create and run the compliance report processor
@@ -0,0 +1,15 @@
1
+ """Compliance-specific functionality for Wiz integration."""
2
+
3
+ from regscale.integrations.commercial.wizv2.compliance.helpers import (
4
+ AssetConsolidator,
5
+ ControlAssessmentProcessor,
6
+ ControlImplementationCache,
7
+ IssueFieldSetter,
8
+ )
9
+
10
+ __all__ = [
11
+ "AssetConsolidator",
12
+ "ControlAssessmentProcessor",
13
+ "ControlImplementationCache",
14
+ "IssueFieldSetter",
15
+ ]
@@ -266,32 +266,39 @@ class IssueFieldSetter:
266
266
  )
267
267
 
268
268
  for impl in implementations:
269
- if not hasattr(impl, "controlID") or not impl.controlID:
270
- continue
271
-
272
- # Check cache for security control
273
- security_control = self.cache.get_security_control(impl.controlID)
274
- if not security_control:
275
- security_control = regscale_models.SecurityControl.get_object(object_id=impl.controlID)
276
- if security_control:
277
- self.cache.set_security_control(impl.controlID, security_control)
278
-
279
- if security_control and hasattr(security_control, "controlId"):
280
- from regscale.integrations.commercial.wizv2.policy_compliance import WizPolicyComplianceIntegration
281
-
282
- impl_control_id = WizPolicyComplianceIntegration._normalize_control_id_string(
283
- security_control.controlId
284
- )
285
-
286
- if impl_control_id == control_id:
287
- logger.debug(f"✓ Found control implementation {impl.id} for control {control_id}")
288
- return impl.id
269
+ if impl_id := self._check_implementation_match(impl, control_id):
270
+ return impl_id
289
271
 
290
272
  return None
291
273
  except Exception as e:
292
274
  logger.error(f"Error finding control implementation for {control_id}: {e}")
293
275
  return None
294
276
 
277
+ def _check_implementation_match(self, impl, control_id: str) -> Optional[int]:
278
+ """Check if implementation matches the control ID."""
279
+ if not hasattr(impl, "controlID") or not impl.controlID:
280
+ return None
281
+
282
+ # Check cache for security control
283
+ security_control = self.cache.get_security_control(impl.controlID)
284
+ if not security_control:
285
+ security_control = regscale_models.SecurityControl.get_object(object_id=impl.controlID)
286
+ if security_control:
287
+ self.cache.set_security_control(impl.controlID, security_control)
288
+
289
+ if not security_control or not hasattr(security_control, "controlId"):
290
+ return None
291
+
292
+ from regscale.integrations.commercial.wizv2.policy_compliance import WizPolicyComplianceIntegration
293
+
294
+ impl_control_id = WizPolicyComplianceIntegration._normalize_control_id_string(security_control.controlId)
295
+
296
+ if impl_control_id == control_id:
297
+ logger.debug(f"✓ Found control implementation {impl.id} for control {control_id}")
298
+ return impl.id
299
+
300
+ return None
301
+
295
302
  def _get_or_find_assessment_id(self, impl_id: int) -> Optional[int]:
296
303
  """
297
304
  Get assessment ID from cache or database.
@@ -480,22 +487,26 @@ class ControlAssessmentProcessor:
480
487
  assessments = regscale_models.Assessment.get_all_by_parent(parent_id=impl_id, parent_module="controls")
481
488
 
482
489
  for assessment in assessments:
483
- if hasattr(assessment, "actualFinish") and assessment.actualFinish:
484
- try:
485
- if isinstance(assessment.actualFinish, str):
486
- assessment_date = regscale_string_to_datetime(assessment.actualFinish).date()
487
- elif hasattr(assessment.actualFinish, "date"):
488
- assessment_date = assessment.actualFinish.date()
489
- else:
490
- assessment_date = assessment.actualFinish
490
+ if assessment_date := self._get_assessment_date(assessment):
491
+ if assessment_date == today:
492
+ self.cache.set_assessment(impl_id, assessment)
493
+ return assessment
491
494
 
492
- if assessment_date == today:
493
- self.cache.set_assessment(impl_id, assessment)
494
- return assessment
495
- except Exception:
496
- continue
495
+ return None
496
+ except Exception:
497
+ return None
497
498
 
499
+ def _get_assessment_date(self, assessment):
500
+ """Extract date from assessment actualFinish field."""
501
+ if not hasattr(assessment, "actualFinish") or not assessment.actualFinish:
498
502
  return None
503
+
504
+ try:
505
+ if isinstance(assessment.actualFinish, str):
506
+ return regscale_string_to_datetime(assessment.actualFinish).date()
507
+ if hasattr(assessment.actualFinish, "date"):
508
+ return assessment.actualFinish.date()
509
+ return assessment.actualFinish
499
510
  except Exception:
500
511
  return None
501
512
 
@@ -511,8 +522,14 @@ class ControlAssessmentProcessor:
511
522
  result_color = "#d32f2f" if result == "Fail" else "#2e7d32"
512
523
  bg_color = "#ffebee" if result == "Fail" else "#e8f5e8"
513
524
 
514
- html_parts = [
515
- f"""
525
+ header_html = self._create_report_header(control_id, result, result_color, bg_color, len(compliance_items))
526
+ summary_html = self._create_report_summary(compliance_items) if compliance_items else ""
527
+
528
+ return "\n".join([header_html, summary_html])
529
+
530
+ def _create_report_header(self, control_id: str, result: str, result_color: str, bg_color: str, total: int) -> str:
531
+ """Create HTML header section for assessment report."""
532
+ return f"""
516
533
  <div style="margin-bottom: 20px; padding: 15px; border: 2px solid {result_color};
517
534
  border-radius: 5px; background-color: {bg_color};">
518
535
  <h3 style="margin: 0 0 10px 0; color: {result_color};">
@@ -522,34 +539,24 @@ class ControlAssessmentProcessor:
522
539
  <span style="color: {result_color}; font-weight: bold;">{result}</span></p>
523
540
  <p><strong>Assessment Date:</strong> {self.scan_date}</p>
524
541
  <p><strong>Framework:</strong> {self.framework}</p>
525
- <p><strong>Total Policy Assessments:</strong> {len(compliance_items)}</p>
542
+ <p><strong>Total Policy Assessments:</strong> {total}</p>
526
543
  </div>
527
544
  """
528
- ]
529
-
530
- if compliance_items:
531
- pass_count = len(
532
- [
533
- item
534
- for item in compliance_items
535
- if hasattr(item, "compliance_result")
536
- and item.compliance_result in ["PASS", "PASSED", "pass", "passed"]
537
- ]
538
- )
539
- fail_count = len(compliance_items) - pass_count
540
545
 
541
- unique_resources = set()
542
- unique_policies = set()
546
+ def _create_report_summary(self, compliance_items: List[Any]) -> str:
547
+ """Create HTML summary section for assessment report."""
548
+ pass_count = len(
549
+ [
550
+ item
551
+ for item in compliance_items
552
+ if hasattr(item, "compliance_result") and item.compliance_result in ["PASS", "PASSED", "pass", "passed"]
553
+ ]
554
+ )
555
+ fail_count = len(compliance_items) - pass_count
543
556
 
544
- for item in compliance_items:
545
- if hasattr(item, "resource_id"):
546
- unique_resources.add(item.resource_id)
547
- if hasattr(item, "description") and item.description:
548
- policy_desc = item.description[:50] + "..." if len(item.description) > 50 else item.description
549
- unique_policies.add(policy_desc)
557
+ unique_resources, unique_policies = self._extract_unique_items(compliance_items)
550
558
 
551
- html_parts.append(
552
- f"""
559
+ return f"""
553
560
  <div style="margin-top: 20px;">
554
561
  <h4>Assessment Summary</h4>
555
562
  <p><strong>Policy Assessments:</strong> {len(compliance_items)} total</p>
@@ -559,6 +566,17 @@ class ControlAssessmentProcessor:
559
566
  <p><strong>Failing:</strong> <span style="color: #d32f2f;">{fail_count}</span></p>
560
567
  </div>
561
568
  """
562
- )
563
569
 
564
- return "\n".join(html_parts)
570
+ def _extract_unique_items(self, compliance_items: List[Any]):
571
+ """Extract unique resources and policies from compliance items."""
572
+ unique_resources = set()
573
+ unique_policies = set()
574
+
575
+ for item in compliance_items:
576
+ if hasattr(item, "resource_id"):
577
+ unique_resources.add(item.resource_id)
578
+ if hasattr(item, "description") and item.description:
579
+ policy_desc = item.description[:50] + "..." if len(item.description) > 50 else item.description
580
+ unique_policies.add(policy_desc)
581
+
582
+ return unique_resources, unique_policies