regscale-cli 6.25.1.0__py3-none-any.whl → 6.27.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/application.py +19 -4
- regscale/core/app/internal/evidence.py +419 -2
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- 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/sicura/api.py +14 -13
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +49 -39
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- 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 +64 -79
- 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 +161 -165
- 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 +39 -99
- 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 +60 -41
- regscale/integrations/control_matcher.py +377 -0
- regscale/integrations/due_date_handler.py +14 -8
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +1 -0
- regscale/integrations/public/cci_importer.py +37 -38
- regscale/integrations/public/fedramp/click.py +60 -2
- 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/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +277 -153
- regscale/models/integration_models/cisa_kev_data.json +282 -9
- regscale/models/integration_models/nexpose.py +36 -10
- 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/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +47 -22
- regscale/models/regscale_models/issue.py +256 -95
- regscale/models/regscale_models/milestone.py +1 -1
- regscale/models/regscale_models/regscale_model.py +6 -1
- regscale/templates/__init__.py +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3731 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +2204 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +350 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1365 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- 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_compliance_report_normalization.py +138 -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 +343 -0
- 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 +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- 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_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
- 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/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/public/test_fedramp.py +301 -0
- tests/regscale/integrations/test_control_matcher.py +1397 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_issue.py +378 -1
- regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
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
|
|
@@ -15,10 +15,12 @@ from regscale.core.app.utils.app_utils import get_current_datetime
|
|
|
15
15
|
from regscale.integrations.commercial.wizv2.file_cleanup import ReportFileCleanup
|
|
16
16
|
from regscale.integrations.commercial.wizv2.reports import WizReportManager
|
|
17
17
|
from regscale.integrations.commercial.wizv2.variables import WizVariables
|
|
18
|
-
from regscale.integrations.commercial.wizv2.
|
|
18
|
+
from regscale.integrations.commercial.wizv2.core.auth import wiz_authenticate
|
|
19
19
|
from regscale.integrations.compliance_integration import ComplianceIntegration, ComplianceItem
|
|
20
|
+
from regscale.integrations.control_matcher import ControlMatcher
|
|
20
21
|
from regscale.models import regscale_models
|
|
21
|
-
from regscale.models.regscale_models.control_implementation import ControlImplementation
|
|
22
|
+
from regscale.models.regscale_models.control_implementation import ControlImplementation
|
|
23
|
+
from regscale.models.regscale_models.issue import IssueIdentification
|
|
22
24
|
|
|
23
25
|
logger = logging.getLogger("regscale")
|
|
24
26
|
|
|
@@ -135,7 +137,12 @@ class WizComplianceReportItem(ComplianceItem):
|
|
|
135
137
|
def _format_control_id(self, base_control: str, enhancement: str) -> str:
|
|
136
138
|
"""Format control ID with optional enhancement."""
|
|
137
139
|
if enhancement:
|
|
138
|
-
|
|
140
|
+
# Normalize enhancement number to remove leading zeros
|
|
141
|
+
try:
|
|
142
|
+
normalized_enhancement = str(int(enhancement))
|
|
143
|
+
except ValueError:
|
|
144
|
+
normalized_enhancement = enhancement
|
|
145
|
+
return f"{base_control}({normalized_enhancement})"
|
|
139
146
|
else:
|
|
140
147
|
return base_control
|
|
141
148
|
|
|
@@ -277,6 +284,10 @@ class WizComplianceReportProcessor(ComplianceIntegration):
|
|
|
277
284
|
|
|
278
285
|
self.report_manager = WizReportManager(WizVariables.wizUrl, access_token)
|
|
279
286
|
|
|
287
|
+
# Initialize control matcher for robust control ID matching (inherited from parent but ensure it's set)
|
|
288
|
+
if not hasattr(self, "_control_matcher"):
|
|
289
|
+
self._control_matcher = ControlMatcher()
|
|
290
|
+
|
|
280
291
|
def parse_csv_report(self, file_path: str) -> List[WizComplianceReportItem]:
|
|
281
292
|
"""
|
|
282
293
|
Parse CSV compliance report.
|
|
@@ -782,25 +793,26 @@ class WizComplianceReportProcessor(ComplianceIntegration):
|
|
|
782
793
|
:rtype: Optional[str]
|
|
783
794
|
"""
|
|
784
795
|
try:
|
|
785
|
-
# Filter for compliance reports
|
|
786
|
-
filter_by = {"
|
|
796
|
+
# Filter for compliance reports (projectId not supported in ReportFilters, using name-based lookup)
|
|
797
|
+
filter_by = {"type": ["COMPLIANCE_ASSESSMENTS"]}
|
|
787
798
|
|
|
788
799
|
logger.debug(f"Searching for existing compliance reports with filter: {filter_by}")
|
|
789
800
|
reports = self.report_manager.list_reports(filter_by=filter_by)
|
|
790
801
|
|
|
791
802
|
if not reports:
|
|
792
|
-
logger.info("No existing compliance reports found
|
|
803
|
+
logger.info("No existing compliance reports found")
|
|
793
804
|
return None
|
|
794
805
|
|
|
795
|
-
# Look for
|
|
796
|
-
|
|
806
|
+
# Look for report with project-specific name
|
|
807
|
+
expected_name = f"Compliance Report - {self.wiz_project_id}"
|
|
808
|
+
matching_reports = [report for report in reports if report.get("name", "").strip() == expected_name]
|
|
797
809
|
|
|
798
|
-
if not
|
|
799
|
-
logger.info("No compliance
|
|
810
|
+
if not matching_reports:
|
|
811
|
+
logger.info(f"No existing compliance report found with name: {expected_name}")
|
|
800
812
|
return None
|
|
801
813
|
|
|
802
814
|
# Return the first matching report (most recent will be used)
|
|
803
|
-
selected_report =
|
|
815
|
+
selected_report = matching_reports[0]
|
|
804
816
|
report_id = selected_report.get("id")
|
|
805
817
|
report_name = selected_report.get("name", "Unknown")
|
|
806
818
|
|
|
@@ -876,76 +888,66 @@ class WizComplianceReportProcessor(ComplianceIntegration):
|
|
|
876
888
|
"""
|
|
877
889
|
Update passing controls to 'Implemented' status in RegScale.
|
|
878
890
|
|
|
891
|
+
Uses ControlMatcher for robust control ID matching with leading zero normalization.
|
|
892
|
+
|
|
879
893
|
:param list[str] passing_control_ids: List of control IDs that passed
|
|
880
894
|
"""
|
|
881
895
|
if not passing_control_ids:
|
|
882
896
|
return
|
|
883
897
|
|
|
884
|
-
# Initialize Application for control implementation updates
|
|
885
|
-
# app = Application() # Will be used through self.app
|
|
886
|
-
|
|
887
898
|
try:
|
|
888
|
-
# Use the existing method that works for getting control name to implementation ID mapping
|
|
889
|
-
control_impl_map = ControlImplementation.get_control_label_map_by_parent(
|
|
890
|
-
parent_id=self.plan_id, parent_module=self.parent_module
|
|
891
|
-
)
|
|
892
|
-
|
|
893
|
-
logger.debug(
|
|
894
|
-
f"Built control implementation map with {len(control_impl_map)} entries using get_control_label_map_by_parent"
|
|
895
|
-
)
|
|
896
|
-
if control_impl_map:
|
|
897
|
-
sample_keys = list(control_impl_map.keys())[:10]
|
|
898
|
-
logger.debug(f"Sample control names in map: {sample_keys}")
|
|
899
|
-
|
|
900
899
|
logger.debug(f"Looking for passing control IDs: {passing_control_ids}")
|
|
901
900
|
|
|
902
901
|
# Prepare batch updates for passing controls
|
|
903
902
|
implementations_to_update = []
|
|
904
|
-
|
|
905
|
-
# Debug: Show what keys are actually in the control_impl_map
|
|
906
|
-
if control_impl_map:
|
|
907
|
-
logger.debug(f"Control implementation map keys: {list(control_impl_map.keys())[:20]}")
|
|
903
|
+
controls_not_found = []
|
|
908
904
|
|
|
909
905
|
for control_id in passing_control_ids:
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
906
|
+
# Use ControlMatcher to find implementation with robust control ID matching
|
|
907
|
+
impl = self._control_matcher.find_control_implementation(
|
|
908
|
+
control_id=control_id, parent_id=self.plan_id, parent_module=self.parent_module
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
if impl:
|
|
912
|
+
logger.debug(f"Found matching implementation for '{control_id}': {impl.id}")
|
|
913
|
+
|
|
914
|
+
# Update status using compliance settings
|
|
915
|
+
new_status = self._get_implementation_status_from_result("Pass")
|
|
916
|
+
logger.debug(f"Setting control {control_id} status from 'Pass' result to: {new_status}")
|
|
917
|
+
impl.status = new_status
|
|
918
|
+
impl.dateLastAssessed = get_current_datetime()
|
|
919
|
+
impl.lastAssessmentResult = "Pass"
|
|
920
|
+
impl.bStatusImplemented = True
|
|
921
|
+
|
|
922
|
+
# Ensure required fields are set if empty
|
|
923
|
+
if not impl.responsibility:
|
|
924
|
+
impl.responsibility = ControlImplementation.get_default_responsibility(parent_id=impl.parentId)
|
|
925
|
+
logger.debug(f"Setting default responsibility for control {control_id}: {impl.responsibility}")
|
|
926
|
+
|
|
927
|
+
if not impl.implementation:
|
|
928
|
+
impl.implementation = f"Implementation details for {control_id} will be documented."
|
|
929
|
+
logger.debug(f"Setting default implementation statement for control {control_id}")
|
|
930
|
+
|
|
931
|
+
# Set audit fields if available
|
|
932
|
+
user_id = self.app.config.get("userId")
|
|
933
|
+
if user_id:
|
|
934
|
+
impl.lastUpdatedById = user_id
|
|
935
|
+
impl.dateLastUpdated = get_current_datetime()
|
|
936
|
+
|
|
937
|
+
implementations_to_update.append(impl.dict())
|
|
938
|
+
logger.info(f"Marking control {control_id} as {new_status}")
|
|
939
|
+
else:
|
|
940
|
+
logger.debug(f"Control '{control_id}' not found in implementation map")
|
|
941
|
+
controls_not_found.append(control_id)
|
|
942
|
+
|
|
943
|
+
# Log summary
|
|
944
|
+
if controls_not_found:
|
|
945
|
+
logger.info(f"Passing control IDs not found in plan: {', '.join(sorted(controls_not_found))}")
|
|
946
|
+
|
|
947
|
+
logger.info(
|
|
948
|
+
f"Control implementation status update summary: {len(implementations_to_update)} found, "
|
|
949
|
+
f"{len(controls_not_found)} not in plan"
|
|
950
|
+
)
|
|
949
951
|
|
|
950
952
|
# Batch update all implementations
|
|
951
953
|
if implementations_to_update:
|
|
@@ -957,104 +959,47 @@ class WizComplianceReportProcessor(ComplianceIntegration):
|
|
|
957
959
|
except Exception as e:
|
|
958
960
|
logger.error(f"Error updating control implementation status: {e}")
|
|
959
961
|
|
|
960
|
-
def
|
|
961
|
-
"""
|
|
962
|
-
Update control implementation status to In Remediation for failing controls.
|
|
963
|
-
|
|
964
|
-
:param List[str] control_ids: List of control IDs that are failing
|
|
965
|
-
:return: None
|
|
966
|
-
:rtype: None
|
|
962
|
+
def _prepare_failing_control_update(self, control_id: str) -> Optional[dict]:
|
|
967
963
|
"""
|
|
968
|
-
|
|
969
|
-
return
|
|
970
|
-
|
|
971
|
-
try:
|
|
972
|
-
control_impl_map = self._get_control_implementation_map()
|
|
973
|
-
if not control_impl_map:
|
|
974
|
-
return
|
|
975
|
-
|
|
976
|
-
implementations_to_update, controls_not_found = self._process_failing_control_ids(
|
|
977
|
-
control_ids, control_impl_map
|
|
978
|
-
)
|
|
979
|
-
|
|
980
|
-
self._log_update_summary(implementations_to_update, controls_not_found)
|
|
981
|
-
self._batch_update_implementations(implementations_to_update)
|
|
982
|
-
|
|
983
|
-
except Exception as e:
|
|
984
|
-
logger.error(f"Error updating failing control implementation status: {e}")
|
|
985
|
-
|
|
986
|
-
def _get_control_implementation_map(self) -> dict:
|
|
987
|
-
"""Get control implementation map and validate it exists."""
|
|
988
|
-
|
|
989
|
-
control_impl_map = ControlImplementation.get_control_label_map_by_parent(
|
|
990
|
-
parent_id=self.plan_id, parent_module=self.parent_module
|
|
991
|
-
)
|
|
992
|
-
|
|
993
|
-
if not control_impl_map:
|
|
994
|
-
logger.warning("No control implementation mapping found for security plan")
|
|
995
|
-
return {}
|
|
964
|
+
Prepare a single failing control for update.
|
|
996
965
|
|
|
997
|
-
|
|
998
|
-
return
|
|
999
|
-
|
|
1000
|
-
def _process_failing_control_ids(self, control_ids: List[str], control_impl_map: dict) -> tuple[list, list]:
|
|
1001
|
-
"""Process failing control IDs and return implementations to update and controls not found."""
|
|
1002
|
-
|
|
1003
|
-
logger.debug(f"Looking for failing control IDs: {control_ids}")
|
|
1004
|
-
implementations_to_update = []
|
|
1005
|
-
controls_not_found = []
|
|
1006
|
-
|
|
1007
|
-
# Debug: Show what keys are actually in the control_impl_map for comparison
|
|
1008
|
-
if control_impl_map:
|
|
1009
|
-
logger.debug(f"Control implementation map keys (first 20): {list(control_impl_map.keys())[:20]}")
|
|
1010
|
-
|
|
1011
|
-
for control_id in control_ids:
|
|
1012
|
-
control_id_normalized = control_id.lower()
|
|
1013
|
-
logger.debug(f"Looking for control '{control_id_normalized}' in implementation map")
|
|
1014
|
-
|
|
1015
|
-
if control_id_normalized in control_impl_map:
|
|
1016
|
-
impl = self._update_single_control_implementation(
|
|
1017
|
-
control_id, control_id_normalized, control_impl_map[control_id_normalized]
|
|
1018
|
-
)
|
|
1019
|
-
if impl:
|
|
1020
|
-
implementations_to_update.append(impl)
|
|
1021
|
-
else:
|
|
1022
|
-
controls_not_found.append(control_id)
|
|
1023
|
-
else:
|
|
1024
|
-
logger.debug(f"Control '{control_id_normalized}' not found in implementation map")
|
|
1025
|
-
controls_not_found.append(control_id)
|
|
1026
|
-
|
|
1027
|
-
return implementations_to_update, controls_not_found
|
|
1028
|
-
|
|
1029
|
-
def _update_single_control_implementation(
|
|
1030
|
-
self, control_id: str, control_id_normalized: str, impl_id: int
|
|
1031
|
-
) -> Optional[dict]:
|
|
1032
|
-
"""Update a single control implementation to In Remediation status.
|
|
1033
|
-
:param str control_id: ID of the control to update
|
|
1034
|
-
:param str control_id_normalized: ID of the control to update
|
|
1035
|
-
:param int impl_id: ID of the implementation to update
|
|
1036
|
-
:return: Updated implementation status if implementation exists
|
|
966
|
+
:param str control_id: Control ID to update
|
|
967
|
+
:return: Dictionary representation of updated implementation, or None if not found
|
|
1037
968
|
:rtype: Optional[dict]
|
|
1038
969
|
"""
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
logger.debug(f"Found matching implementation for '{control_id_normalized}': {impl_id}")
|
|
970
|
+
impl = self._control_matcher.find_control_implementation(
|
|
971
|
+
control_id=control_id, parent_id=self.plan_id, parent_module=self.parent_module
|
|
972
|
+
)
|
|
1043
973
|
|
|
1044
|
-
impl = ControlImplementation.get_object(object_id=impl_id)
|
|
1045
974
|
if not impl:
|
|
1046
|
-
logger.
|
|
975
|
+
logger.debug(f"Control '{control_id}' not found in implementation map")
|
|
1047
976
|
return None
|
|
1048
977
|
|
|
1049
|
-
|
|
978
|
+
logger.debug(f"Found matching implementation for '{control_id}': {impl.id}")
|
|
979
|
+
|
|
1050
980
|
new_status = self._get_implementation_status_from_result("Fail")
|
|
1051
981
|
logger.debug(f"Setting control {control_id} status from 'Fail' result to: {new_status}")
|
|
982
|
+
|
|
1052
983
|
impl.status = new_status
|
|
1053
984
|
impl.dateLastAssessed = get_current_datetime()
|
|
1054
985
|
impl.lastAssessmentResult = "Fail"
|
|
1055
986
|
impl.bStatusImplemented = False
|
|
1056
987
|
|
|
1057
|
-
|
|
988
|
+
self._set_default_fields_if_empty(impl, control_id)
|
|
989
|
+
self._set_audit_fields(impl)
|
|
990
|
+
|
|
991
|
+
logger.info(f"Marking control {control_id} as {new_status}")
|
|
992
|
+
return impl.dict()
|
|
993
|
+
|
|
994
|
+
def _set_default_fields_if_empty(self, impl: ControlImplementation, control_id: str) -> None:
|
|
995
|
+
"""
|
|
996
|
+
Set default values for required fields if they are empty.
|
|
997
|
+
|
|
998
|
+
:param ControlImplementation impl: Implementation to update
|
|
999
|
+
:param str control_id: Control ID for logging
|
|
1000
|
+
:return: None
|
|
1001
|
+
:rtype: None
|
|
1002
|
+
"""
|
|
1058
1003
|
if not impl.responsibility:
|
|
1059
1004
|
impl.responsibility = ControlImplementation.get_default_responsibility(parent_id=impl.parentId)
|
|
1060
1005
|
logger.debug(f"Setting default responsibility for control {control_id}: {impl.responsibility}")
|
|
@@ -1063,27 +1008,76 @@ class WizComplianceReportProcessor(ComplianceIntegration):
|
|
|
1063
1008
|
impl.implementation = f"Implementation details for {control_id} will be documented."
|
|
1064
1009
|
logger.debug(f"Setting default implementation statement for control {control_id}")
|
|
1065
1010
|
|
|
1066
|
-
|
|
1011
|
+
def _set_audit_fields(self, impl: ControlImplementation) -> None:
|
|
1012
|
+
"""
|
|
1013
|
+
Set audit fields on implementation if user ID is available.
|
|
1014
|
+
|
|
1015
|
+
:param ControlImplementation impl: Implementation to update
|
|
1016
|
+
:return: None
|
|
1017
|
+
:rtype: None
|
|
1018
|
+
"""
|
|
1067
1019
|
user_id = self.app.config.get("userId")
|
|
1068
1020
|
if user_id:
|
|
1069
1021
|
impl.lastUpdatedById = user_id
|
|
1070
1022
|
impl.dateLastUpdated = get_current_datetime()
|
|
1071
1023
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1024
|
+
def _update_failing_controls_to_in_remediation(self, control_ids: List[str]) -> None:
|
|
1025
|
+
"""
|
|
1026
|
+
Update control implementation status to In Remediation for failing controls.
|
|
1027
|
+
|
|
1028
|
+
Uses ControlMatcher for robust control ID matching with leading zero normalization.
|
|
1029
|
+
|
|
1030
|
+
:param List[str] control_ids: List of control IDs that are failing
|
|
1031
|
+
:return: None
|
|
1032
|
+
:rtype: None
|
|
1033
|
+
"""
|
|
1034
|
+
if not control_ids:
|
|
1035
|
+
return
|
|
1074
1036
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1037
|
+
try:
|
|
1038
|
+
logger.debug(f"Looking for failing control IDs: {control_ids}")
|
|
1039
|
+
|
|
1040
|
+
implementations_to_update = []
|
|
1041
|
+
controls_not_found = []
|
|
1042
|
+
|
|
1043
|
+
for control_id in control_ids:
|
|
1044
|
+
impl_dict = self._prepare_failing_control_update(control_id)
|
|
1045
|
+
if impl_dict:
|
|
1046
|
+
implementations_to_update.append(impl_dict)
|
|
1047
|
+
else:
|
|
1048
|
+
controls_not_found.append(control_id)
|
|
1049
|
+
|
|
1050
|
+
self._log_update_summary(controls_not_found, implementations_to_update)
|
|
1051
|
+
self._batch_update_implementations(implementations_to_update)
|
|
1052
|
+
|
|
1053
|
+
except Exception as e:
|
|
1054
|
+
logger.error(f"Error updating failing control implementation status: {e}")
|
|
1055
|
+
|
|
1056
|
+
def _log_update_summary(self, controls_not_found: List[str], implementations_to_update: List[dict]) -> None:
|
|
1057
|
+
"""
|
|
1058
|
+
Log summary of control update operation.
|
|
1059
|
+
|
|
1060
|
+
:param List[str] controls_not_found: List of controls not found
|
|
1061
|
+
:param List[dict] implementations_to_update: List of implementations to update
|
|
1062
|
+
:return: None
|
|
1063
|
+
:rtype: None
|
|
1064
|
+
"""
|
|
1077
1065
|
if controls_not_found:
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1066
|
+
logger.info(f"Control IDs not found in plan: {', '.join(sorted(controls_not_found))}")
|
|
1067
|
+
|
|
1068
|
+
logger.info(
|
|
1069
|
+
f"Control implementation status update summary: {len(implementations_to_update)} found, "
|
|
1070
|
+
f"{len(controls_not_found)} not in plan"
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
def _batch_update_implementations(self, implementations_to_update: List[dict]) -> None:
|
|
1074
|
+
"""
|
|
1075
|
+
Perform batch update of control implementations.
|
|
1084
1076
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1077
|
+
:param List[dict] implementations_to_update: List of implementations to update
|
|
1078
|
+
:return: None
|
|
1079
|
+
:rtype: None
|
|
1080
|
+
"""
|
|
1087
1081
|
if implementations_to_update:
|
|
1088
1082
|
ControlImplementation.put_batch_implementation(self.app, implementations_to_update)
|
|
1089
1083
|
logger.debug(f"Updated {len(implementations_to_update)} Control Implementations, Successfully!")
|
|
@@ -1551,6 +1545,7 @@ class WizComplianceReportProcessor(ComplianceIntegration):
|
|
|
1551
1545
|
rule_id=control_id,
|
|
1552
1546
|
baseline=representative_item.framework,
|
|
1553
1547
|
affected_controls=control_id,
|
|
1548
|
+
identification=IssueIdentification.SecurityControlAssessment.value,
|
|
1554
1549
|
)
|
|
1555
1550
|
|
|
1556
1551
|
def _create_finding_from_compliance_item(self, compliance_item: ComplianceItem) -> Optional[Any]:
|
|
@@ -1587,6 +1582,7 @@ class WizComplianceReportProcessor(ComplianceIntegration):
|
|
|
1587
1582
|
rule_id=compliance_item.control_id,
|
|
1588
1583
|
baseline=compliance_item.framework,
|
|
1589
1584
|
affected_controls=compliance_item.affected_controls, # Use our property with all control IDs
|
|
1585
|
+
identification=IssueIdentification.SecurityControlAssessment.value,
|
|
1590
1586
|
)
|
|
1591
1587
|
|
|
1592
1588
|
return finding
|