regscale-cli 6.26.0.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.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +1 -1
- regscale/core/app/internal/evidence.py +419 -2
- regscale/dev/code_gen.py +24 -20
- regscale/integrations/commercial/jira.py +367 -126
- regscale/integrations/commercial/qualys/__init__.py +7 -8
- regscale/integrations/commercial/qualys/scanner.py +8 -3
- regscale/integrations/commercial/synqly/assets.py +17 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
- regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
- regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
- regscale/integrations/commercial/tenablev2/commands.py +142 -1
- regscale/integrations/commercial/tenablev2/scanner.py +0 -1
- regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
- regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +44 -59
- regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
- regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
- regscale/integrations/commercial/wizv2/compliance_report.py +10 -9
- regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
- regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
- regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
- regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
- regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
- regscale/integrations/commercial/wizv2/issue.py +1 -1
- regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
- regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
- regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
- regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
- regscale/integrations/commercial/wizv2/reports.py +1 -1
- regscale/integrations/commercial/wizv2/sbom.py +1 -1
- regscale/integrations/commercial/wizv2/scanner.py +40 -100
- regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
- regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
- regscale/integrations/commercial/wizv2/variables.py +89 -3
- regscale/integrations/compliance_integration.py +0 -46
- regscale/integrations/control_matcher.py +22 -3
- regscale/integrations/due_date_handler.py +14 -8
- regscale/integrations/public/fedramp/docx_parser.py +10 -1
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
- regscale/integrations/public/fedramp/fedramp_five.py +1 -1
- regscale/integrations/scanner_integration.py +127 -57
- regscale/models/integration_models/cisa_kev_data.json +132 -9
- regscale/models/integration_models/qualys.py +3 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
- regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
- regscale/models/regscale_models/control_implementation.py +1 -1
- regscale/models/regscale_models/issue.py +0 -1
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +93 -60
- tests/regscale/integrations/commercial/test_jira.py +481 -91
- tests/regscale/integrations/commercial/test_wiz.py +96 -200
- tests/regscale/integrations/commercial/wizv2/__init__.py +1 -1
- tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
- tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
- tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
- tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
- tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
- tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
- tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
- tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
- tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
- tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +1 -1
- tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
- tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
- tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
- tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +1 -1
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +72 -29
- tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +946 -78
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +97 -202
- tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
- tests/regscale/integrations/public/test_fedramp.py +301 -0
- tests/regscale/integrations/test_control_matcher.py +83 -0
- regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
- tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +0 -750
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.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(
|
|
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
|
-
|
|
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
|
-
|
|
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*(
|
|
49
|
-
vuln_discuss = _extract_field(r"VulnDiscussion='(
|
|
50
|
-
result = _extract_field(r"Result:\s*(
|
|
51
|
-
solution
|
|
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
|
-
|
|
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",
|
|
59
|
-
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
304
|
-
|
|
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.
|
|
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,
|
|
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=
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
436
|
-
|
|
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(
|
|
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
|
-
#
|
|
453
|
-
|
|
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
|
|
466
|
-
|
|
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
|
|
569
|
+
# Use environment variables if not provided or empty
|
|
570
|
+
if not client_secret:
|
|
586
571
|
client_secret = WizVariables.wizClientSecret
|
|
587
|
-
if client_id
|
|
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
|
+
]
|
regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py}
RENAMED
|
@@ -266,32 +266,39 @@ class IssueFieldSetter:
|
|
|
266
266
|
)
|
|
267
267
|
|
|
268
268
|
for impl in implementations:
|
|
269
|
-
if
|
|
270
|
-
|
|
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
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
515
|
-
|
|
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> {
|
|
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
|
-
|
|
542
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|