regscale-cli 6.21.2.0__py3-none-any.whl → 6.28.2.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.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/api.py +5 -2
- regscale/core/app/application.py +36 -6
- regscale/core/app/internal/control_editor.py +73 -21
- regscale/core/app/internal/evidence.py +727 -204
- regscale/core/app/internal/login.py +4 -2
- regscale/core/app/internal/model_editor.py +219 -64
- regscale/core/app/utils/app_utils.py +86 -12
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/core/login.py +21 -4
- regscale/core/utils/async_graphql_client.py +363 -0
- regscale/core/utils/date.py +77 -1
- regscale/dev/cli.py +26 -0
- regscale/dev/code_gen.py +109 -24
- regscale/dev/version.py +72 -0
- regscale/integrations/commercial/__init__.py +30 -2
- regscale/integrations/commercial/aws/audit_manager_compliance.py +3908 -0
- regscale/integrations/commercial/aws/cli.py +3107 -54
- regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
- regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
- regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
- regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
- regscale/integrations/commercial/{amazon → aws}/common.py +71 -19
- regscale/integrations/commercial/aws/config_compliance.py +914 -0
- regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
- regscale/integrations/commercial/aws/control_compliance_analyzer.py +439 -0
- regscale/integrations/commercial/aws/evidence_generator.py +283 -0
- regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
- regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
- regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
- regscale/integrations/commercial/aws/iam_evidence.py +574 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +338 -22
- regscale/integrations/commercial/aws/inventory/base.py +107 -5
- regscale/integrations/commercial/aws/inventory/resources/analytics.py +390 -0
- regscale/integrations/commercial/aws/inventory/resources/applications.py +234 -0
- regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +328 -9
- regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
- regscale/integrations/commercial/aws/inventory/resources/database.py +481 -31
- regscale/integrations/commercial/aws/inventory/resources/developer_tools.py +253 -0
- regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
- regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
- regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
- regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
- regscale/integrations/commercial/aws/inventory/resources/machine_learning.py +358 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +390 -67
- regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
- regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +288 -29
- regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
- regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
- regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
- regscale/integrations/commercial/aws/kms_evidence.py +879 -0
- regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
- regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
- regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
- regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
- regscale/integrations/commercial/aws/org_evidence.py +666 -0
- regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
- regscale/integrations/commercial/aws/s3_evidence.py +632 -0
- regscale/integrations/commercial/aws/scanner.py +1072 -205
- regscale/integrations/commercial/aws/security_hub.py +319 -0
- regscale/integrations/commercial/aws/session_manager.py +282 -0
- regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
- regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
- regscale/integrations/commercial/jira.py +489 -153
- regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
- regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
- regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
- regscale/integrations/commercial/qualys/__init__.py +167 -68
- regscale/integrations/commercial/qualys/scanner.py +305 -39
- regscale/integrations/commercial/sarif/sairf_importer.py +432 -0
- regscale/integrations/commercial/sarif/sarif_converter.py +67 -0
- regscale/integrations/commercial/sicura/api.py +79 -42
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +83 -44
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/synqly/assets.py +133 -16
- regscale/integrations/commercial/synqly/edr.py +2 -8
- regscale/integrations/commercial/synqly/query_builder.py +536 -0
- regscale/integrations/commercial/synqly/ticketing.py +27 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +165 -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 +146 -5
- regscale/integrations/commercial/tenablev2/scanner.py +1 -3
- regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
- regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +191 -76
- 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 +1592 -0
- regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
- regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +7 -3
- regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +92 -89
- 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} +66 -9
- regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
- regscale/integrations/commercial/wizv2/issue.py +776 -28
- 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 +243 -0
- regscale/integrations/commercial/wizv2/sbom.py +1 -1
- regscale/integrations/commercial/wizv2/scanner.py +1031 -441
- 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 +1036 -151
- regscale/integrations/control_matcher.py +432 -0
- regscale/integrations/due_date_handler.py +333 -0
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +14 -0
- regscale/integrations/public/cci_importer.py +834 -0
- regscale/integrations/public/csam/__init__.py +0 -0
- regscale/integrations/public/csam/csam.py +938 -0
- regscale/integrations/public/csam/csam_agency_defined.py +179 -0
- regscale/integrations/public/csam/csam_common.py +154 -0
- regscale/integrations/public/csam/csam_controls.py +432 -0
- regscale/integrations/public/csam/csam_poam.py +124 -0
- regscale/integrations/public/fedramp/click.py +77 -6
- regscale/integrations/public/fedramp/docx_parser.py +10 -1
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +675 -289
- regscale/integrations/public/fedramp/fedramp_five.py +1 -1
- regscale/integrations/public/fedramp/poam/scanner.py +75 -7
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +1961 -430
- regscale/models/integration_models/CCI_List.xml +1 -0
- regscale/models/integration_models/aqua.py +2 -2
- regscale/models/integration_models/cisa_kev_data.json +805 -11
- regscale/models/integration_models/flat_file_importer/__init__.py +5 -8
- 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 +87 -18
- regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +124 -25
- regscale/models/integration_models/synqly_models/synqly_model.py +89 -16
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +4 -2
- regscale/models/regscale_models/__init__.py +7 -0
- regscale/models/regscale_models/assessment.py +2 -1
- regscale/models/regscale_models/catalog.py +1 -1
- regscale/models/regscale_models/compliance_settings.py +251 -1
- regscale/models/regscale_models/component.py +1 -0
- regscale/models/regscale_models/control_implementation.py +236 -41
- regscale/models/regscale_models/control_objective.py +74 -5
- regscale/models/regscale_models/file.py +2 -0
- regscale/models/regscale_models/form_field_value.py +5 -3
- regscale/models/regscale_models/inheritance.py +44 -0
- regscale/models/regscale_models/issue.py +301 -102
- regscale/models/regscale_models/milestone.py +33 -14
- regscale/models/regscale_models/organization.py +3 -0
- regscale/models/regscale_models/regscale_model.py +310 -73
- regscale/models/regscale_models/security_plan.py +4 -2
- regscale/models/regscale_models/vulnerability.py +3 -3
- regscale/regscale.py +25 -4
- regscale/templates/__init__.py +0 -0
- regscale/utils/threading/threadhandler.py +20 -15
- regscale/validation/record.py +23 -1
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/METADATA +17 -33
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/RECORD +310 -111
- tests/core/__init__.py +0 -0
- tests/core/utils/__init__.py +0 -0
- tests/core/utils/test_async_graphql_client.py +472 -0
- tests/fixtures/test_fixture.py +13 -8
- tests/regscale/core/test_login.py +171 -4
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
- tests/regscale/integrations/commercial/aws/test_aws_analytics_collector.py +260 -0
- tests/regscale/integrations/commercial/aws/test_aws_applications_collector.py +242 -0
- tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
- tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
- tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
- tests/regscale/integrations/commercial/aws/test_aws_developer_tools_collector.py +203 -0
- tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
- tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
- tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
- tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
- tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
- tests/regscale/integrations/commercial/aws/test_aws_machine_learning_collector.py +237 -0
- tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
- tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
- tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
- tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
- tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
- tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
- tests/regscale/integrations/commercial/aws/test_control_compliance_analyzer.py +375 -0
- tests/regscale/integrations/commercial/aws/test_datetime_parsing.py +223 -0
- tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
- tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
- tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
- tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
- tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
- tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
- tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
- tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
- tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
- tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
- tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
- tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
- tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
- tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
- tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -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 +3742 -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 +349 -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 +1218 -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/__init__.py +0 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_gen_asset_list.py +150 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/public/test_alienvault.py +220 -0
- tests/regscale/integrations/public/test_cci.py +1053 -0
- tests/regscale/integrations/public/test_cisa.py +1021 -0
- tests/regscale/integrations/public/test_emass.py +518 -0
- tests/regscale/integrations/public/test_fedramp.py +1152 -0
- tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
- tests/regscale/integrations/public/test_file_uploads.py +506 -0
- tests/regscale/integrations/public/test_oscal.py +453 -0
- tests/regscale/integrations/test_compliance_status_mapping.py +406 -0
- tests/regscale/integrations/test_control_matcher.py +1421 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_control_implementation.py +118 -3
- tests/regscale/models/test_form_field_value_integration.py +304 -0
- tests/regscale/models/test_issue.py +378 -1
- tests/regscale/models/test_module_integration.py +582 -0
- tests/regscale/models/test_tenable_integrations.py +811 -105
- regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3057
- regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +0 -7388
- regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +0 -9605
- regscale/integrations/public/fedramp/parts_mapper.py +0 -107
- /regscale/integrations/commercial/{amazon → sarif}/__init__.py +0 -0
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/LICENSE +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/WHEEL +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1592 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Wiz Compliance Report Integration for RegScale CLI."""
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import gzip
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
from typing import Dict, Any, Optional, List
|
|
12
|
+
|
|
13
|
+
from regscale.core.app.utils.app_utils import error_and_exit
|
|
14
|
+
from regscale.core.app.utils.app_utils import get_current_datetime
|
|
15
|
+
from regscale.integrations.commercial.wizv2.file_cleanup import ReportFileCleanup
|
|
16
|
+
from regscale.integrations.commercial.wizv2.reports import WizReportManager
|
|
17
|
+
from regscale.integrations.commercial.wizv2.variables import WizVariables
|
|
18
|
+
from regscale.integrations.commercial.wizv2.core.auth import wiz_authenticate
|
|
19
|
+
from regscale.integrations.compliance_integration import ComplianceIntegration, ComplianceItem
|
|
20
|
+
from regscale.integrations.control_matcher import ControlMatcher
|
|
21
|
+
from regscale.models import regscale_models
|
|
22
|
+
from regscale.models.regscale_models.control_implementation import ControlImplementation
|
|
23
|
+
from regscale.models.regscale_models.issue import IssueIdentification
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("regscale")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WizComplianceReportItem(ComplianceItem):
|
|
29
|
+
"""Compliance item parsed from Wiz CSV report."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, csv_row: Dict[str, str]):
|
|
32
|
+
"""
|
|
33
|
+
Initialize from CSV row data.
|
|
34
|
+
|
|
35
|
+
:param Dict[str, str] csv_row: Row data from CSV report
|
|
36
|
+
"""
|
|
37
|
+
self.csv_data = csv_row
|
|
38
|
+
self._resource_name = csv_row.get("Resource Name", "") # Use _resource_name to avoid conflict with property
|
|
39
|
+
self.cloud_provider = csv_row.get("Cloud Provider", "")
|
|
40
|
+
self.cloud_provider_id = csv_row.get("Cloud Provider ID", "")
|
|
41
|
+
self._resource_id = csv_row.get("Resource ID", "") # Use _resource_id to avoid conflict with property
|
|
42
|
+
self.resource_region = csv_row.get("Resource Region", "")
|
|
43
|
+
self.subscription = csv_row.get("Subscription", "")
|
|
44
|
+
self.subscription_name = csv_row.get("Subscription Name", "")
|
|
45
|
+
self.policy_name = csv_row.get("Policy Name", "")
|
|
46
|
+
self.policy_id = csv_row.get("Policy ID", "")
|
|
47
|
+
self.result = csv_row.get("Result", "")
|
|
48
|
+
self._severity = csv_row.get("Severity", "") # Use _severity to avoid conflict with property
|
|
49
|
+
self.compliance_check_name = csv_row.get("Compliance Check Name (Wiz Subcategory)", "")
|
|
50
|
+
self._framework = csv_row.get("Framework", "") # Use _framework to avoid conflict with property
|
|
51
|
+
self.remediation_steps = csv_row.get("Remediation Steps", "")
|
|
52
|
+
|
|
53
|
+
# ComplianceItem abstract property implementations
|
|
54
|
+
@property
|
|
55
|
+
def resource_id(self) -> str:
|
|
56
|
+
"""Unique identifier for the resource being assessed."""
|
|
57
|
+
return self.cloud_provider_id or self._resource_id or self._resource_name or "Unknown"
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def resource_name(self) -> str:
|
|
61
|
+
"""Human-readable name of the resource."""
|
|
62
|
+
return self.get_unique_resource_name()
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def control_id(self) -> str:
|
|
66
|
+
"""Control identifier (e.g., AC-3, SI-2)."""
|
|
67
|
+
return self.get_control_id()
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def compliance_result(self) -> str:
|
|
71
|
+
"""Result of compliance check (PASS, FAIL, etc)."""
|
|
72
|
+
return self.result
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def severity(self) -> Optional[str]:
|
|
76
|
+
"""Severity level of the compliance violation (if failed)."""
|
|
77
|
+
return self._severity if self._severity else None
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def description(self) -> str:
|
|
81
|
+
"""Description of the compliance check."""
|
|
82
|
+
return self.get_finding_details()
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def framework(self) -> str:
|
|
86
|
+
"""Compliance framework (e.g., NIST800-53R5, CSF)."""
|
|
87
|
+
if not self._framework:
|
|
88
|
+
return "NIST800-53R5"
|
|
89
|
+
|
|
90
|
+
# Normalize Wiz framework names to RegScale format
|
|
91
|
+
framework_mappings = {
|
|
92
|
+
"NIST SP 800-53 Revision 5": "NIST800-53R5",
|
|
93
|
+
"NIST SP 800-53 Rev 5": "NIST800-53R5",
|
|
94
|
+
"NIST SP 800-53 R5": "NIST800-53R5",
|
|
95
|
+
"NIST 800-53 Revision 5": "NIST800-53R5",
|
|
96
|
+
"NIST 800-53 Rev 5": "NIST800-53R5",
|
|
97
|
+
"NIST 800-53 R5": "NIST800-53R5",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return framework_mappings.get(self._framework, self._framework)
|
|
101
|
+
|
|
102
|
+
def get_control_id(self) -> str:
|
|
103
|
+
"""Extract first control ID from compliance check name for compatibility."""
|
|
104
|
+
control_ids = self.get_all_control_ids()
|
|
105
|
+
return control_ids[0] if control_ids else ""
|
|
106
|
+
|
|
107
|
+
def get_all_control_ids(self) -> list:
|
|
108
|
+
"""Extract all control IDs from compliance check name and normalize leading zeros."""
|
|
109
|
+
if not self.compliance_check_name:
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
control_id_pattern = r"([A-Za-z]{2}-\d+)(?:\s*\(\s*(\d+)\s*\))?"
|
|
113
|
+
control_ids = []
|
|
114
|
+
|
|
115
|
+
for part in self.compliance_check_name.split(", "):
|
|
116
|
+
matches = re.findall(control_id_pattern, part.strip())
|
|
117
|
+
for match in matches:
|
|
118
|
+
base_control, enhancement = match
|
|
119
|
+
normalized_control = self._normalize_base_control(base_control)
|
|
120
|
+
formatted_control = self._format_control_id(normalized_control, enhancement)
|
|
121
|
+
control_ids.append(formatted_control)
|
|
122
|
+
|
|
123
|
+
return control_ids
|
|
124
|
+
|
|
125
|
+
def _normalize_base_control(self, base_control: str) -> str:
|
|
126
|
+
"""Normalize leading zeros in base control number (e.g., AC-01 -> AC-1)."""
|
|
127
|
+
if "-" in base_control:
|
|
128
|
+
prefix, number = base_control.split("-", 1)
|
|
129
|
+
try:
|
|
130
|
+
normalized_number = str(int(number))
|
|
131
|
+
return f"{prefix.upper()}-{normalized_number}"
|
|
132
|
+
except ValueError:
|
|
133
|
+
return base_control.upper()
|
|
134
|
+
else:
|
|
135
|
+
return base_control.upper()
|
|
136
|
+
|
|
137
|
+
def _format_control_id(self, base_control: str, enhancement: str) -> str:
|
|
138
|
+
"""Format control ID with optional enhancement."""
|
|
139
|
+
if enhancement:
|
|
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})"
|
|
146
|
+
else:
|
|
147
|
+
return base_control
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def affected_controls(self) -> str:
|
|
151
|
+
"""Get affected controls as comma-separated string for issues."""
|
|
152
|
+
control_ids = self.get_all_control_ids()
|
|
153
|
+
return ",".join(control_ids) if control_ids else self.control_id
|
|
154
|
+
|
|
155
|
+
def get_status(self) -> str:
|
|
156
|
+
"""Get compliance status based on result."""
|
|
157
|
+
return "Satisfied" if self.result.lower() == "pass" else "Other Than Satisfied"
|
|
158
|
+
|
|
159
|
+
def get_implementation_status(self) -> str:
|
|
160
|
+
"""Get implementation status based on result."""
|
|
161
|
+
return "Implemented" if self.result.lower() == "pass" else "In Remediation"
|
|
162
|
+
|
|
163
|
+
def get_severity(self) -> str:
|
|
164
|
+
"""Map Wiz severity to RegScale severity."""
|
|
165
|
+
severity_map = {"CRITICAL": "High", "HIGH": "High", "MEDIUM": "Moderate", "LOW": "Low", "INFORMATIONAL": "Low"}
|
|
166
|
+
return severity_map.get(self._severity.upper(), "Low")
|
|
167
|
+
|
|
168
|
+
def get_unique_resource_name(self) -> str:
|
|
169
|
+
"""Get a unique resource name by appending provider ID or resource ID."""
|
|
170
|
+
base_name = self._resource_name
|
|
171
|
+
if not base_name:
|
|
172
|
+
base_name = "Unknown Resource"
|
|
173
|
+
|
|
174
|
+
# Add region if available
|
|
175
|
+
if self.resource_region:
|
|
176
|
+
base_name = f"{base_name} ({self.resource_region})"
|
|
177
|
+
|
|
178
|
+
# Add unique identifier (prefer resource_id over cloud_provider_id)
|
|
179
|
+
unique_id = self._resource_id or self.cloud_provider_id
|
|
180
|
+
if unique_id:
|
|
181
|
+
# Extract just the last part of Azure resource IDs for brevity
|
|
182
|
+
if "/" in unique_id:
|
|
183
|
+
unique_suffix = unique_id.split("/")[-1]
|
|
184
|
+
else:
|
|
185
|
+
unique_suffix = unique_id
|
|
186
|
+
|
|
187
|
+
# Only append if not already in the name
|
|
188
|
+
if unique_suffix.lower() not in base_name.lower():
|
|
189
|
+
base_name = f"{base_name} [{unique_suffix[:12]}]" # Limit to 12 chars
|
|
190
|
+
|
|
191
|
+
return base_name
|
|
192
|
+
|
|
193
|
+
def get_unique_issue_identifier(self) -> str:
|
|
194
|
+
"""Get a unique identifier for deduplication of issues."""
|
|
195
|
+
# Use resource_id + policy_id + control_id for uniqueness
|
|
196
|
+
resource_key = self._resource_id or self.cloud_provider_id or self._resource_name
|
|
197
|
+
policy_key = self.policy_id or self.policy_name
|
|
198
|
+
control_key = self.get_control_id()
|
|
199
|
+
return f"{resource_key}|{policy_key}|{control_key}"
|
|
200
|
+
|
|
201
|
+
def get_title(self) -> str:
|
|
202
|
+
"""Get assessment title."""
|
|
203
|
+
return f"{self.get_control_id()} - {self.policy_name}"
|
|
204
|
+
|
|
205
|
+
def get_description(self) -> str:
|
|
206
|
+
"""Get assessment description."""
|
|
207
|
+
return f"Wiz compliance assessment for {self.get_unique_resource_name()} - {self.policy_name}"
|
|
208
|
+
|
|
209
|
+
def get_finding_details(self) -> str:
|
|
210
|
+
"""Get finding details for issues."""
|
|
211
|
+
details = f"Resource: {self.get_unique_resource_name()}\n"
|
|
212
|
+
details += f"Cloud Provider: {self.cloud_provider}\n"
|
|
213
|
+
if self.subscription_name:
|
|
214
|
+
details += f"Subscription: {self.subscription_name}\n"
|
|
215
|
+
details += f"Result: {self.result}\n"
|
|
216
|
+
details += f"Remediation: {self.remediation_steps}"
|
|
217
|
+
return details
|
|
218
|
+
|
|
219
|
+
def get_asset_identifier(self) -> str:
|
|
220
|
+
"""Get asset identifier using cloud provider ID for issues."""
|
|
221
|
+
return self.cloud_provider_id or self._resource_id or self._resource_name or "Unknown"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class WizComplianceReportProcessor(ComplianceIntegration):
|
|
225
|
+
"""Process compliance reports from Wiz and create assessments in RegScale."""
|
|
226
|
+
|
|
227
|
+
# Set the asset identifier field to match Wiz integration standard
|
|
228
|
+
asset_identifier_field: str = "wizId"
|
|
229
|
+
|
|
230
|
+
def __init__(
|
|
231
|
+
self,
|
|
232
|
+
plan_id: int,
|
|
233
|
+
wiz_project_id: str,
|
|
234
|
+
client_id: str,
|
|
235
|
+
client_secret: str,
|
|
236
|
+
regscale_module: str = "securityplans",
|
|
237
|
+
create_poams: bool = False,
|
|
238
|
+
report_file_path: Optional[str] = None,
|
|
239
|
+
bypass_control_filtering: bool = False,
|
|
240
|
+
max_report_age_days: int = 7,
|
|
241
|
+
force_fresh_report: bool = False,
|
|
242
|
+
reuse_existing_reports: bool = True,
|
|
243
|
+
**kwargs,
|
|
244
|
+
):
|
|
245
|
+
"""
|
|
246
|
+
Initialize the compliance report processor.
|
|
247
|
+
|
|
248
|
+
:param int plan_id: RegScale plan/SSP ID
|
|
249
|
+
:param str wiz_project_id: Wiz project ID
|
|
250
|
+
:param str client_id: Wiz client ID
|
|
251
|
+
:param str client_secret: Wiz client secret
|
|
252
|
+
:param str regscale_module: RegScale module to use
|
|
253
|
+
:param bool create_poams: Whether to create POAMs for failed assessments
|
|
254
|
+
:param Optional[str] report_file_path: Path to existing report file to use instead of creating new one
|
|
255
|
+
:param bool bypass_control_filtering: Skip control filtering for performance with large control sets
|
|
256
|
+
:param int max_report_age_days: Maximum age in days for reusing existing reports (default: 7 days)
|
|
257
|
+
:param bool force_fresh_report: Force creation of fresh report, ignoring existing reports
|
|
258
|
+
:param bool reuse_existing_reports: Whether to reuse existing Wiz reports instead of creating new ones (default: True)
|
|
259
|
+
"""
|
|
260
|
+
# Call parent constructor with ComplianceIntegration parameters
|
|
261
|
+
super().__init__(
|
|
262
|
+
plan_id=plan_id,
|
|
263
|
+
framework="NIST800-53R5",
|
|
264
|
+
create_poams=create_poams,
|
|
265
|
+
parent_module=regscale_module,
|
|
266
|
+
**kwargs,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Wiz-specific attributes
|
|
270
|
+
self.wiz_project_id = wiz_project_id
|
|
271
|
+
self.client_id = client_id
|
|
272
|
+
self.client_secret = client_secret
|
|
273
|
+
self.report_file_path = report_file_path
|
|
274
|
+
self.bypass_control_filtering = bypass_control_filtering
|
|
275
|
+
self.max_report_age_days = max_report_age_days
|
|
276
|
+
self.force_fresh_report = force_fresh_report
|
|
277
|
+
self.reuse_existing_reports = reuse_existing_reports
|
|
278
|
+
self.title = "Wiz Compliance" # Required by ScannerIntegration
|
|
279
|
+
|
|
280
|
+
# Initialize Wiz authentication
|
|
281
|
+
access_token = wiz_authenticate(client_id, client_secret)
|
|
282
|
+
if not access_token:
|
|
283
|
+
error_and_exit("Failed to authenticate with Wiz")
|
|
284
|
+
|
|
285
|
+
self.report_manager = WizReportManager(WizVariables.wizUrl, access_token)
|
|
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
|
+
|
|
291
|
+
def parse_csv_report(self, file_path: str) -> List[WizComplianceReportItem]:
|
|
292
|
+
"""
|
|
293
|
+
Parse CSV compliance report.
|
|
294
|
+
|
|
295
|
+
:param str file_path: Path to CSV report file
|
|
296
|
+
:return: List of compliance items
|
|
297
|
+
:rtype: List[WizComplianceReportItem]
|
|
298
|
+
"""
|
|
299
|
+
items = []
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
# Handle gzipped files
|
|
303
|
+
if file_path.endswith(".gz"):
|
|
304
|
+
with gzip.open(file_path, "rt", encoding="utf-8") as f:
|
|
305
|
+
reader = csv.DictReader(f)
|
|
306
|
+
for row in reader:
|
|
307
|
+
items.append(WizComplianceReportItem(row))
|
|
308
|
+
else:
|
|
309
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
310
|
+
reader = csv.DictReader(f)
|
|
311
|
+
for row in reader:
|
|
312
|
+
items.append(WizComplianceReportItem(row))
|
|
313
|
+
|
|
314
|
+
logger.info(f"Parsed {len(items)} compliance items from report")
|
|
315
|
+
return items
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logger.error(f"Error parsing CSV report: {e}")
|
|
319
|
+
return []
|
|
320
|
+
|
|
321
|
+
# ComplianceIntegration abstract method implementations
|
|
322
|
+
def fetch_compliance_data(self) -> List[Dict[str, str]]:
|
|
323
|
+
"""
|
|
324
|
+
Fetch raw compliance data from CSV report.
|
|
325
|
+
|
|
326
|
+
:return: List of raw compliance data (CSV rows as dictionaries)
|
|
327
|
+
:rtype: List[Dict[str, str]]
|
|
328
|
+
"""
|
|
329
|
+
# Use provided report file or get/create one
|
|
330
|
+
if self.report_file_path and os.path.exists(self.report_file_path):
|
|
331
|
+
report_file_path = self.report_file_path
|
|
332
|
+
else:
|
|
333
|
+
report_file_path = self._get_or_create_report()
|
|
334
|
+
if not report_file_path or not os.path.exists(report_file_path):
|
|
335
|
+
logger.error("Failed to get compliance report")
|
|
336
|
+
return []
|
|
337
|
+
|
|
338
|
+
# Read CSV file and return raw data
|
|
339
|
+
raw_data = []
|
|
340
|
+
try:
|
|
341
|
+
with open(report_file_path, "r", encoding="utf-8") as file:
|
|
342
|
+
csv_reader = csv.DictReader(file)
|
|
343
|
+
raw_data = list(csv_reader)
|
|
344
|
+
|
|
345
|
+
logger.info(f"Fetched {len(raw_data)} raw compliance records from CSV")
|
|
346
|
+
return raw_data
|
|
347
|
+
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logger.error(f"Error reading CSV report: {e}")
|
|
350
|
+
return []
|
|
351
|
+
|
|
352
|
+
def create_compliance_item(self, raw_data: Dict[str, str]) -> ComplianceItem:
|
|
353
|
+
"""
|
|
354
|
+
Create a ComplianceItem from raw compliance data.
|
|
355
|
+
|
|
356
|
+
:param Dict[str, str] raw_data: Raw compliance data from CSV row
|
|
357
|
+
:return: ComplianceItem instance
|
|
358
|
+
:rtype: ComplianceItem
|
|
359
|
+
"""
|
|
360
|
+
return WizComplianceReportItem(raw_data)
|
|
361
|
+
|
|
362
|
+
def _map_string_severity_to_enum(self, severity_str: str) -> regscale_models.IssueSeverity:
|
|
363
|
+
"""
|
|
364
|
+
Convert string severity to regscale_models.IssueSeverity enum.
|
|
365
|
+
|
|
366
|
+
:param str severity_str: String severity like "HIGH", "MEDIUM", etc.
|
|
367
|
+
:return: IssueSeverity enum value
|
|
368
|
+
:rtype: regscale_models.IssueSeverity
|
|
369
|
+
"""
|
|
370
|
+
severity_mapping = {
|
|
371
|
+
"CRITICAL": regscale_models.IssueSeverity.Critical,
|
|
372
|
+
"HIGH": regscale_models.IssueSeverity.High,
|
|
373
|
+
"MEDIUM": regscale_models.IssueSeverity.Moderate,
|
|
374
|
+
"MODERATE": regscale_models.IssueSeverity.Moderate,
|
|
375
|
+
"LOW": regscale_models.IssueSeverity.Low,
|
|
376
|
+
"INFORMATIONAL": regscale_models.IssueSeverity.Low,
|
|
377
|
+
}
|
|
378
|
+
return severity_mapping.get(severity_str.upper(), regscale_models.IssueSeverity.Low)
|
|
379
|
+
|
|
380
|
+
def process_compliance_data(self) -> None:
|
|
381
|
+
"""
|
|
382
|
+
Override the parent method to implement bypass logic for large control sets.
|
|
383
|
+
"""
|
|
384
|
+
if self.bypass_control_filtering:
|
|
385
|
+
logger.info("Bypassing control filtering due to bypass_control_filtering=True")
|
|
386
|
+
# Call parent method but bypass the allowed_controls_normalized logic
|
|
387
|
+
self._process_compliance_data_without_filtering()
|
|
388
|
+
else:
|
|
389
|
+
# Use standard parent implementation
|
|
390
|
+
super().process_compliance_data()
|
|
391
|
+
|
|
392
|
+
def _process_compliance_data_without_filtering(self) -> None:
|
|
393
|
+
"""
|
|
394
|
+
Process compliance data without control filtering for performance.
|
|
395
|
+
"""
|
|
396
|
+
logger.info("Processing compliance data without control filtering...")
|
|
397
|
+
|
|
398
|
+
self._reset_compliance_state()
|
|
399
|
+
raw_compliance_data = self.fetch_compliance_data()
|
|
400
|
+
self._process_raw_compliance_items(raw_compliance_data)
|
|
401
|
+
self._log_processing_debug_info()
|
|
402
|
+
self._categorize_controls_fail_first()
|
|
403
|
+
self._log_processing_summary()
|
|
404
|
+
self._log_categorization_debug_info()
|
|
405
|
+
|
|
406
|
+
def _reset_compliance_state(self) -> None:
|
|
407
|
+
"""Reset state to avoid double counting on repeated calls."""
|
|
408
|
+
self.all_compliance_items = []
|
|
409
|
+
self.failed_compliance_items = []
|
|
410
|
+
self.passing_controls = {}
|
|
411
|
+
self.failing_controls = {}
|
|
412
|
+
self.asset_compliance_map.clear()
|
|
413
|
+
|
|
414
|
+
def _process_raw_compliance_items(self, raw_compliance_data: List[Any], allowed_controls: set = None) -> dict:
|
|
415
|
+
"""Convert raw compliance data to ComplianceItem objects.
|
|
416
|
+
|
|
417
|
+
:param List[Any] raw_compliance_data: Raw compliance data from CSV row
|
|
418
|
+
:param set allowed_controls: Allowed control IDs (unused in this override, provided for interface compatibility)
|
|
419
|
+
:return: Processing statistics dictionary (empty dict for this implementation)
|
|
420
|
+
:rtype: dict
|
|
421
|
+
"""
|
|
422
|
+
for raw_item in raw_compliance_data:
|
|
423
|
+
try:
|
|
424
|
+
compliance_item = self.create_compliance_item(raw_item)
|
|
425
|
+
|
|
426
|
+
if not self._is_valid_compliance_item_for_processing(compliance_item):
|
|
427
|
+
continue
|
|
428
|
+
|
|
429
|
+
self._add_compliance_item_to_collections(compliance_item)
|
|
430
|
+
|
|
431
|
+
except Exception as e:
|
|
432
|
+
logger.error(f"Error processing compliance item: {e}")
|
|
433
|
+
continue
|
|
434
|
+
|
|
435
|
+
# Return empty stats dict for interface compatibility
|
|
436
|
+
return {}
|
|
437
|
+
|
|
438
|
+
def _is_valid_compliance_item_for_processing(self, compliance_item: Any) -> bool:
|
|
439
|
+
"""Check if compliance item has required control and resource IDs.
|
|
440
|
+
|
|
441
|
+
:param Any compliance_item: Compliance item to check
|
|
442
|
+
:return: True if compliance item has required control and resource IDs
|
|
443
|
+
:rtype: bool
|
|
444
|
+
"""
|
|
445
|
+
control_id = getattr(compliance_item, "control_id", "")
|
|
446
|
+
resource_id = getattr(compliance_item, "resource_id", "")
|
|
447
|
+
return bool(control_id and resource_id)
|
|
448
|
+
|
|
449
|
+
def _add_compliance_item_to_collections(self, compliance_item: Any) -> None:
|
|
450
|
+
"""Add compliance item to appropriate collections and categorize.
|
|
451
|
+
|
|
452
|
+
:param Any compliance_item: Compliance item to add to collections
|
|
453
|
+
:return: None
|
|
454
|
+
:rtype: None
|
|
455
|
+
"""
|
|
456
|
+
self.all_compliance_items.append(compliance_item)
|
|
457
|
+
self.asset_compliance_map[compliance_item.resource_id].append(compliance_item)
|
|
458
|
+
|
|
459
|
+
# Categorize by result - normalize to handle case variations
|
|
460
|
+
result_lower = compliance_item.compliance_result.lower()
|
|
461
|
+
fail_statuses_lower = [status.lower() for status in self.FAIL_STATUSES]
|
|
462
|
+
|
|
463
|
+
if result_lower in fail_statuses_lower:
|
|
464
|
+
self.failed_compliance_items.append(compliance_item)
|
|
465
|
+
|
|
466
|
+
def _log_processing_debug_info(self) -> None:
|
|
467
|
+
"""
|
|
468
|
+
Log debug information before categorization.
|
|
469
|
+
|
|
470
|
+
Logs sample compliance item data and status configurations
|
|
471
|
+
to help with debugging categorization issues.
|
|
472
|
+
|
|
473
|
+
:return: None
|
|
474
|
+
:rtype: None
|
|
475
|
+
"""
|
|
476
|
+
logger.debug(f"About to categorize {len(self.all_compliance_items)} compliance items")
|
|
477
|
+
if self.all_compliance_items:
|
|
478
|
+
sample_item = self.all_compliance_items[0]
|
|
479
|
+
logger.debug(
|
|
480
|
+
f"DEBUG: Sample item control_id='{sample_item.control_id}', result='{sample_item.compliance_result}'"
|
|
481
|
+
)
|
|
482
|
+
logger.debug(f"FAIL_STATUSES = {self.FAIL_STATUSES}")
|
|
483
|
+
logger.debug(f"PASS_STATUSES = {self.PASS_STATUSES}")
|
|
484
|
+
|
|
485
|
+
def _log_processing_summary(self, raw_compliance_data: list = None, stats: dict = None) -> None:
|
|
486
|
+
"""
|
|
487
|
+
Log summary of processed compliance items.
|
|
488
|
+
|
|
489
|
+
Provides a summary count of total items, passing items, failing items,
|
|
490
|
+
and control categorization results for monitoring processing progress.
|
|
491
|
+
|
|
492
|
+
:param list raw_compliance_data: Raw compliance data (unused in this implementation, for interface compatibility)
|
|
493
|
+
:param dict stats: Processing statistics (unused in this implementation, for interface compatibility)
|
|
494
|
+
:return: None
|
|
495
|
+
:rtype: None
|
|
496
|
+
"""
|
|
497
|
+
passing_count = len(self.all_compliance_items) - len(self.failed_compliance_items)
|
|
498
|
+
failing_count = len(self.failed_compliance_items)
|
|
499
|
+
|
|
500
|
+
logger.info(
|
|
501
|
+
f"Processed {len(self.all_compliance_items)} compliance items: "
|
|
502
|
+
f"{passing_count} passing, {failing_count} failing"
|
|
503
|
+
)
|
|
504
|
+
logger.info(
|
|
505
|
+
f"Control categorization: {len(self.passing_controls)} passing controls, "
|
|
506
|
+
f"{len(self.failing_controls)} failing controls"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
def _log_categorization_debug_info(self) -> None:
|
|
510
|
+
"""
|
|
511
|
+
Log debug information about categorized controls.
|
|
512
|
+
|
|
513
|
+
Outputs lists of passing and failing control IDs for debugging
|
|
514
|
+
categorization logic and identifying potential issues.
|
|
515
|
+
|
|
516
|
+
:return: None
|
|
517
|
+
:rtype: None
|
|
518
|
+
"""
|
|
519
|
+
if self.passing_controls:
|
|
520
|
+
logger.debug(f"Passing control IDs: {list(self.passing_controls.keys())}")
|
|
521
|
+
if self.failing_controls:
|
|
522
|
+
logger.debug(f"Failing control IDs: {list(self.failing_controls.keys())}")
|
|
523
|
+
if not self.passing_controls and not self.failing_controls:
|
|
524
|
+
logger.error(
|
|
525
|
+
"DEBUG: No controls were categorized! This indicates an issue in _categorize_controls_fail_first"
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
def _categorize_controls_fail_first(self) -> None:
|
|
529
|
+
"""
|
|
530
|
+
Categorize controls using fail-first logic.
|
|
531
|
+
|
|
532
|
+
If ANY compliance item for a control is failing, the entire control is marked as failing.
|
|
533
|
+
A control is only marked as passing if ALL instances of that control are passing.
|
|
534
|
+
"""
|
|
535
|
+
logger.info("Starting fail-first control categorization...")
|
|
536
|
+
|
|
537
|
+
control_results = self._determine_control_results()
|
|
538
|
+
self._populate_control_collections(control_results)
|
|
539
|
+
self._populate_failed_compliance_items()
|
|
540
|
+
self._log_categorization_completion()
|
|
541
|
+
|
|
542
|
+
def _determine_control_results(self) -> Dict[str, str]:
|
|
543
|
+
"""
|
|
544
|
+
Determine pass/fail status for each control based on compliance items.
|
|
545
|
+
|
|
546
|
+
Analyzes all compliance items and applies fail-first logic to determine
|
|
547
|
+
the overall status for each control. A control is marked as "fail" if
|
|
548
|
+
ANY compliance item for that control is failing.
|
|
549
|
+
|
|
550
|
+
:return: Dictionary mapping control IDs (lowercase) to "pass" or "fail"
|
|
551
|
+
:rtype: Dict[str, str]
|
|
552
|
+
"""
|
|
553
|
+
control_results = {} # {control_id: "pass" or "fail"}
|
|
554
|
+
|
|
555
|
+
for item in self.all_compliance_items:
|
|
556
|
+
control_ids = self._get_control_ids_for_item(item)
|
|
557
|
+
|
|
558
|
+
for control_id in control_ids:
|
|
559
|
+
if not control_id:
|
|
560
|
+
continue
|
|
561
|
+
|
|
562
|
+
control_id_lower = control_id.lower()
|
|
563
|
+
|
|
564
|
+
if self._is_compliance_item_failing(item):
|
|
565
|
+
control_results[control_id_lower] = "fail"
|
|
566
|
+
logger.debug(f"Control {control_id} marked as FAILING due to failed item")
|
|
567
|
+
elif control_id_lower not in control_results:
|
|
568
|
+
control_results[control_id_lower] = "pass"
|
|
569
|
+
|
|
570
|
+
return control_results
|
|
571
|
+
|
|
572
|
+
def _get_control_ids_for_item(self, item: Any) -> List[str]:
|
|
573
|
+
"""
|
|
574
|
+
Get all control IDs for a compliance item.
|
|
575
|
+
|
|
576
|
+
Extracts control IDs from compliance items that may reference
|
|
577
|
+
multiple controls (e.g., multi-control compliance checks).
|
|
578
|
+
|
|
579
|
+
:param Any item: Compliance item to extract control IDs from
|
|
580
|
+
:return: List of control ID strings
|
|
581
|
+
:rtype: List[str]
|
|
582
|
+
"""
|
|
583
|
+
if hasattr(item, "get_all_control_ids"):
|
|
584
|
+
return item.get_all_control_ids()
|
|
585
|
+
else:
|
|
586
|
+
return [item.control_id] if item.control_id else []
|
|
587
|
+
|
|
588
|
+
def _is_compliance_item_failing(self, item: Any) -> bool:
|
|
589
|
+
"""
|
|
590
|
+
Check if a compliance item is failing.
|
|
591
|
+
|
|
592
|
+
Compares the compliance result against the list of failure statuses
|
|
593
|
+
using case-insensitive matching.
|
|
594
|
+
|
|
595
|
+
:param Any item: Compliance item to check
|
|
596
|
+
:return: True if the item is failing, False otherwise
|
|
597
|
+
:rtype: bool
|
|
598
|
+
"""
|
|
599
|
+
result_lower = item.compliance_result.lower()
|
|
600
|
+
fail_statuses_lower = [status.lower() for status in self.FAIL_STATUSES]
|
|
601
|
+
return result_lower in fail_statuses_lower
|
|
602
|
+
|
|
603
|
+
def _populate_control_collections(self, control_results: Dict[str, str]) -> None:
|
|
604
|
+
"""
|
|
605
|
+
Populate passing and failing control collections.
|
|
606
|
+
|
|
607
|
+
Based on the control results dictionary, populates the passing_controls
|
|
608
|
+
and failing_controls collections with the appropriate compliance items.
|
|
609
|
+
|
|
610
|
+
:param Dict[str, str] control_results: Dictionary mapping control IDs to "pass" or "fail"
|
|
611
|
+
:return: None
|
|
612
|
+
:rtype: None
|
|
613
|
+
"""
|
|
614
|
+
for control_id_lower, result in control_results.items():
|
|
615
|
+
if result == "fail":
|
|
616
|
+
self.failing_controls[control_id_lower] = self._get_items_for_control(control_id_lower)
|
|
617
|
+
else:
|
|
618
|
+
self.passing_controls[control_id_lower] = self._get_items_for_control(control_id_lower)
|
|
619
|
+
|
|
620
|
+
def _get_items_for_control(self, control_id_lower: str) -> List[Any]:
|
|
621
|
+
"""
|
|
622
|
+
Get all compliance items that belong to a specific control.
|
|
623
|
+
|
|
624
|
+
Searches through all compliance items to find those that reference
|
|
625
|
+
the specified control ID (case-insensitive matching).
|
|
626
|
+
|
|
627
|
+
:param str control_id_lower: Control ID in lowercase format
|
|
628
|
+
:return: List of compliance items for the control
|
|
629
|
+
:rtype: List[Any]
|
|
630
|
+
"""
|
|
631
|
+
items = []
|
|
632
|
+
for item in self.all_compliance_items:
|
|
633
|
+
item_control_ids = self._get_normalized_control_ids_for_item(item)
|
|
634
|
+
if control_id_lower in item_control_ids:
|
|
635
|
+
items.append(item)
|
|
636
|
+
return items
|
|
637
|
+
|
|
638
|
+
def _get_normalized_control_ids_for_item(self, item: Any) -> List[str]:
|
|
639
|
+
"""
|
|
640
|
+
Get normalized (lowercase) control IDs for an item.
|
|
641
|
+
|
|
642
|
+
Extracts all control IDs from a compliance item and normalizes
|
|
643
|
+
them to lowercase for consistent comparison and matching.
|
|
644
|
+
|
|
645
|
+
:param Any item: Compliance item to extract control IDs from
|
|
646
|
+
:return: List of normalized control ID strings
|
|
647
|
+
:rtype: List[str]
|
|
648
|
+
"""
|
|
649
|
+
if hasattr(item, "get_all_control_ids"):
|
|
650
|
+
return [cid.lower() for cid in item.get_all_control_ids()]
|
|
651
|
+
else:
|
|
652
|
+
return [item.control_id.lower()] if item.control_id else []
|
|
653
|
+
|
|
654
|
+
def _populate_failed_compliance_items(self) -> None:
|
|
655
|
+
"""
|
|
656
|
+
Populate and deduplicate the failed compliance items list.
|
|
657
|
+
|
|
658
|
+
Collects all failing compliance items from the failing_controls
|
|
659
|
+
collection and removes duplicates to create a clean list of
|
|
660
|
+
failed items for issue processing.
|
|
661
|
+
|
|
662
|
+
:return: None
|
|
663
|
+
:rtype: None
|
|
664
|
+
"""
|
|
665
|
+
self.failed_compliance_items.clear()
|
|
666
|
+
|
|
667
|
+
for control_id, failing_items in self.failing_controls.items():
|
|
668
|
+
self.failed_compliance_items.extend(failing_items)
|
|
669
|
+
|
|
670
|
+
self.failed_compliance_items = self._remove_duplicate_items(self.failed_compliance_items)
|
|
671
|
+
|
|
672
|
+
def _remove_duplicate_items(self, items: List[Any]) -> List[Any]:
|
|
673
|
+
"""
|
|
674
|
+
Remove duplicate compliance items while preserving order.
|
|
675
|
+
|
|
676
|
+
Uses resource_id and control_id to create unique keys for
|
|
677
|
+
deduplication while maintaining the original order of items.
|
|
678
|
+
|
|
679
|
+
:param List[Any] items: List of compliance items to deduplicate
|
|
680
|
+
:return: List of unique compliance items
|
|
681
|
+
:rtype: List[Any]
|
|
682
|
+
"""
|
|
683
|
+
seen = set()
|
|
684
|
+
unique_items = []
|
|
685
|
+
|
|
686
|
+
for item in items:
|
|
687
|
+
item_key = f"{getattr(item, 'resource_id', '')}-{getattr(item, 'control_id', '')}"
|
|
688
|
+
if item_key not in seen:
|
|
689
|
+
seen.add(item_key)
|
|
690
|
+
unique_items.append(item)
|
|
691
|
+
|
|
692
|
+
return unique_items
|
|
693
|
+
|
|
694
|
+
def _log_categorization_completion(self) -> None:
|
|
695
|
+
"""
|
|
696
|
+
Log completion of control categorization.
|
|
697
|
+
|
|
698
|
+
Provides final summary statistics about the categorization process,
|
|
699
|
+
including counts of passing/failing controls and failed items.
|
|
700
|
+
|
|
701
|
+
:return: None
|
|
702
|
+
:rtype: None
|
|
703
|
+
"""
|
|
704
|
+
logger.info(
|
|
705
|
+
f"Fail-first categorization complete: {len(self.passing_controls)} passing, "
|
|
706
|
+
f"{len(self.failing_controls)} failing controls"
|
|
707
|
+
)
|
|
708
|
+
logger.info(f"Populated failed_compliance_items list with {len(self.failed_compliance_items)} items")
|
|
709
|
+
|
|
710
|
+
def process_compliance_sync(self) -> None:
|
|
711
|
+
"""
|
|
712
|
+
New main method using ComplianceIntegration pattern.
|
|
713
|
+
|
|
714
|
+
This replaces the old process_compliance_report method.
|
|
715
|
+
"""
|
|
716
|
+
logger.info("Starting Wiz compliance sync using ComplianceIntegration pattern...")
|
|
717
|
+
self.sync_compliance()
|
|
718
|
+
|
|
719
|
+
def _get_or_create_report(self, max_age_hours: int = None) -> Optional[str]:
|
|
720
|
+
"""
|
|
721
|
+
Get existing recent report or create a new one if needed.
|
|
722
|
+
|
|
723
|
+
:param int max_age_hours: Maximum age in hours for reusing existing reports (deprecated, use max_report_age_days)
|
|
724
|
+
:return: Path to report file
|
|
725
|
+
:rtype: Optional[str]
|
|
726
|
+
"""
|
|
727
|
+
# Handle force fresh report request
|
|
728
|
+
if self.force_fresh_report:
|
|
729
|
+
logger.info("Force fresh report requested, creating new compliance report...")
|
|
730
|
+
return self._create_and_download_report(force_new=True)
|
|
731
|
+
|
|
732
|
+
# Use instance variable max_report_age_days or legacy max_age_hours
|
|
733
|
+
if max_age_hours is not None:
|
|
734
|
+
# Legacy behavior for backward compatibility
|
|
735
|
+
max_age_hours_to_use = max_age_hours
|
|
736
|
+
logger.warning("Using deprecated max_age_hours parameter. Consider using max_report_age_days instead.")
|
|
737
|
+
else:
|
|
738
|
+
# Convert days to hours for the internal method
|
|
739
|
+
max_age_hours_to_use = self.max_report_age_days * 24
|
|
740
|
+
|
|
741
|
+
# Check for existing recent reports
|
|
742
|
+
existing_report = self._find_recent_report(max_age_hours_to_use)
|
|
743
|
+
if existing_report:
|
|
744
|
+
logger.info(f"Using existing report: {existing_report}")
|
|
745
|
+
return existing_report
|
|
746
|
+
|
|
747
|
+
# No recent report found, create a new one
|
|
748
|
+
logger.info(f"No recent report found within {self.max_report_age_days} days, creating new compliance report...")
|
|
749
|
+
return self._create_and_download_report()
|
|
750
|
+
|
|
751
|
+
def _find_recent_report(self, max_age_hours: int = 24) -> Optional[str]:
|
|
752
|
+
"""
|
|
753
|
+
Find the most recent compliance report within the specified age limit.
|
|
754
|
+
|
|
755
|
+
:param int max_age_hours: Maximum age in hours
|
|
756
|
+
:return: Path to recent report file or None
|
|
757
|
+
:rtype: Optional[str]
|
|
758
|
+
"""
|
|
759
|
+
artifacts_dir = "artifacts/wiz"
|
|
760
|
+
if not os.path.exists(artifacts_dir):
|
|
761
|
+
return None
|
|
762
|
+
|
|
763
|
+
report_prefix = f"compliance_report_{self.wiz_project_id}_"
|
|
764
|
+
cutoff_time = datetime.now() - timedelta(hours=max_age_hours)
|
|
765
|
+
|
|
766
|
+
# Find matching files
|
|
767
|
+
matching_files = []
|
|
768
|
+
for filename in os.listdir(artifacts_dir):
|
|
769
|
+
if filename.startswith(report_prefix) and filename.endswith(".csv"):
|
|
770
|
+
file_path = os.path.join(artifacts_dir, filename)
|
|
771
|
+
try:
|
|
772
|
+
# Get file modification time
|
|
773
|
+
mod_time = datetime.fromtimestamp(os.path.getmtime(file_path))
|
|
774
|
+
if mod_time > cutoff_time:
|
|
775
|
+
matching_files.append((file_path, mod_time))
|
|
776
|
+
except (OSError, ValueError):
|
|
777
|
+
continue
|
|
778
|
+
|
|
779
|
+
if not matching_files:
|
|
780
|
+
return None
|
|
781
|
+
|
|
782
|
+
# Return the most recent file
|
|
783
|
+
most_recent = max(matching_files, key=lambda x: x[1])
|
|
784
|
+
age_hours = (datetime.now() - most_recent[1]).total_seconds() / 3600
|
|
785
|
+
logger.info(f"Found recent report (age: {age_hours:.1f}h): {most_recent[0]}")
|
|
786
|
+
return most_recent[0]
|
|
787
|
+
|
|
788
|
+
def _find_existing_compliance_report(self) -> Optional[str]:
|
|
789
|
+
"""
|
|
790
|
+
Find existing compliance report for the current project.
|
|
791
|
+
|
|
792
|
+
:return: Report ID if found, None otherwise
|
|
793
|
+
:rtype: Optional[str]
|
|
794
|
+
"""
|
|
795
|
+
try:
|
|
796
|
+
# Filter for compliance reports (projectId not supported in ReportFilters, using name-based lookup)
|
|
797
|
+
filter_by = {"type": ["COMPLIANCE_ASSESSMENTS"]}
|
|
798
|
+
|
|
799
|
+
logger.debug(f"Searching for existing compliance reports with filter: {filter_by}")
|
|
800
|
+
reports = self.report_manager.list_reports(filter_by=filter_by)
|
|
801
|
+
|
|
802
|
+
if not reports:
|
|
803
|
+
logger.info("No existing compliance reports found")
|
|
804
|
+
return None
|
|
805
|
+
|
|
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]
|
|
809
|
+
|
|
810
|
+
if not matching_reports:
|
|
811
|
+
logger.info(f"No existing compliance report found with name: {expected_name}")
|
|
812
|
+
return None
|
|
813
|
+
|
|
814
|
+
# Return the first matching report (most recent will be used)
|
|
815
|
+
selected_report = matching_reports[0]
|
|
816
|
+
report_id = selected_report.get("id")
|
|
817
|
+
report_name = selected_report.get("name", "Unknown")
|
|
818
|
+
|
|
819
|
+
logger.info(f"Found existing compliance report: '{report_name}' (ID: {report_id})")
|
|
820
|
+
return report_id
|
|
821
|
+
|
|
822
|
+
except Exception as e:
|
|
823
|
+
logger.error(f"Error searching for existing compliance reports: {e}")
|
|
824
|
+
return None
|
|
825
|
+
|
|
826
|
+
def _create_and_download_report(self, force_new: bool = False) -> Optional[str]:
|
|
827
|
+
"""
|
|
828
|
+
Find existing compliance report and rerun it, or create a new one if none exists.
|
|
829
|
+
|
|
830
|
+
:param bool force_new: Force creation of new report, skip reuse logic
|
|
831
|
+
:return: Path to downloaded report file
|
|
832
|
+
:rtype: Optional[str]
|
|
833
|
+
"""
|
|
834
|
+
if force_new or not self.reuse_existing_reports:
|
|
835
|
+
logger.info("Creating new compliance report (reuse disabled or forced)")
|
|
836
|
+
# Create new report
|
|
837
|
+
report_id = self.report_manager.create_compliance_report(self.wiz_project_id)
|
|
838
|
+
if not report_id:
|
|
839
|
+
logger.error("Failed to create compliance report")
|
|
840
|
+
return None
|
|
841
|
+
|
|
842
|
+
# Wait for completion and get download URL
|
|
843
|
+
download_url = self.report_manager.wait_for_report_completion(report_id)
|
|
844
|
+
else:
|
|
845
|
+
logger.info(f"Looking for existing compliance report for project: {self.wiz_project_id}")
|
|
846
|
+
|
|
847
|
+
# Try to find existing compliance report for this project
|
|
848
|
+
if existing_report_id := self._find_existing_compliance_report():
|
|
849
|
+
logger.info(
|
|
850
|
+
f"Found existing compliance report {existing_report_id}, rerunning instead of creating new one"
|
|
851
|
+
)
|
|
852
|
+
# Rerun existing report
|
|
853
|
+
download_url = self.report_manager.rerun_report(existing_report_id)
|
|
854
|
+
else:
|
|
855
|
+
logger.info("No existing compliance report found, creating new one")
|
|
856
|
+
# Create new report
|
|
857
|
+
report_id = self.report_manager.create_compliance_report(self.wiz_project_id)
|
|
858
|
+
if not report_id:
|
|
859
|
+
logger.error("Failed to create compliance report")
|
|
860
|
+
return None
|
|
861
|
+
|
|
862
|
+
# Wait for completion and get download URL
|
|
863
|
+
download_url = self.report_manager.wait_for_report_completion(report_id)
|
|
864
|
+
|
|
865
|
+
if not download_url:
|
|
866
|
+
logger.error("Failed to get download URL for report")
|
|
867
|
+
return None
|
|
868
|
+
|
|
869
|
+
# Download report
|
|
870
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
871
|
+
output_path = f"artifacts/wiz/compliance_report_{self.wiz_project_id}_{timestamp}.csv"
|
|
872
|
+
|
|
873
|
+
# Ensure directory exists
|
|
874
|
+
artifacts_dir = os.path.dirname(output_path)
|
|
875
|
+
os.makedirs(artifacts_dir, exist_ok=True)
|
|
876
|
+
|
|
877
|
+
if self.report_manager.download_report(download_url, output_path):
|
|
878
|
+
# Clean up old report files
|
|
879
|
+
ReportFileCleanup.cleanup_old_files(
|
|
880
|
+
directory=artifacts_dir, file_prefix="compliance_report_", extensions=[".csv"], keep_count=5
|
|
881
|
+
)
|
|
882
|
+
return output_path
|
|
883
|
+
else:
|
|
884
|
+
logger.error("Failed to download report")
|
|
885
|
+
return None
|
|
886
|
+
|
|
887
|
+
def _update_passing_controls_to_implemented(self, passing_control_ids: list[str]) -> None:
|
|
888
|
+
"""
|
|
889
|
+
Update passing controls to 'Implemented' status in RegScale.
|
|
890
|
+
|
|
891
|
+
Uses ControlMatcher for robust control ID matching with leading zero normalization.
|
|
892
|
+
|
|
893
|
+
:param list[str] passing_control_ids: List of control IDs that passed
|
|
894
|
+
"""
|
|
895
|
+
if not passing_control_ids:
|
|
896
|
+
return
|
|
897
|
+
|
|
898
|
+
try:
|
|
899
|
+
logger.debug(f"Looking for passing control IDs: {passing_control_ids}")
|
|
900
|
+
|
|
901
|
+
# Prepare batch updates for passing controls
|
|
902
|
+
implementations_to_update = []
|
|
903
|
+
controls_not_found = []
|
|
904
|
+
|
|
905
|
+
for control_id in passing_control_ids:
|
|
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
|
+
)
|
|
951
|
+
|
|
952
|
+
# Batch update all implementations
|
|
953
|
+
if implementations_to_update:
|
|
954
|
+
ControlImplementation.put_batch_implementation(self.app, implementations_to_update)
|
|
955
|
+
logger.info(f"Successfully updated {len(implementations_to_update)} controls to Implemented status")
|
|
956
|
+
else:
|
|
957
|
+
logger.warning("No matching control implementations found to update")
|
|
958
|
+
|
|
959
|
+
except Exception as e:
|
|
960
|
+
logger.error(f"Error updating control implementation status: {e}")
|
|
961
|
+
|
|
962
|
+
def _prepare_failing_control_update(self, control_id: str) -> Optional[dict]:
|
|
963
|
+
"""
|
|
964
|
+
Prepare a single failing control for update.
|
|
965
|
+
|
|
966
|
+
:param str control_id: Control ID to update
|
|
967
|
+
:return: Dictionary representation of updated implementation, or None if not found
|
|
968
|
+
:rtype: Optional[dict]
|
|
969
|
+
"""
|
|
970
|
+
impl = self._control_matcher.find_control_implementation(
|
|
971
|
+
control_id=control_id, parent_id=self.plan_id, parent_module=self.parent_module
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
if not impl:
|
|
975
|
+
logger.debug(f"Control '{control_id}' not found in implementation map")
|
|
976
|
+
return None
|
|
977
|
+
|
|
978
|
+
logger.debug(f"Found matching implementation for '{control_id}': {impl.id}")
|
|
979
|
+
|
|
980
|
+
new_status = self._get_implementation_status_from_result("Fail")
|
|
981
|
+
logger.debug(f"Setting control {control_id} status from 'Fail' result to: {new_status}")
|
|
982
|
+
|
|
983
|
+
impl.status = new_status
|
|
984
|
+
impl.dateLastAssessed = get_current_datetime()
|
|
985
|
+
impl.lastAssessmentResult = "Fail"
|
|
986
|
+
impl.bStatusImplemented = False
|
|
987
|
+
|
|
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
|
+
"""
|
|
1003
|
+
if not impl.responsibility:
|
|
1004
|
+
impl.responsibility = ControlImplementation.get_default_responsibility(parent_id=impl.parentId)
|
|
1005
|
+
logger.debug(f"Setting default responsibility for control {control_id}: {impl.responsibility}")
|
|
1006
|
+
|
|
1007
|
+
if not impl.implementation:
|
|
1008
|
+
impl.implementation = f"Implementation details for {control_id} will be documented."
|
|
1009
|
+
logger.debug(f"Setting default implementation statement for control {control_id}")
|
|
1010
|
+
|
|
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
|
+
"""
|
|
1019
|
+
user_id = self.app.config.get("userId")
|
|
1020
|
+
if user_id:
|
|
1021
|
+
impl.lastUpdatedById = user_id
|
|
1022
|
+
impl.dateLastUpdated = get_current_datetime()
|
|
1023
|
+
|
|
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
|
|
1036
|
+
|
|
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
|
+
"""
|
|
1065
|
+
if controls_not_found:
|
|
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.
|
|
1076
|
+
|
|
1077
|
+
:param List[dict] implementations_to_update: List of implementations to update
|
|
1078
|
+
:return: None
|
|
1079
|
+
:rtype: None
|
|
1080
|
+
"""
|
|
1081
|
+
if implementations_to_update:
|
|
1082
|
+
ControlImplementation.put_batch_implementation(self.app, implementations_to_update)
|
|
1083
|
+
logger.debug(f"Updated {len(implementations_to_update)} Control Implementations, Successfully!")
|
|
1084
|
+
logger.info(f"Successfully updated {len(implementations_to_update)} controls to In Remediation status")
|
|
1085
|
+
else:
|
|
1086
|
+
logger.warning("No matching control implementations found to update for failing controls")
|
|
1087
|
+
|
|
1088
|
+
def _process_control_assessments(self) -> None:
|
|
1089
|
+
"""
|
|
1090
|
+
Override parent method to add control implementation status updates.
|
|
1091
|
+
"""
|
|
1092
|
+
# Call parent method to create assessments
|
|
1093
|
+
super()._process_control_assessments()
|
|
1094
|
+
|
|
1095
|
+
# Update control implementation status for both passing and failing controls if enabled
|
|
1096
|
+
if self.update_control_status:
|
|
1097
|
+
if self.passing_controls:
|
|
1098
|
+
passing_control_ids = list(self.passing_controls.keys())
|
|
1099
|
+
logger.info(f"Updating control implementation status for {len(passing_control_ids)} passing controls")
|
|
1100
|
+
self._update_passing_controls_to_implemented(passing_control_ids)
|
|
1101
|
+
|
|
1102
|
+
if self.failing_controls:
|
|
1103
|
+
failing_control_ids = list(self.failing_controls.keys())
|
|
1104
|
+
logger.info(
|
|
1105
|
+
f"Attempting to update control implementation status for {len(failing_control_ids)} failing controls"
|
|
1106
|
+
)
|
|
1107
|
+
self._update_failing_controls_to_in_remediation(failing_control_ids)
|
|
1108
|
+
|
|
1109
|
+
def _categorize_controls_by_aggregation(self) -> None:
|
|
1110
|
+
"""
|
|
1111
|
+
Override parent method to implement "fail-first" logic for Wiz compliance.
|
|
1112
|
+
|
|
1113
|
+
In the Wiz compliance integration, we implement strict "fail-first" logic:
|
|
1114
|
+
- If ANY compliance item for a control is failing, the entire control is marked as failing
|
|
1115
|
+
- A control is only marked as passing if ALL instances of that control are passing
|
|
1116
|
+
- This applies to both single-control and multi-control compliance items
|
|
1117
|
+
"""
|
|
1118
|
+
control_items = self._group_compliance_items_by_control()
|
|
1119
|
+
self._apply_fail_first_logic_to_controls(control_items)
|
|
1120
|
+
self._populate_failed_compliance_items_from_control_items(control_items)
|
|
1121
|
+
self._log_categorization_results()
|
|
1122
|
+
|
|
1123
|
+
def _group_compliance_items_by_control(self) -> dict:
|
|
1124
|
+
"""
|
|
1125
|
+
Group compliance items by control ID.
|
|
1126
|
+
|
|
1127
|
+
Creates a dictionary mapping control IDs (lowercase) to lists of
|
|
1128
|
+
compliance items that reference those controls. Handles multi-control
|
|
1129
|
+
items that may reference multiple control IDs.
|
|
1130
|
+
|
|
1131
|
+
:return: Dictionary mapping control IDs to lists of compliance items
|
|
1132
|
+
:rtype: dict
|
|
1133
|
+
"""
|
|
1134
|
+
from collections import defaultdict
|
|
1135
|
+
|
|
1136
|
+
control_items = defaultdict(list)
|
|
1137
|
+
|
|
1138
|
+
for item in self.all_compliance_items:
|
|
1139
|
+
control_ids = self._extract_control_ids_from_item(item)
|
|
1140
|
+
self._add_item_to_control_groups(item, control_ids, control_items)
|
|
1141
|
+
|
|
1142
|
+
logger.debug(
|
|
1143
|
+
f"Grouped {len(self.all_compliance_items)} compliance items into {len(control_items)} control groups"
|
|
1144
|
+
)
|
|
1145
|
+
return control_items
|
|
1146
|
+
|
|
1147
|
+
def _extract_control_ids_from_item(self, item) -> list:
|
|
1148
|
+
"""
|
|
1149
|
+
Extract all control IDs that an item affects.
|
|
1150
|
+
|
|
1151
|
+
Checks if the item has a get_all_control_ids method for multi-control
|
|
1152
|
+
items, otherwise falls back to the single control_id attribute.
|
|
1153
|
+
|
|
1154
|
+
:param item: Compliance item to extract control IDs from
|
|
1155
|
+
:type item: Any
|
|
1156
|
+
:return: List of control ID strings
|
|
1157
|
+
:rtype: list
|
|
1158
|
+
"""
|
|
1159
|
+
if hasattr(item, "get_all_control_ids") and callable(item.get_all_control_ids):
|
|
1160
|
+
return item.get_all_control_ids()
|
|
1161
|
+
return [item.control_id] if item.control_id else []
|
|
1162
|
+
|
|
1163
|
+
def _add_item_to_control_groups(self, item, control_ids: list, control_items: dict) -> None:
|
|
1164
|
+
"""
|
|
1165
|
+
Add item to all control groups it affects.
|
|
1166
|
+
|
|
1167
|
+
Adds the compliance item to the appropriate control groups based on
|
|
1168
|
+
all the control IDs it references. Uses lowercase control IDs as keys.
|
|
1169
|
+
|
|
1170
|
+
:param item: Compliance item to add to groups
|
|
1171
|
+
:type item: Any
|
|
1172
|
+
:param list control_ids: List of control IDs the item affects
|
|
1173
|
+
:param dict control_items: Dictionary of control groups to update
|
|
1174
|
+
:return: None
|
|
1175
|
+
:rtype: None
|
|
1176
|
+
"""
|
|
1177
|
+
for control_id in control_ids:
|
|
1178
|
+
if control_id:
|
|
1179
|
+
control_key = control_id.lower()
|
|
1180
|
+
control_items[control_key].append(item)
|
|
1181
|
+
|
|
1182
|
+
def _apply_fail_first_logic_to_controls(self, control_items: dict) -> None:
|
|
1183
|
+
"""
|
|
1184
|
+
Apply fail-first logic to categorize each control as passing or failing.
|
|
1185
|
+
|
|
1186
|
+
For each control, determines its overall status based on all associated
|
|
1187
|
+
compliance items. Any failure in the items makes the control fail.
|
|
1188
|
+
|
|
1189
|
+
:param dict control_items: Dictionary mapping control IDs to compliance items
|
|
1190
|
+
:return: None
|
|
1191
|
+
:rtype: None
|
|
1192
|
+
"""
|
|
1193
|
+
for control_key, items in control_items.items():
|
|
1194
|
+
control_status = self._determine_control_status(items)
|
|
1195
|
+
self._categorize_control(control_key, control_status, len(items))
|
|
1196
|
+
|
|
1197
|
+
def _determine_control_status(self, items: list) -> dict:
|
|
1198
|
+
"""
|
|
1199
|
+
Determine the overall status of a control based on its items.
|
|
1200
|
+
|
|
1201
|
+
Analyzes all compliance items for a control to determine if any are
|
|
1202
|
+
failing or passing. Returns status indicators and representative items.
|
|
1203
|
+
|
|
1204
|
+
:param list items: List of compliance items for the control
|
|
1205
|
+
:return: Dictionary with status flags and representative items
|
|
1206
|
+
:rtype: dict
|
|
1207
|
+
"""
|
|
1208
|
+
fail_statuses_lower = [status.lower() for status in self.FAIL_STATUSES]
|
|
1209
|
+
pass_statuses_lower = [status.lower() for status in self.PASS_STATUSES]
|
|
1210
|
+
|
|
1211
|
+
status = {"has_failure": False, "has_pass": False, "failing_item": None, "passing_item": None}
|
|
1212
|
+
|
|
1213
|
+
for item in items:
|
|
1214
|
+
result_lower = item.compliance_result.lower()
|
|
1215
|
+
|
|
1216
|
+
if result_lower in fail_statuses_lower:
|
|
1217
|
+
status["has_failure"] = True
|
|
1218
|
+
if not status["failing_item"]:
|
|
1219
|
+
status["failing_item"] = item
|
|
1220
|
+
elif result_lower in pass_statuses_lower:
|
|
1221
|
+
status["has_pass"] = True
|
|
1222
|
+
if not status["passing_item"]:
|
|
1223
|
+
status["passing_item"] = item
|
|
1224
|
+
|
|
1225
|
+
return status
|
|
1226
|
+
|
|
1227
|
+
def _categorize_control(self, control_key: str, status: dict, item_count: int) -> None:
|
|
1228
|
+
"""
|
|
1229
|
+
Categorize a control as passing or failing based on its status.
|
|
1230
|
+
|
|
1231
|
+
Uses the status information to place the control in the appropriate
|
|
1232
|
+
passing or failing collection and logs the categorization decision.
|
|
1233
|
+
|
|
1234
|
+
:param str control_key: Control ID (lowercase)
|
|
1235
|
+
:param dict status: Status information from _determine_control_status
|
|
1236
|
+
:param int item_count: Number of items analyzed for the control
|
|
1237
|
+
:return: None
|
|
1238
|
+
:rtype: None
|
|
1239
|
+
"""
|
|
1240
|
+
if status["has_failure"]:
|
|
1241
|
+
self.failing_controls[control_key] = status["failing_item"]
|
|
1242
|
+
logger.debug(f"Control {control_key} marked as FAILING: fail-first logic triggered")
|
|
1243
|
+
elif status["has_pass"]:
|
|
1244
|
+
self.passing_controls[control_key] = status["passing_item"]
|
|
1245
|
+
logger.debug(f"Control {control_key} marked as PASSING: all {item_count} items passed")
|
|
1246
|
+
else:
|
|
1247
|
+
logger.debug(f"Control {control_key} has unclear results - no pass or fail statuses found")
|
|
1248
|
+
|
|
1249
|
+
def _populate_failed_compliance_items_from_control_items(self, control_items: dict) -> None:
|
|
1250
|
+
"""
|
|
1251
|
+
Populate the list of failed compliance items from failing controls.
|
|
1252
|
+
|
|
1253
|
+
Collects all failing compliance items from controls marked as failing,
|
|
1254
|
+
removes duplicates, and updates the failed_compliance_items list.
|
|
1255
|
+
|
|
1256
|
+
:param dict control_items: Dictionary mapping control IDs to compliance items
|
|
1257
|
+
:return: None
|
|
1258
|
+
:rtype: None
|
|
1259
|
+
"""
|
|
1260
|
+
self.failed_compliance_items.clear()
|
|
1261
|
+
failing_items = self._collect_failing_items_from_controls(control_items)
|
|
1262
|
+
self.failed_compliance_items = self._remove_duplicate_items(failing_items)
|
|
1263
|
+
logger.info(f"Populated failed_compliance_items list with {len(self.failed_compliance_items)} items")
|
|
1264
|
+
|
|
1265
|
+
def _collect_failing_items_from_controls(self, control_items: dict) -> list:
|
|
1266
|
+
"""
|
|
1267
|
+
Collect all failing items from controls marked as failing.
|
|
1268
|
+
|
|
1269
|
+
Iterates through controls marked as failing and collects all their
|
|
1270
|
+
compliance items that have failing status results.
|
|
1271
|
+
|
|
1272
|
+
:param dict control_items: Dictionary mapping control IDs to compliance items
|
|
1273
|
+
:return: List of failing compliance items
|
|
1274
|
+
:rtype: list
|
|
1275
|
+
"""
|
|
1276
|
+
failing_items = []
|
|
1277
|
+
fail_statuses_lower = [status.lower() for status in self.FAIL_STATUSES]
|
|
1278
|
+
|
|
1279
|
+
for control_key, items in control_items.items():
|
|
1280
|
+
if control_key in self.failing_controls:
|
|
1281
|
+
for item in items:
|
|
1282
|
+
if item.compliance_result.lower() in fail_statuses_lower:
|
|
1283
|
+
failing_items.append(item)
|
|
1284
|
+
|
|
1285
|
+
return failing_items
|
|
1286
|
+
|
|
1287
|
+
def _remove_duplicate_items(self, items: list) -> list:
|
|
1288
|
+
"""
|
|
1289
|
+
Remove duplicate items while preserving order.
|
|
1290
|
+
|
|
1291
|
+
Uses resource_id and control_id combinations to create unique keys
|
|
1292
|
+
for deduplication while maintaining the original item order.
|
|
1293
|
+
|
|
1294
|
+
:param list items: List of compliance items to deduplicate
|
|
1295
|
+
:return: List of unique compliance items
|
|
1296
|
+
:rtype: list
|
|
1297
|
+
"""
|
|
1298
|
+
seen = set()
|
|
1299
|
+
unique_items = []
|
|
1300
|
+
|
|
1301
|
+
for item in items:
|
|
1302
|
+
item_key = f"{getattr(item, 'resource_id', '')}-{getattr(item, 'control_id', '')}"
|
|
1303
|
+
if item_key not in seen:
|
|
1304
|
+
seen.add(item_key)
|
|
1305
|
+
unique_items.append(item)
|
|
1306
|
+
|
|
1307
|
+
return unique_items
|
|
1308
|
+
|
|
1309
|
+
def _log_categorization_results(self) -> None:
|
|
1310
|
+
"""
|
|
1311
|
+
Log the final results of control categorization.
|
|
1312
|
+
|
|
1313
|
+
Provides summary statistics about the fail-first categorization
|
|
1314
|
+
process, including counts of passing and failing controls.
|
|
1315
|
+
|
|
1316
|
+
:return: None
|
|
1317
|
+
:rtype: None
|
|
1318
|
+
"""
|
|
1319
|
+
logger.info(
|
|
1320
|
+
f"Control categorization with fail-first logic: "
|
|
1321
|
+
f"{len(self.passing_controls)} passing controls, "
|
|
1322
|
+
f"{len(self.failing_controls)} failing controls"
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1325
|
+
def fetch_findings(self, *args, **kwargs):
|
|
1326
|
+
"""
|
|
1327
|
+
Override to create one finding per control rather than per compliance item.
|
|
1328
|
+
|
|
1329
|
+
This ensures that each failing control gets exactly one issue in RegScale,
|
|
1330
|
+
consolidating all failed compliance items for that control.
|
|
1331
|
+
"""
|
|
1332
|
+
logger.info("Fetching findings from failed controls (one per control)...")
|
|
1333
|
+
|
|
1334
|
+
processed_controls = set()
|
|
1335
|
+
findings_created = 0
|
|
1336
|
+
|
|
1337
|
+
for compliance_item in self.failed_compliance_items:
|
|
1338
|
+
control_ids = self._get_control_ids_for_item(compliance_item)
|
|
1339
|
+
|
|
1340
|
+
for control_id in control_ids:
|
|
1341
|
+
if not control_id or self._is_control_already_processed(control_id, processed_controls):
|
|
1342
|
+
continue
|
|
1343
|
+
|
|
1344
|
+
control_id_normalized = control_id.upper()
|
|
1345
|
+
processed_controls.add(control_id.lower())
|
|
1346
|
+
|
|
1347
|
+
control_failed_items = self._get_failed_items_for_control(control_id_normalized)
|
|
1348
|
+
finding = self._create_consolidated_finding_for_control(
|
|
1349
|
+
control_id=control_id_normalized, failed_items=control_failed_items
|
|
1350
|
+
)
|
|
1351
|
+
|
|
1352
|
+
if finding:
|
|
1353
|
+
findings_created += 1
|
|
1354
|
+
yield finding
|
|
1355
|
+
|
|
1356
|
+
self._log_findings_generation_summary(findings_created, len(processed_controls))
|
|
1357
|
+
|
|
1358
|
+
def _is_control_already_processed(self, control_id: str, processed_controls: set) -> bool:
|
|
1359
|
+
"""
|
|
1360
|
+
Check if control has already been processed to avoid duplicates.
|
|
1361
|
+
|
|
1362
|
+
Uses case-insensitive comparison to determine if a control has
|
|
1363
|
+
already been processed for finding generation.
|
|
1364
|
+
|
|
1365
|
+
:param str control_id: Control ID to check
|
|
1366
|
+
:param set processed_controls: Set of already processed control IDs
|
|
1367
|
+
:return: True if control has been processed, False otherwise
|
|
1368
|
+
:rtype: bool
|
|
1369
|
+
"""
|
|
1370
|
+
return control_id.lower() in processed_controls
|
|
1371
|
+
|
|
1372
|
+
def _get_failed_items_for_control(self, control_id_normalized: str) -> List[Any]:
|
|
1373
|
+
"""
|
|
1374
|
+
Get all failed compliance items for a specific control.
|
|
1375
|
+
|
|
1376
|
+
Searches through the failed compliance items to find all items
|
|
1377
|
+
that reference the specified control ID (case-insensitive).
|
|
1378
|
+
|
|
1379
|
+
:param str control_id_normalized: Control ID in normalized format
|
|
1380
|
+
:return: List of failed compliance items for the control
|
|
1381
|
+
:rtype: List[Any]
|
|
1382
|
+
"""
|
|
1383
|
+
control_failed_items = []
|
|
1384
|
+
|
|
1385
|
+
for item in self.failed_compliance_items:
|
|
1386
|
+
item_control_ids = self._get_control_ids_for_item(item)
|
|
1387
|
+
|
|
1388
|
+
if any(cid.upper() == control_id_normalized for cid in item_control_ids):
|
|
1389
|
+
control_failed_items.append(item)
|
|
1390
|
+
|
|
1391
|
+
return control_failed_items
|
|
1392
|
+
|
|
1393
|
+
def _log_findings_generation_summary(self, findings_created: int, controls_processed: int) -> None:
|
|
1394
|
+
"""
|
|
1395
|
+
Log summary of findings generation.
|
|
1396
|
+
|
|
1397
|
+
Provides statistics about the finding generation process,
|
|
1398
|
+
including number of findings created and controls processed.
|
|
1399
|
+
|
|
1400
|
+
:param int findings_created: Number of findings successfully created
|
|
1401
|
+
:param int controls_processed: Number of controls processed
|
|
1402
|
+
:return: None
|
|
1403
|
+
:rtype: None
|
|
1404
|
+
"""
|
|
1405
|
+
logger.info(
|
|
1406
|
+
f"Generated {findings_created} findings from {controls_processed} failing controls for issue processing"
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1409
|
+
def _create_consolidated_finding_for_control(self, control_id: str, failed_items: list) -> Optional[Any]:
|
|
1410
|
+
"""
|
|
1411
|
+
Create a single consolidated finding for a control with all its failed compliance items.
|
|
1412
|
+
|
|
1413
|
+
:param str control_id: The control identifier
|
|
1414
|
+
:param list failed_items: List of failed compliance items for this control
|
|
1415
|
+
:return: IntegrationFinding or None
|
|
1416
|
+
"""
|
|
1417
|
+
try:
|
|
1418
|
+
from regscale.integrations.scanner_integration import IntegrationFinding
|
|
1419
|
+
|
|
1420
|
+
if not failed_items:
|
|
1421
|
+
return None
|
|
1422
|
+
|
|
1423
|
+
representative_item = failed_items[0]
|
|
1424
|
+
resource_info = self._collect_resource_information(failed_items)
|
|
1425
|
+
severity = self._determine_highest_severity(resource_info["severities"])
|
|
1426
|
+
description = self._build_consolidated_description(control_id, resource_info)
|
|
1427
|
+
|
|
1428
|
+
severity_enum = self._map_string_severity_to_enum(severity)
|
|
1429
|
+
|
|
1430
|
+
return self._create_integration_finding(
|
|
1431
|
+
control_id=control_id,
|
|
1432
|
+
severity_enum=severity_enum,
|
|
1433
|
+
description=description,
|
|
1434
|
+
representative_item=representative_item,
|
|
1435
|
+
)
|
|
1436
|
+
|
|
1437
|
+
except Exception as e:
|
|
1438
|
+
logger.error(f"Error creating consolidated finding for control {control_id}: {e}")
|
|
1439
|
+
return None
|
|
1440
|
+
|
|
1441
|
+
def _map_severity_to_priority(self, severity: Any) -> str:
|
|
1442
|
+
"""
|
|
1443
|
+
Map severity enum to priority string.
|
|
1444
|
+
|
|
1445
|
+
Converts RegScale severity enumeration values to corresponding
|
|
1446
|
+
priority strings used in issue creation.
|
|
1447
|
+
|
|
1448
|
+
:param Any severity: Severity enum value
|
|
1449
|
+
:return: Priority string (High, Moderate, Low)
|
|
1450
|
+
:rtype: str
|
|
1451
|
+
"""
|
|
1452
|
+
# Map severity to priority
|
|
1453
|
+
if hasattr(severity, "value"):
|
|
1454
|
+
severity_value = severity.value
|
|
1455
|
+
else:
|
|
1456
|
+
severity_value = str(severity)
|
|
1457
|
+
|
|
1458
|
+
priority_map = {"Critical": "High", "High": "High", "Moderate": "Moderate", "Low": "Low"}
|
|
1459
|
+
|
|
1460
|
+
return priority_map.get(severity_value, "Low")
|
|
1461
|
+
|
|
1462
|
+
def _collect_resource_information(self, failed_items: list) -> Dict[str, Any]:
|
|
1463
|
+
"""Collect resource information from failed compliance items.
|
|
1464
|
+
|
|
1465
|
+
:param list failed_items: List of failed compliance items to process
|
|
1466
|
+
:return: Dictionary with resource information including affected_resources, severities, and descriptions
|
|
1467
|
+
:rtype: Dict[str, Any]
|
|
1468
|
+
"""
|
|
1469
|
+
affected_resources = set()
|
|
1470
|
+
severities = []
|
|
1471
|
+
descriptions = []
|
|
1472
|
+
|
|
1473
|
+
for item in failed_items:
|
|
1474
|
+
affected_resources.add(item.resource_name)
|
|
1475
|
+
if item.severity:
|
|
1476
|
+
severities.append(item.severity)
|
|
1477
|
+
descriptions.append(f"- {item.resource_name}: {item.description}")
|
|
1478
|
+
|
|
1479
|
+
return {"affected_resources": affected_resources, "severities": severities, "descriptions": descriptions}
|
|
1480
|
+
|
|
1481
|
+
def _determine_highest_severity(self, severities: List[str]) -> str:
|
|
1482
|
+
"""Determine the highest severity from a list of severities.
|
|
1483
|
+
|
|
1484
|
+
:param List[str] severities: List of severity strings to analyze
|
|
1485
|
+
:return: The highest severity found in the list
|
|
1486
|
+
:rtype: str
|
|
1487
|
+
"""
|
|
1488
|
+
severity = "HIGH" # Default
|
|
1489
|
+
if severities:
|
|
1490
|
+
severity_order = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFORMATIONAL"]
|
|
1491
|
+
for sev in severity_order:
|
|
1492
|
+
if sev in [s.upper() for s in severities]:
|
|
1493
|
+
severity = sev
|
|
1494
|
+
break
|
|
1495
|
+
return severity
|
|
1496
|
+
|
|
1497
|
+
def _build_consolidated_description(self, control_id: str, resource_info: Dict[str, Any]) -> str:
|
|
1498
|
+
"""Build consolidated description for the finding.
|
|
1499
|
+
|
|
1500
|
+
:param str control_id: The control identifier
|
|
1501
|
+
:param Dict[str, Any] resource_info: Dictionary with resource information
|
|
1502
|
+
:return: Consolidated description string for the finding
|
|
1503
|
+
:rtype: str
|
|
1504
|
+
"""
|
|
1505
|
+
affected_resources = resource_info["affected_resources"]
|
|
1506
|
+
descriptions = resource_info["descriptions"]
|
|
1507
|
+
|
|
1508
|
+
description = f"Control {control_id} failed for {len(affected_resources)} resource(s):\n\n"
|
|
1509
|
+
description += "\n".join(descriptions[:10]) # Limit to first 10 for readability
|
|
1510
|
+
|
|
1511
|
+
if len(descriptions) > 10:
|
|
1512
|
+
description += f"\n... and {len(descriptions) - 10} more resources"
|
|
1513
|
+
|
|
1514
|
+
return description
|
|
1515
|
+
|
|
1516
|
+
def _create_integration_finding(
|
|
1517
|
+
self, control_id: str, severity_enum: Any, description: str, representative_item: Any
|
|
1518
|
+
) -> Any:
|
|
1519
|
+
"""Create the IntegrationFinding object.
|
|
1520
|
+
|
|
1521
|
+
:param str control_id: The control identifier
|
|
1522
|
+
:param Any severity_enum: Severity enumeration value
|
|
1523
|
+
:param str description: Description for the finding
|
|
1524
|
+
:param Any representative_item: Representative compliance item
|
|
1525
|
+
:return: IntegrationFinding object
|
|
1526
|
+
:rtype: Any
|
|
1527
|
+
"""
|
|
1528
|
+
from regscale.integrations.scanner_integration import IntegrationFinding
|
|
1529
|
+
|
|
1530
|
+
return IntegrationFinding(
|
|
1531
|
+
control_labels=[control_id],
|
|
1532
|
+
title=f"Compliance Violation: {control_id}",
|
|
1533
|
+
category="Compliance",
|
|
1534
|
+
plugin_name=f"{self.title} Compliance Scanner - {control_id}",
|
|
1535
|
+
severity=severity_enum,
|
|
1536
|
+
description=description,
|
|
1537
|
+
status="Open",
|
|
1538
|
+
priority=self._map_severity_to_priority(severity_enum),
|
|
1539
|
+
external_id=f"{self.title.lower().replace(' ', '-')}-control-{control_id}",
|
|
1540
|
+
first_seen=self.scan_date,
|
|
1541
|
+
last_seen=self.scan_date,
|
|
1542
|
+
scan_date=self.scan_date,
|
|
1543
|
+
asset_identifier=representative_item.resource_id,
|
|
1544
|
+
vulnerability_type="Compliance Violation",
|
|
1545
|
+
rule_id=control_id,
|
|
1546
|
+
baseline=representative_item.framework,
|
|
1547
|
+
affected_controls=control_id,
|
|
1548
|
+
identification=IssueIdentification.SecurityControlAssessment.value,
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
def _create_finding_from_compliance_item(self, compliance_item: ComplianceItem) -> Optional[Any]:
|
|
1552
|
+
"""
|
|
1553
|
+
Override parent method to properly set affected_controls for multi-control items.
|
|
1554
|
+
|
|
1555
|
+
:param ComplianceItem compliance_item: The compliance item
|
|
1556
|
+
:return: Finding object or None if creation fails
|
|
1557
|
+
:rtype: Optional[Any]
|
|
1558
|
+
"""
|
|
1559
|
+
try:
|
|
1560
|
+
# Get severity mapping
|
|
1561
|
+
severity = compliance_item.severity or "Low"
|
|
1562
|
+
severity_enum = self._map_string_severity_to_enum(severity)
|
|
1563
|
+
|
|
1564
|
+
# Create the finding using the parent class structure
|
|
1565
|
+
from regscale.integrations.scanner_integration import IntegrationFinding
|
|
1566
|
+
|
|
1567
|
+
finding = IntegrationFinding(
|
|
1568
|
+
control_labels=[compliance_item.control_id],
|
|
1569
|
+
title=f"Compliance Violation: {compliance_item.control_id}",
|
|
1570
|
+
category="Compliance",
|
|
1571
|
+
plugin_name=f"{self.title} Compliance Scanner",
|
|
1572
|
+
severity=severity_enum,
|
|
1573
|
+
description=compliance_item.description,
|
|
1574
|
+
status="Open", # Use string instead of enum to avoid import issues
|
|
1575
|
+
priority=self._map_severity_to_priority(severity_enum),
|
|
1576
|
+
external_id=f"{self.title.lower()}-{compliance_item.control_id}-{compliance_item.resource_id}",
|
|
1577
|
+
first_seen=self.scan_date,
|
|
1578
|
+
last_seen=self.scan_date,
|
|
1579
|
+
scan_date=self.scan_date,
|
|
1580
|
+
asset_identifier=compliance_item.resource_id,
|
|
1581
|
+
vulnerability_type="Compliance Violation",
|
|
1582
|
+
rule_id=compliance_item.control_id,
|
|
1583
|
+
baseline=compliance_item.framework,
|
|
1584
|
+
affected_controls=compliance_item.affected_controls, # Use our property with all control IDs
|
|
1585
|
+
identification=IssueIdentification.SecurityControlAssessment.value,
|
|
1586
|
+
)
|
|
1587
|
+
|
|
1588
|
+
return finding
|
|
1589
|
+
|
|
1590
|
+
except Exception as e:
|
|
1591
|
+
logger.error(f"Error creating finding from compliance item: {e}")
|
|
1592
|
+
return None
|