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
|
@@ -5,13 +5,16 @@ import json
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
8
|
-
from
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
9
11
|
|
|
10
|
-
from regscale.core.app.utils.app_utils import check_file_path,
|
|
12
|
+
from regscale.core.app.utils.app_utils import check_file_path, error_and_exit, get_current_datetime
|
|
11
13
|
from regscale.core.utils import get_base_protocol_from_port
|
|
12
14
|
from regscale.core.utils.date import format_to_regscale_iso
|
|
13
|
-
from regscale.integrations.commercial.wizv2.
|
|
14
|
-
from regscale.integrations.commercial.wizv2.
|
|
15
|
+
from regscale.integrations.commercial.wizv2.core.client import run_async_queries
|
|
16
|
+
from regscale.integrations.commercial.wizv2.core.file_operations import FileOperations
|
|
17
|
+
from regscale.integrations.commercial.wizv2.core.constants import (
|
|
15
18
|
END_OF_LIFE_FILE_PATH,
|
|
16
19
|
EXTERNAL_ATTACK_SURFACE_FILE_PATH,
|
|
17
20
|
INVENTORY_FILE_PATH,
|
|
@@ -41,11 +44,16 @@ from regscale.integrations.commercial.wizv2.utils import (
|
|
|
41
44
|
map_category,
|
|
42
45
|
)
|
|
43
46
|
from regscale.integrations.commercial.wizv2.variables import WizVariables
|
|
44
|
-
from regscale.integrations.commercial.wizv2.wiz_auth import wiz_authenticate
|
|
45
|
-
from regscale.integrations.scanner_integration import IntegrationAsset, IntegrationFinding, ScannerIntegration
|
|
46
47
|
from regscale.integrations.variables import ScannerVariables
|
|
48
|
+
from regscale.integrations.commercial.wizv2.core.auth import wiz_authenticate
|
|
49
|
+
from regscale.integrations.scanner_integration import (
|
|
50
|
+
IntegrationAsset,
|
|
51
|
+
IntegrationFinding,
|
|
52
|
+
ScannerIntegration,
|
|
53
|
+
)
|
|
47
54
|
from regscale.models import IssueStatus, regscale_models
|
|
48
55
|
from regscale.models.regscale_models.compliance_settings import ComplianceSettings
|
|
56
|
+
from regscale.models.regscale_models.regscale_model import RegScaleModel
|
|
49
57
|
|
|
50
58
|
logger = logging.getLogger("regscale")
|
|
51
59
|
|
|
@@ -61,11 +69,21 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
61
69
|
"High": regscale_models.IssueSeverity.High,
|
|
62
70
|
"Medium": regscale_models.IssueSeverity.Moderate,
|
|
63
71
|
"Low": regscale_models.IssueSeverity.Low,
|
|
72
|
+
"INFORMATIONAL": regscale_models.IssueSeverity.NotAssigned,
|
|
73
|
+
"INFO": regscale_models.IssueSeverity.NotAssigned, # Wiz uses "INFO" for informational data findings
|
|
74
|
+
"None": regscale_models.IssueSeverity.NotAssigned, # Wiz uses "NONE" for findings without severity
|
|
64
75
|
}
|
|
65
76
|
asset_lookup = "vulnerableAsset"
|
|
66
77
|
wiz_token = None
|
|
67
78
|
_compliance_settings = None
|
|
68
79
|
|
|
80
|
+
def __init__(self, *args, **kwargs):
|
|
81
|
+
super().__init__(*args, **kwargs)
|
|
82
|
+
# Suppress generic asset not found errors but use enhanced diagnostics instead
|
|
83
|
+
self.suppress_asset_not_found_errors = True
|
|
84
|
+
# Track unique missing asset types for summary reporting
|
|
85
|
+
self._missing_asset_types = set()
|
|
86
|
+
|
|
69
87
|
@staticmethod
|
|
70
88
|
def get_variables() -> Dict[str, Any]:
|
|
71
89
|
"""
|
|
@@ -79,6 +97,31 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
79
97
|
"filterBy": {},
|
|
80
98
|
}
|
|
81
99
|
|
|
100
|
+
def get_finding_identifier(self, finding) -> str:
|
|
101
|
+
"""
|
|
102
|
+
Gets the finding identifier for Wiz findings.
|
|
103
|
+
For Wiz integrations, prioritize external_id since plugin_id can be non-unique.
|
|
104
|
+
|
|
105
|
+
:param finding: The finding
|
|
106
|
+
:return: The finding identifier
|
|
107
|
+
:rtype: str
|
|
108
|
+
"""
|
|
109
|
+
# We could have a string truncation error platform side on IntegrationFindingId nvarchar(450)
|
|
110
|
+
prefix = f"{self.plan_id}:"
|
|
111
|
+
|
|
112
|
+
# For Wiz, prioritize external_id since plugin_id can be non-unique
|
|
113
|
+
if finding.external_id:
|
|
114
|
+
prefix += self.hash_string(finding.external_id).__str__()
|
|
115
|
+
else:
|
|
116
|
+
prefix += (
|
|
117
|
+
finding.cve or finding.plugin_id or finding.rule_id or self.hash_string(finding.external_id).__str__()
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if ScannerVariables.issueCreation.lower() == "perasset":
|
|
121
|
+
res = f"{prefix}:{finding.asset_identifier}"
|
|
122
|
+
return res[:450]
|
|
123
|
+
return prefix[:450]
|
|
124
|
+
|
|
82
125
|
def authenticate(self, client_id: Optional[str] = None, client_secret: Optional[str] = None) -> None:
|
|
83
126
|
"""
|
|
84
127
|
Authenticates to Wiz using the client ID and client secret
|
|
@@ -92,14 +135,16 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
92
135
|
logger.info("Authenticating to Wiz...")
|
|
93
136
|
self.wiz_token = wiz_authenticate(client_id, client_secret)
|
|
94
137
|
|
|
95
|
-
def get_query_types(self, project_id: str) -> List[Dict[str, Any]]:
|
|
138
|
+
def get_query_types(self, project_id: str, filter_by: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
|
96
139
|
"""Get the query types for vulnerability scanning.
|
|
97
140
|
|
|
98
141
|
:param str project_id: The project ID to get queries for
|
|
142
|
+
:param Optional[Dict[str, Any]] filter_by: Optional filter criteria (used by subclasses)
|
|
99
143
|
:return: List of query types
|
|
100
144
|
:rtype: List[Dict[str, Any]]
|
|
101
145
|
"""
|
|
102
|
-
|
|
146
|
+
# Base class ignores filter_by, subclasses can override to use it
|
|
147
|
+
return get_wiz_vulnerability_queries(project_id=project_id, filter_by=filter_by)
|
|
103
148
|
|
|
104
149
|
def fetch_findings(self, *args, **kwargs) -> Iterator[IntegrationFinding]:
|
|
105
150
|
"""
|
|
@@ -119,7 +164,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
119
164
|
# Use async concurrent queries for better performance
|
|
120
165
|
yield from self.fetch_findings_async(*args, **kwargs)
|
|
121
166
|
except Exception as e:
|
|
122
|
-
logger.warning(f"Async query failed, falling back to sync: {
|
|
167
|
+
logger.warning(f"Async query failed, falling back to sync: {e!s}")
|
|
123
168
|
# Fallback to synchronous method
|
|
124
169
|
yield from self.fetch_findings_sync(**kwargs)
|
|
125
170
|
else:
|
|
@@ -195,8 +240,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
195
240
|
progress_tracker=self.finding_progress,
|
|
196
241
|
max_concurrent=5,
|
|
197
242
|
)
|
|
198
|
-
|
|
199
|
-
return self._load_cached_data_with_progress(query_configs)
|
|
243
|
+
return self._load_cached_data_with_progress(query_configs)
|
|
200
244
|
|
|
201
245
|
def _process_query_results(
|
|
202
246
|
self,
|
|
@@ -215,35 +259,63 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
215
259
|
:yield: IntegrationFinding objects
|
|
216
260
|
:rtype: Iterator[IntegrationFinding]
|
|
217
261
|
"""
|
|
218
|
-
parse_task = self.
|
|
262
|
+
parse_task = self._init_progress_task(len(results))
|
|
219
263
|
|
|
220
264
|
for query_type_str, nodes, error in results:
|
|
221
265
|
if error:
|
|
222
266
|
logger.error(f"Error fetching {query_type_str}: {error}")
|
|
223
|
-
self.
|
|
267
|
+
self._advance_progress(parse_task)
|
|
224
268
|
continue
|
|
225
269
|
|
|
226
|
-
# Find corresponding vulnerability type and config
|
|
227
270
|
vulnerability_type, config = self._find_vulnerability_config(query_type_str, query_configs)
|
|
228
271
|
if not vulnerability_type or not config:
|
|
229
272
|
logger.warning(f"Could not find vulnerability type for {query_type_str}")
|
|
230
|
-
self.
|
|
273
|
+
self._advance_progress(parse_task)
|
|
231
274
|
continue
|
|
232
275
|
|
|
233
|
-
# Save fetched data to cache if fresh data was fetched
|
|
234
276
|
if should_fetch_fresh and nodes:
|
|
235
277
|
self._save_data_to_cache(nodes, config.get("file_path"))
|
|
236
278
|
|
|
237
|
-
# Apply project filtering for certain vulnerability types
|
|
238
279
|
nodes = self._apply_project_filtering(nodes, vulnerability_type, project_id, query_type_str)
|
|
239
280
|
|
|
240
281
|
logger.info(f"Processing {len(nodes)} {query_type_str} findings...")
|
|
241
282
|
yield from self.parse_findings(nodes, vulnerability_type)
|
|
283
|
+
self._advance_progress(parse_task)
|
|
284
|
+
|
|
285
|
+
self._finalize_progress(parse_task)
|
|
286
|
+
|
|
287
|
+
def _init_progress_task(self, total_results: int):
|
|
288
|
+
"""
|
|
289
|
+
Initialize progress tracking task.
|
|
290
|
+
|
|
291
|
+
:param int total_results: Total number of results to process
|
|
292
|
+
:return: Task ID or None
|
|
293
|
+
"""
|
|
294
|
+
if self.finding_progress is not None and hasattr(self.finding_progress, "add_task"):
|
|
295
|
+
return self.finding_progress.add_task("[magenta]Processing fetched findings...", total=total_results)
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
def _advance_progress(self, parse_task) -> None:
|
|
299
|
+
"""
|
|
300
|
+
Advance progress bar by one step.
|
|
301
|
+
|
|
302
|
+
:param parse_task: Progress task ID
|
|
303
|
+
:rtype: None
|
|
304
|
+
"""
|
|
305
|
+
if parse_task is not None and self.finding_progress is not None and hasattr(self.finding_progress, "advance"):
|
|
242
306
|
self.finding_progress.advance(parse_task, 1)
|
|
243
307
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
308
|
+
def _finalize_progress(self, parse_task) -> None:
|
|
309
|
+
"""
|
|
310
|
+
Finalize progress tracking with completion message.
|
|
311
|
+
|
|
312
|
+
:param parse_task: Progress task ID
|
|
313
|
+
:rtype: None
|
|
314
|
+
"""
|
|
315
|
+
if parse_task is not None and self.finding_progress is not None and hasattr(self.finding_progress, "update"):
|
|
316
|
+
self.finding_progress.update(
|
|
317
|
+
parse_task, description=f"[green]✓ Processed all findings ({self.num_findings_to_process} total)"
|
|
318
|
+
)
|
|
247
319
|
|
|
248
320
|
def _find_vulnerability_config(
|
|
249
321
|
self, query_type_str: str, query_configs: List[Dict[str, Any]]
|
|
@@ -309,11 +381,16 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
309
381
|
# Step 2: Initialize progress tracking
|
|
310
382
|
logger.info("Fetching Wiz findings using async concurrent queries...")
|
|
311
383
|
self.num_findings_to_process = 0
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
384
|
+
# Pass filter_by_override if provided
|
|
385
|
+
filter_by = kwargs.get("filter_by_override")
|
|
386
|
+
query_configs = self.get_query_types(project_id=project_id, filter_by=filter_by)
|
|
387
|
+
|
|
388
|
+
# Backwards compatibility: check if finding_progress exists and has add_task method
|
|
389
|
+
main_task = None
|
|
390
|
+
if self.finding_progress is not None and hasattr(self.finding_progress, "add_task"):
|
|
391
|
+
main_task = self.finding_progress.add_task(
|
|
392
|
+
"[cyan]Running concurrent GraphQL queries...", total=len(query_configs)
|
|
393
|
+
)
|
|
317
394
|
|
|
318
395
|
# Step 3: Setup authentication
|
|
319
396
|
headers = self._setup_authentication_headers()
|
|
@@ -323,22 +400,37 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
323
400
|
should_fetch_fresh = self._should_fetch_fresh_data(query_configs)
|
|
324
401
|
|
|
325
402
|
# Step 5: Update main progress
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
403
|
+
# Backwards compatibility: check if finding_progress exists and has update method
|
|
404
|
+
if main_task is not None and self.finding_progress is not None and hasattr(self.finding_progress, "update"):
|
|
405
|
+
self.finding_progress.update(
|
|
406
|
+
main_task,
|
|
407
|
+
description="[green]✓ Completed all concurrent queries",
|
|
408
|
+
completed=len(query_configs),
|
|
409
|
+
total=len(query_configs),
|
|
410
|
+
)
|
|
332
411
|
|
|
333
412
|
# Step 6: Process results
|
|
334
413
|
yield from self._process_query_results(results, query_configs, project_id, should_fetch_fresh)
|
|
335
414
|
|
|
336
|
-
# Step 7: Complete main task
|
|
337
|
-
|
|
415
|
+
# Step 7: Complete main task - ensure it's marked as 100% complete
|
|
416
|
+
# Backwards compatibility: check if finding_progress exists and has update method
|
|
417
|
+
if main_task is not None and self.finding_progress is not None and hasattr(self.finding_progress, "update"):
|
|
418
|
+
self.finding_progress.update(
|
|
419
|
+
main_task,
|
|
420
|
+
description="[green]✓ Completed processing all Wiz findings",
|
|
421
|
+
completed=len(query_configs),
|
|
422
|
+
total=len(query_configs),
|
|
423
|
+
)
|
|
338
424
|
|
|
339
425
|
except Exception as e:
|
|
340
|
-
logger.error(f"Error in async findings fetch: {
|
|
341
|
-
if
|
|
426
|
+
logger.error(f"Error in async findings fetch: {e!s}", exc_info=True)
|
|
427
|
+
# Backwards compatibility: check if finding_progress exists and has update method
|
|
428
|
+
if (
|
|
429
|
+
"main_task" in locals()
|
|
430
|
+
and main_task is not None
|
|
431
|
+
and self.finding_progress is not None
|
|
432
|
+
and hasattr(self.finding_progress, "update")
|
|
433
|
+
):
|
|
342
434
|
self.finding_progress.update(
|
|
343
435
|
main_task, description=f"[red]✗ Error in concurrent queries: {str(e)[:50]}..."
|
|
344
436
|
)
|
|
@@ -346,6 +438,14 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
346
438
|
logger.info("Falling back to synchronous query method...")
|
|
347
439
|
yield from self.fetch_findings_sync(**kwargs)
|
|
348
440
|
|
|
441
|
+
# Log summary of missing asset types if any were found
|
|
442
|
+
if hasattr(self, "_missing_asset_types") and self._missing_asset_types:
|
|
443
|
+
logger.warning(
|
|
444
|
+
"Summary: Found references to missing asset types: %s. "
|
|
445
|
+
"Consider adding these to RECOMMENDED_WIZ_INVENTORY_TYPES in constants.py",
|
|
446
|
+
", ".join(sorted(self._missing_asset_types)),
|
|
447
|
+
)
|
|
448
|
+
|
|
349
449
|
logger.info(
|
|
350
450
|
"Finished async fetching Wiz findings. Total findings to process: %d", self.num_findings_to_process or 0
|
|
351
451
|
)
|
|
@@ -386,34 +486,28 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
386
486
|
:return: Results in the same format as async queries
|
|
387
487
|
:rtype: List[Tuple[str, List[Dict[str, Any]], Optional[Exception]]]
|
|
388
488
|
"""
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
self.finding_progress.advance(cache_task, 1)
|
|
413
|
-
|
|
414
|
-
self.finding_progress.update(
|
|
415
|
-
cache_task, description=f"[green]✓ Loaded cached data for {len(query_configs)} query types"
|
|
416
|
-
)
|
|
489
|
+
# Backwards compatibility: check if finding_progress exists and has add_task method
|
|
490
|
+
cache_task = None
|
|
491
|
+
if self.finding_progress is not None and hasattr(self.finding_progress, "add_task"):
|
|
492
|
+
cache_task = self.finding_progress.add_task("[green]Loading cached Wiz data...", total=len(query_configs))
|
|
493
|
+
|
|
494
|
+
def progress_callback(query_type: str, status: str):
|
|
495
|
+
if status == "loaded":
|
|
496
|
+
# Backwards compatibility: check if finding_progress exists and has advance method
|
|
497
|
+
if (
|
|
498
|
+
cache_task is not None
|
|
499
|
+
and self.finding_progress is not None
|
|
500
|
+
and hasattr(self.finding_progress, "advance")
|
|
501
|
+
):
|
|
502
|
+
self.finding_progress.advance(cache_task, 1)
|
|
503
|
+
|
|
504
|
+
results = FileOperations.load_cached_findings(query_configs, progress_callback)
|
|
505
|
+
|
|
506
|
+
# Backwards compatibility: check if finding_progress exists and has update method
|
|
507
|
+
if cache_task is not None and self.finding_progress is not None and hasattr(self.finding_progress, "update"):
|
|
508
|
+
self.finding_progress.update(
|
|
509
|
+
cache_task, description=f"[green]✓ Loaded cached data for {len(query_configs)} query types"
|
|
510
|
+
)
|
|
417
511
|
|
|
418
512
|
return results
|
|
419
513
|
|
|
@@ -428,21 +522,10 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
428
522
|
if not file_path:
|
|
429
523
|
return
|
|
430
524
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
# Ensure directory exists
|
|
435
|
-
check_file_path(os.path.dirname(file_path))
|
|
436
|
-
|
|
437
|
-
# Save data to file
|
|
438
|
-
with open(file_path, "w", encoding="utf-8") as file:
|
|
439
|
-
json.dump(nodes, file)
|
|
440
|
-
|
|
525
|
+
success = FileOperations.save_json_file(nodes, file_path, create_dir=True)
|
|
526
|
+
if success:
|
|
441
527
|
logger.debug(f"Saved {len(nodes)} nodes to cache file: {file_path}")
|
|
442
528
|
|
|
443
|
-
except Exception as e:
|
|
444
|
-
logger.warning(f"Failed to save data to cache file {file_path}: {e}")
|
|
445
|
-
|
|
446
529
|
def fetch_findings_sync(self, **kwargs) -> Iterator[IntegrationFinding]:
|
|
447
530
|
"""
|
|
448
531
|
Original synchronous method for fetching findings (renamed for fallback)
|
|
@@ -450,81 +533,214 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
450
533
|
:return: Results in the same format as async queries
|
|
451
534
|
:rtype: Iterator[IntegrationFinding]
|
|
452
535
|
"""
|
|
453
|
-
# Use shared validation logic
|
|
454
536
|
project_id = self._validate_project_id(kwargs.get("wiz_project_id"))
|
|
455
|
-
|
|
456
537
|
logger.info("Fetching Wiz findings using synchronous queries...")
|
|
457
538
|
self.num_findings_to_process = 0
|
|
458
|
-
query_types = self.get_query_types(project_id=project_id)
|
|
459
539
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
)
|
|
540
|
+
filter_by = kwargs.get("filter_by_override")
|
|
541
|
+
query_types = self.get_query_types(project_id=project_id, filter_by=filter_by)
|
|
542
|
+
|
|
543
|
+
main_task = self._create_main_progress_task(len(query_types))
|
|
464
544
|
|
|
465
545
|
for i, wiz_vulnerability_type in enumerate(query_types, 1):
|
|
466
|
-
|
|
546
|
+
yield from self._process_single_query_type(
|
|
547
|
+
wiz_vulnerability_type, project_id, i, len(query_types), main_task
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
self._complete_main_progress_task(main_task)
|
|
551
|
+
self._log_missing_asset_types_summary()
|
|
552
|
+
|
|
553
|
+
def _create_main_progress_task(self, total_query_types: int):
|
|
554
|
+
"""
|
|
555
|
+
Create main progress tracking task.
|
|
556
|
+
|
|
557
|
+
:param int total_query_types: Total number of query types
|
|
558
|
+
:return: Task ID or None
|
|
559
|
+
"""
|
|
560
|
+
if self.finding_progress is not None and hasattr(self.finding_progress, "add_task"):
|
|
561
|
+
return self.finding_progress.add_task(
|
|
562
|
+
"[cyan]Fetching Wiz findings across all query types...", total=total_query_types
|
|
563
|
+
)
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
def _process_single_query_type(
|
|
567
|
+
self, wiz_vulnerability_type: dict, project_id: str, step: int, total_steps: int, main_task
|
|
568
|
+
) -> Iterator[IntegrationFinding]:
|
|
569
|
+
"""
|
|
570
|
+
Process a single query type and yield findings.
|
|
571
|
+
|
|
572
|
+
:param dict wiz_vulnerability_type: Query type configuration
|
|
573
|
+
:param str project_id: Project ID for filtering
|
|
574
|
+
:param int step: Current step number
|
|
575
|
+
:param int total_steps: Total number of steps
|
|
576
|
+
:param main_task: Main progress task ID
|
|
577
|
+
:yield: IntegrationFinding objects
|
|
578
|
+
:rtype: Iterator[IntegrationFinding]
|
|
579
|
+
"""
|
|
580
|
+
vulnerability_name = self._get_friendly_vulnerability_name(wiz_vulnerability_type["type"])
|
|
581
|
+
self._update_main_progress_description(main_task, step, total_steps, vulnerability_name)
|
|
467
582
|
|
|
468
|
-
|
|
583
|
+
query_task = self._create_query_task(vulnerability_name)
|
|
584
|
+
nodes = self._fetch_query_data(wiz_vulnerability_type, vulnerability_name, query_task)
|
|
585
|
+
nodes = self._apply_query_filtering(wiz_vulnerability_type, nodes, project_id, vulnerability_name)
|
|
586
|
+
|
|
587
|
+
if nodes:
|
|
588
|
+
yield from self._parse_query_results(nodes, wiz_vulnerability_type["type"], vulnerability_name)
|
|
589
|
+
|
|
590
|
+
self._complete_query_tasks(query_task, main_task)
|
|
591
|
+
|
|
592
|
+
def _update_main_progress_description(
|
|
593
|
+
self, main_task, step: int, total_steps: int, vulnerability_name: str
|
|
594
|
+
) -> None:
|
|
595
|
+
"""
|
|
596
|
+
Update main progress task description.
|
|
597
|
+
|
|
598
|
+
:param main_task: Main task ID
|
|
599
|
+
:param int step: Current step
|
|
600
|
+
:param int total_steps: Total steps
|
|
601
|
+
:param str vulnerability_name: Vulnerability name
|
|
602
|
+
:rtype: None
|
|
603
|
+
"""
|
|
604
|
+
if main_task is not None and self.finding_progress is not None and hasattr(self.finding_progress, "update"):
|
|
469
605
|
self.finding_progress.update(
|
|
470
|
-
main_task, description=f"[cyan]Step {
|
|
606
|
+
main_task, description=f"[cyan]Step {step}/{total_steps}: Fetching {vulnerability_name}..."
|
|
471
607
|
)
|
|
472
608
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
609
|
+
def _create_query_task(self, vulnerability_name: str):
|
|
610
|
+
"""
|
|
611
|
+
Create query-specific progress task.
|
|
612
|
+
|
|
613
|
+
:param str vulnerability_name: Vulnerability name
|
|
614
|
+
:return: Task ID or None
|
|
615
|
+
"""
|
|
616
|
+
if self.finding_progress is not None and hasattr(self.finding_progress, "add_task"):
|
|
617
|
+
return self.finding_progress.add_task(f"[yellow]Querying Wiz API for {vulnerability_name}...", total=None)
|
|
618
|
+
return None
|
|
619
|
+
|
|
620
|
+
def _fetch_query_data(self, wiz_vulnerability_type: dict, vulnerability_name: str, query_task) -> list:
|
|
621
|
+
"""
|
|
622
|
+
Fetch data for a single query type.
|
|
623
|
+
|
|
624
|
+
:param dict wiz_vulnerability_type: Query type configuration
|
|
625
|
+
:param str vulnerability_name: Vulnerability name
|
|
626
|
+
:param query_task: Query task ID
|
|
627
|
+
:return: List of nodes
|
|
628
|
+
:rtype: list
|
|
629
|
+
"""
|
|
630
|
+
variables = wiz_vulnerability_type.get("variables", self.get_variables())
|
|
631
|
+
nodes = self.fetch_wiz_data_if_needed(
|
|
632
|
+
query=wiz_vulnerability_type["query"],
|
|
633
|
+
variables=variables,
|
|
634
|
+
topic_key=wiz_vulnerability_type["topic_key"],
|
|
635
|
+
file_path=wiz_vulnerability_type["file_path"],
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
if query_task is not None and self.finding_progress is not None and hasattr(self.finding_progress, "update"):
|
|
639
|
+
self.finding_progress.update(
|
|
640
|
+
query_task, description=f"[green]✓ Fetched {len(nodes)} {vulnerability_name} from Wiz API"
|
|
476
641
|
)
|
|
477
642
|
|
|
478
|
-
|
|
479
|
-
variables = wiz_vulnerability_type.get("variables", self.get_variables())
|
|
643
|
+
return nodes
|
|
480
644
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
645
|
+
def _apply_query_filtering(
|
|
646
|
+
self, wiz_vulnerability_type: dict, nodes: list, project_id: str, vulnerability_name: str
|
|
647
|
+
) -> list:
|
|
648
|
+
"""
|
|
649
|
+
Apply project filtering if needed for the query type.
|
|
650
|
+
|
|
651
|
+
:param dict wiz_vulnerability_type: Query type configuration
|
|
652
|
+
:param list nodes: Nodes to filter
|
|
653
|
+
:param str project_id: Project ID
|
|
654
|
+
:param str vulnerability_name: Vulnerability name
|
|
655
|
+
:return: Filtered nodes
|
|
656
|
+
:rtype: list
|
|
657
|
+
"""
|
|
658
|
+
if wiz_vulnerability_type["type"] not in [
|
|
659
|
+
WizVulnerabilityType.EXCESSIVE_ACCESS_FINDING,
|
|
660
|
+
WizVulnerabilityType.NETWORK_EXPOSURE_FINDING,
|
|
661
|
+
WizVulnerabilityType.EXTERNAL_ATTACH_SURFACE,
|
|
662
|
+
]:
|
|
663
|
+
return nodes
|
|
664
|
+
|
|
665
|
+
filter_task = None
|
|
666
|
+
if self.finding_progress is not None and hasattr(self.finding_progress, "add_task"):
|
|
667
|
+
filter_task = self.finding_progress.add_task(
|
|
668
|
+
f"[blue]Filtering {vulnerability_name} by project...", total=len(nodes)
|
|
486
669
|
)
|
|
487
670
|
|
|
488
|
-
|
|
671
|
+
nodes = self._filter_findings_by_project_with_progress(nodes, project_id, filter_task)
|
|
672
|
+
|
|
673
|
+
if filter_task is not None and self.finding_progress is not None and hasattr(self.finding_progress, "update"):
|
|
489
674
|
self.finding_progress.update(
|
|
490
|
-
|
|
675
|
+
filter_task, description=f"[green]✓ Filtered to {len(nodes)} {vulnerability_name} for project"
|
|
491
676
|
)
|
|
492
677
|
|
|
493
|
-
|
|
494
|
-
if wiz_vulnerability_type["type"] in [
|
|
495
|
-
WizVulnerabilityType.EXCESSIVE_ACCESS_FINDING,
|
|
496
|
-
WizVulnerabilityType.NETWORK_EXPOSURE_FINDING,
|
|
497
|
-
WizVulnerabilityType.EXTERNAL_ATTACH_SURFACE,
|
|
498
|
-
]:
|
|
499
|
-
filter_task = self.finding_progress.add_task(
|
|
500
|
-
f"[blue]Filtering {vulnerability_name} by project...", total=len(nodes)
|
|
501
|
-
)
|
|
502
|
-
nodes = self._filter_findings_by_project_with_progress(nodes, project_id, filter_task)
|
|
503
|
-
self.finding_progress.update(
|
|
504
|
-
filter_task, description=f"[green]✓ Filtered to {len(nodes)} {vulnerability_name} for project"
|
|
505
|
-
)
|
|
678
|
+
return nodes
|
|
506
679
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
680
|
+
def _parse_query_results(
|
|
681
|
+
self, nodes: list, vulnerability_type, vulnerability_name: str
|
|
682
|
+
) -> Iterator[IntegrationFinding]:
|
|
683
|
+
"""
|
|
684
|
+
Parse nodes and yield findings.
|
|
512
685
|
|
|
513
|
-
|
|
686
|
+
:param list nodes: Nodes to parse
|
|
687
|
+
:param vulnerability_type: Vulnerability type
|
|
688
|
+
:param str vulnerability_name: Vulnerability name
|
|
689
|
+
:yield: IntegrationFinding objects
|
|
690
|
+
:rtype: Iterator[IntegrationFinding]
|
|
691
|
+
"""
|
|
692
|
+
parse_task = None
|
|
693
|
+
if self.finding_progress is not None and hasattr(self.finding_progress, "add_task"):
|
|
694
|
+
parse_task = self.finding_progress.add_task(
|
|
695
|
+
f"[magenta]Parsing {len(nodes)} {vulnerability_name}...", total=len(nodes)
|
|
696
|
+
)
|
|
514
697
|
|
|
515
|
-
|
|
516
|
-
parse_task, description=f"[green]✓ Parsed {len(nodes)} {vulnerability_name} successfully"
|
|
517
|
-
)
|
|
698
|
+
yield from self.parse_findings_with_progress(nodes, vulnerability_type, parse_task)
|
|
518
699
|
|
|
519
|
-
|
|
700
|
+
if parse_task is not None and self.finding_progress is not None and hasattr(self.finding_progress, "update"):
|
|
701
|
+
self.finding_progress.update(
|
|
702
|
+
parse_task, description=f"[green]✓ Parsed {len(nodes)} {vulnerability_name} successfully"
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
def _complete_query_tasks(self, query_task, main_task) -> None:
|
|
706
|
+
"""
|
|
707
|
+
Mark query and main tasks as progressing.
|
|
708
|
+
|
|
709
|
+
:param query_task: Query task ID
|
|
710
|
+
:param main_task: Main task ID
|
|
711
|
+
:rtype: None
|
|
712
|
+
"""
|
|
713
|
+
if query_task is not None and self.finding_progress is not None and hasattr(self.finding_progress, "update"):
|
|
520
714
|
self.finding_progress.update(query_task, completed=1, total=1)
|
|
715
|
+
|
|
716
|
+
if main_task is not None and self.finding_progress is not None and hasattr(self.finding_progress, "advance"):
|
|
521
717
|
self.finding_progress.advance(main_task, 1)
|
|
522
718
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
719
|
+
def _complete_main_progress_task(self, main_task) -> None:
|
|
720
|
+
"""
|
|
721
|
+
Complete main progress task with final message.
|
|
722
|
+
|
|
723
|
+
:param main_task: Main task ID
|
|
724
|
+
:rtype: None
|
|
725
|
+
"""
|
|
726
|
+
if main_task is not None and self.finding_progress is not None and hasattr(self.finding_progress, "update"):
|
|
727
|
+
self.finding_progress.update(
|
|
728
|
+
main_task,
|
|
729
|
+
description=f"[green]✓ Completed fetching all Wiz findings ({self.num_findings_to_process or 0} total)",
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
def _log_missing_asset_types_summary(self) -> None:
|
|
733
|
+
"""
|
|
734
|
+
Log summary of missing asset types if any were found.
|
|
735
|
+
|
|
736
|
+
:rtype: None
|
|
737
|
+
"""
|
|
738
|
+
if hasattr(self, "_missing_asset_types") and self._missing_asset_types:
|
|
739
|
+
logger.warning(
|
|
740
|
+
"Summary: Found references to missing asset types: %s. "
|
|
741
|
+
"Consider adding these to RECOMMENDED_WIZ_INVENTORY_TYPES in constants.py",
|
|
742
|
+
", ".join(sorted(self._missing_asset_types)),
|
|
743
|
+
)
|
|
528
744
|
|
|
529
745
|
logger.info(
|
|
530
746
|
"Finished synchronous fetching Wiz findings. Total findings to process: %d",
|
|
@@ -574,7 +790,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
574
790
|
yield finding
|
|
575
791
|
|
|
576
792
|
# Update progress if task_id provided
|
|
577
|
-
if
|
|
793
|
+
# Backwards compatibility: check if finding_progress exists and has advance method
|
|
794
|
+
if task_id is not None and self.finding_progress is not None and hasattr(self.finding_progress, "advance"):
|
|
578
795
|
self.finding_progress.advance(task_id, 1)
|
|
579
796
|
|
|
580
797
|
# Log parsing results for this type
|
|
@@ -612,7 +829,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
612
829
|
filtered_nodes.append(node)
|
|
613
830
|
|
|
614
831
|
# Update progress if task_id provided
|
|
615
|
-
if
|
|
832
|
+
# Backwards compatibility: check if finding_progress exists and has advance method
|
|
833
|
+
if task_id is not None and self.finding_progress is not None and hasattr(self.finding_progress, "advance"):
|
|
616
834
|
self.finding_progress.advance(task_id, 1)
|
|
617
835
|
|
|
618
836
|
filtered_count = len(filtered_nodes)
|
|
@@ -660,16 +878,233 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
660
878
|
) -> Iterator[IntegrationFinding]:
|
|
661
879
|
"""
|
|
662
880
|
Parses a list of Wiz finding nodes into IntegrationFinding objects.
|
|
663
|
-
|
|
664
|
-
This is a compatibility wrapper that calls the progress-aware version.
|
|
881
|
+
Groups findings by rule and scope for consolidation when appropriate.
|
|
665
882
|
|
|
666
883
|
:param List[Dict[str, Any]] nodes: List of Wiz finding nodes
|
|
667
884
|
:param WizVulnerabilityType vulnerability_type: The type of vulnerability
|
|
668
885
|
:yield: IntegrationFinding objects
|
|
669
886
|
:rtype: Iterator[IntegrationFinding]
|
|
670
887
|
"""
|
|
671
|
-
|
|
672
|
-
|
|
888
|
+
logger.debug(f"VULNERABILITY PROCESSING ANALYSIS: Received {len(nodes)} raw Wiz vulnerabilities for processing")
|
|
889
|
+
|
|
890
|
+
# Count issues by severity for analysis
|
|
891
|
+
severity_counts: dict[str, int] = {}
|
|
892
|
+
status_counts: dict[str, int] = {}
|
|
893
|
+
for node in nodes:
|
|
894
|
+
severity = node.get("severity", "Low")
|
|
895
|
+
status = node.get("status", "OPEN")
|
|
896
|
+
severity_counts[severity] = severity_counts.get(severity, 0) + 1
|
|
897
|
+
status_counts[status] = status_counts.get(status, 0) + 1
|
|
898
|
+
|
|
899
|
+
logger.debug(f"Raw vulnerability breakdown by severity: {severity_counts}")
|
|
900
|
+
logger.debug(f"Raw vulnerability breakdown by status: {status_counts}")
|
|
901
|
+
|
|
902
|
+
# Filter nodes by minimum severity configuration
|
|
903
|
+
filtered_nodes = []
|
|
904
|
+
filtered_out_count = 0
|
|
905
|
+
for node in nodes:
|
|
906
|
+
wiz_severity = node.get("severity", "Low")
|
|
907
|
+
wiz_id = node.get("id", "unknown")
|
|
908
|
+
|
|
909
|
+
# Log sample record for NONE severity (only first occurrence per session)
|
|
910
|
+
if wiz_severity and wiz_severity.upper() == "NONE":
|
|
911
|
+
if not hasattr(self, "_none_severity_sample_logged"):
|
|
912
|
+
logger.info(
|
|
913
|
+
f"SAMPLE RECORD - Vulnerability with NONE severity (treating as informational): "
|
|
914
|
+
f"ID={node.get('id', 'Unknown')}, "
|
|
915
|
+
f"Name={node.get('name', 'Unknown')}, "
|
|
916
|
+
f"Type={node.get('type', 'Unknown')}, "
|
|
917
|
+
f"Severity={wiz_severity}"
|
|
918
|
+
)
|
|
919
|
+
self._none_severity_sample_logged = True
|
|
920
|
+
|
|
921
|
+
if self.should_process_finding_by_severity(wiz_severity):
|
|
922
|
+
filtered_nodes.append(node)
|
|
923
|
+
else:
|
|
924
|
+
filtered_out_count += 1
|
|
925
|
+
logger.debug(
|
|
926
|
+
f"FILTERED BY SEVERITY: Vulnerability {wiz_id} with severity '{wiz_severity}' "
|
|
927
|
+
f"filtered due to minimumSeverity configuration"
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
logger.info(
|
|
931
|
+
f"After severity filtering: {len(filtered_nodes)} vulnerabilities kept, {filtered_out_count} filtered out"
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
if not filtered_nodes:
|
|
935
|
+
logger.warning(
|
|
936
|
+
"All vulnerabilities filtered out by severity configuration - check your minimumSeverity setting"
|
|
937
|
+
)
|
|
938
|
+
return
|
|
939
|
+
|
|
940
|
+
# Apply consolidation logic for findings that support it
|
|
941
|
+
if self._should_apply_consolidation(vulnerability_type):
|
|
942
|
+
yield from self._parse_findings_with_consolidation(filtered_nodes, vulnerability_type)
|
|
943
|
+
else:
|
|
944
|
+
# Use original parsing for vulnerability types that shouldn't be consolidated
|
|
945
|
+
yield from self.parse_findings_with_progress(filtered_nodes, vulnerability_type, task_id=None)
|
|
946
|
+
|
|
947
|
+
def _should_apply_consolidation(self, vulnerability_type: WizVulnerabilityType) -> bool:
|
|
948
|
+
"""
|
|
949
|
+
Determine if consolidation should be applied for this vulnerability type.
|
|
950
|
+
|
|
951
|
+
:param WizVulnerabilityType vulnerability_type: The vulnerability type
|
|
952
|
+
:return: True if consolidation should be applied
|
|
953
|
+
:rtype: bool
|
|
954
|
+
"""
|
|
955
|
+
# Apply consolidation to finding types that commonly affect multiple assets
|
|
956
|
+
consolidation_types = {
|
|
957
|
+
WizVulnerabilityType.HOST_FINDING,
|
|
958
|
+
WizVulnerabilityType.DATA_FINDING,
|
|
959
|
+
WizVulnerabilityType.VULNERABILITY,
|
|
960
|
+
}
|
|
961
|
+
return vulnerability_type in consolidation_types
|
|
962
|
+
|
|
963
|
+
def _parse_findings_with_consolidation(
|
|
964
|
+
self, nodes: List[Dict[str, Any]], vulnerability_type: WizVulnerabilityType
|
|
965
|
+
) -> Iterator[IntegrationFinding]:
|
|
966
|
+
"""
|
|
967
|
+
Parse findings with consolidation logic applied.
|
|
968
|
+
|
|
969
|
+
:param List[Dict[str, Any]] nodes: List of Wiz finding nodes
|
|
970
|
+
:param WizVulnerabilityType vulnerability_type: The vulnerability type
|
|
971
|
+
:yield: Consolidated IntegrationFinding objects
|
|
972
|
+
:rtype: Iterator[IntegrationFinding]
|
|
973
|
+
"""
|
|
974
|
+
# Group nodes for potential consolidation
|
|
975
|
+
grouped_nodes = self._group_findings_for_consolidation(nodes)
|
|
976
|
+
|
|
977
|
+
# Process each group
|
|
978
|
+
for group_key, group_nodes in grouped_nodes.items():
|
|
979
|
+
if len(group_nodes) > 1:
|
|
980
|
+
# Multiple nodes with same rule - attempt consolidation
|
|
981
|
+
if consolidated_finding := self._create_consolidated_scanner_finding(group_nodes, vulnerability_type):
|
|
982
|
+
yield consolidated_finding
|
|
983
|
+
else:
|
|
984
|
+
# Single node - process normally
|
|
985
|
+
if finding := self.parse_finding(group_nodes[0], vulnerability_type):
|
|
986
|
+
yield finding
|
|
987
|
+
|
|
988
|
+
def _group_findings_for_consolidation(self, nodes: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
|
989
|
+
"""
|
|
990
|
+
Group findings by rule and appropriate scope for consolidation.
|
|
991
|
+
- Database findings: group by server
|
|
992
|
+
- App Configuration findings: group by resource group
|
|
993
|
+
- Other findings: group by full resource path
|
|
994
|
+
|
|
995
|
+
:param List[Dict[str, Any]] nodes: List of Wiz finding nodes
|
|
996
|
+
:return: Dictionary mapping group keys to lists of nodes
|
|
997
|
+
:rtype: Dict[str, List[Dict[str, Any]]]
|
|
998
|
+
"""
|
|
999
|
+
groups = {}
|
|
1000
|
+
|
|
1001
|
+
for node in nodes:
|
|
1002
|
+
# Create a grouping key based on rule and appropriate scope
|
|
1003
|
+
rule_name = self._get_rule_name_from_node(node)
|
|
1004
|
+
provider_id = self._get_provider_id_from_node(node)
|
|
1005
|
+
|
|
1006
|
+
# Determine the appropriate grouping scope based on resource type
|
|
1007
|
+
grouping_scope = self._determine_grouping_scope(provider_id, rule_name)
|
|
1008
|
+
|
|
1009
|
+
# Group key combines rule name and scope
|
|
1010
|
+
group_key = f"{rule_name}|{grouping_scope}"
|
|
1011
|
+
|
|
1012
|
+
if group_key not in groups:
|
|
1013
|
+
groups[group_key] = []
|
|
1014
|
+
groups[group_key].append(node)
|
|
1015
|
+
|
|
1016
|
+
return groups
|
|
1017
|
+
|
|
1018
|
+
def _get_rule_name_from_node(self, node: Dict[str, Any]) -> str:
|
|
1019
|
+
"""Get rule name from various node structures."""
|
|
1020
|
+
# Try different ways to get rule name
|
|
1021
|
+
if source_rule := node.get("sourceRule"):
|
|
1022
|
+
return source_rule.get("name", "")
|
|
1023
|
+
return node.get("name", node.get("title", ""))
|
|
1024
|
+
|
|
1025
|
+
def _get_provider_id_from_node(self, node: Dict[str, Any]) -> str:
|
|
1026
|
+
"""Get provider ID from various node structures."""
|
|
1027
|
+
# Try different ways to get provider ID
|
|
1028
|
+
if entity_snapshot := node.get("entitySnapshot"):
|
|
1029
|
+
return entity_snapshot.get("providerId", "")
|
|
1030
|
+
|
|
1031
|
+
# Try other asset lookup patterns
|
|
1032
|
+
asset_fields = ["vulnerableAsset", "entity", "resource", "relatedEntity", "sourceEntity", "target"]
|
|
1033
|
+
for field in asset_fields:
|
|
1034
|
+
if asset_obj := node.get(field):
|
|
1035
|
+
if provider_id := asset_obj.get("providerId"):
|
|
1036
|
+
return provider_id
|
|
1037
|
+
# For vulnerability nodes, use asset ID if providerId is not available
|
|
1038
|
+
if field == "vulnerableAsset" and (asset_id := asset_obj.get("id")):
|
|
1039
|
+
return asset_id
|
|
1040
|
+
|
|
1041
|
+
return ""
|
|
1042
|
+
|
|
1043
|
+
def _determine_grouping_scope(self, provider_id: str, rule_name: str) -> str:
|
|
1044
|
+
"""
|
|
1045
|
+
Determine the appropriate grouping scope for consolidation.
|
|
1046
|
+
|
|
1047
|
+
:param str provider_id: The provider ID
|
|
1048
|
+
:param str rule_name: The rule name
|
|
1049
|
+
:return: The grouping scope (server, resource group, or full path)
|
|
1050
|
+
:rtype: str
|
|
1051
|
+
"""
|
|
1052
|
+
# For database issues, group by server
|
|
1053
|
+
if "/databases/" in provider_id:
|
|
1054
|
+
return provider_id.split("/databases/")[0]
|
|
1055
|
+
|
|
1056
|
+
# For App Configuration issues, group by resource group to consolidate multiple stores
|
|
1057
|
+
if (
|
|
1058
|
+
"app configuration" in rule_name.lower()
|
|
1059
|
+
and "/microsoft.appconfiguration/configurationstores/" in provider_id
|
|
1060
|
+
):
|
|
1061
|
+
# Extract resource group path: /subscriptions/.../resourcegroups/rg_name
|
|
1062
|
+
parts = provider_id.split("/resourcegroups/")
|
|
1063
|
+
if len(parts) >= 2:
|
|
1064
|
+
rg_part = parts[1].split("/")[0] # Get just the resource group name
|
|
1065
|
+
return f"{parts[0]}/resourcegroups/{rg_part}"
|
|
1066
|
+
|
|
1067
|
+
# For other resources, use the full provider path (no consolidation)
|
|
1068
|
+
return provider_id
|
|
1069
|
+
|
|
1070
|
+
def _create_consolidated_scanner_finding(
|
|
1071
|
+
self, nodes: List[Dict[str, Any]], vulnerability_type: WizVulnerabilityType
|
|
1072
|
+
) -> Optional[IntegrationFinding]:
|
|
1073
|
+
"""
|
|
1074
|
+
Create a consolidated finding from multiple nodes with the same rule.
|
|
1075
|
+
|
|
1076
|
+
:param List[Dict[str, Any]] nodes: List of nodes to consolidate
|
|
1077
|
+
:param WizVulnerabilityType vulnerability_type: The vulnerability type
|
|
1078
|
+
:return: Consolidated IntegrationFinding or None
|
|
1079
|
+
:rtype: Optional[IntegrationFinding]
|
|
1080
|
+
"""
|
|
1081
|
+
# Use the first node as the base
|
|
1082
|
+
base_node = nodes[0]
|
|
1083
|
+
|
|
1084
|
+
# Collect all asset identifiers and provider IDs
|
|
1085
|
+
asset_ids = []
|
|
1086
|
+
provider_ids = []
|
|
1087
|
+
|
|
1088
|
+
for node in nodes:
|
|
1089
|
+
if asset_id := self.get_asset_id_from_node(node, vulnerability_type):
|
|
1090
|
+
asset_ids.append(asset_id)
|
|
1091
|
+
if provider_id := self.get_provider_unique_id_from_node(node, vulnerability_type):
|
|
1092
|
+
provider_ids.append(provider_id)
|
|
1093
|
+
|
|
1094
|
+
# If we couldn't extract asset info, fall back to normal parsing
|
|
1095
|
+
if not asset_ids:
|
|
1096
|
+
return self.parse_finding(base_node, vulnerability_type)
|
|
1097
|
+
|
|
1098
|
+
# Create the finding using normal parsing, then override asset identifiers
|
|
1099
|
+
base_finding = self.parse_finding(base_node, vulnerability_type)
|
|
1100
|
+
if not base_finding:
|
|
1101
|
+
return None
|
|
1102
|
+
|
|
1103
|
+
# Override with consolidated asset information
|
|
1104
|
+
base_finding.asset_identifier = asset_ids[0] # Use first asset as primary
|
|
1105
|
+
base_finding.issue_asset_identifier_value = "\n".join(provider_ids) if provider_ids else None
|
|
1106
|
+
|
|
1107
|
+
return base_finding
|
|
673
1108
|
|
|
674
1109
|
@classmethod
|
|
675
1110
|
def get_issue_severity(cls, severity: str) -> regscale_models.IssueSeverity:
|
|
@@ -682,6 +1117,44 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
682
1117
|
"""
|
|
683
1118
|
return cls.finding_severity_map.get(severity.capitalize(), regscale_models.IssueSeverity.Low)
|
|
684
1119
|
|
|
1120
|
+
def should_process_finding_by_severity(self, wiz_severity: str) -> bool:
|
|
1121
|
+
"""
|
|
1122
|
+
Check if finding should be processed based on minimum severity configuration.
|
|
1123
|
+
|
|
1124
|
+
:param str wiz_severity: The Wiz severity level (e.g., "INFORMATIONAL", "Low", "Medium", etc.)
|
|
1125
|
+
:return: True if finding should be processed, False if it should be filtered out
|
|
1126
|
+
:rtype: bool
|
|
1127
|
+
"""
|
|
1128
|
+
# Get minimum severity from configuration, default to "low"
|
|
1129
|
+
min_severity = self.app.config.get("scanners", {}).get("wiz", {}).get("minimumSeverity", "low").lower()
|
|
1130
|
+
|
|
1131
|
+
# Log the configuration being used (only once to avoid spam)
|
|
1132
|
+
if not hasattr(self, "_severity_config_logged"):
|
|
1133
|
+
logger.debug(f"SEVERITY FILTER CONFIG: minimumSeverity = '{min_severity}'")
|
|
1134
|
+
self._severity_config_logged = True
|
|
1135
|
+
|
|
1136
|
+
# Define severity hierarchy (lower index = higher severity)
|
|
1137
|
+
# Note: "info", "informational", and "none" are all treated as informational
|
|
1138
|
+
severity_hierarchy = ["critical", "high", "medium", "low", "informational", "info", "none"]
|
|
1139
|
+
|
|
1140
|
+
try:
|
|
1141
|
+
wiz_severity_lower = wiz_severity.lower()
|
|
1142
|
+
|
|
1143
|
+
# Handle empty or None severity values - treat as informational
|
|
1144
|
+
# Normalize "info" to "informational" for consistent processing
|
|
1145
|
+
if not wiz_severity_lower or wiz_severity_lower == "none" or wiz_severity_lower == "info":
|
|
1146
|
+
wiz_severity_lower = "informational"
|
|
1147
|
+
|
|
1148
|
+
min_severity_index = severity_hierarchy.index(min_severity)
|
|
1149
|
+
finding_severity_index = severity_hierarchy.index(wiz_severity_lower)
|
|
1150
|
+
|
|
1151
|
+
# Process if finding severity is equal or higher (lower index) than minimum
|
|
1152
|
+
return finding_severity_index <= min_severity_index
|
|
1153
|
+
except ValueError:
|
|
1154
|
+
# If severity not found in hierarchy, default to processing it
|
|
1155
|
+
logger.warning(f"Unknown severity level: {wiz_severity}, processing anyway")
|
|
1156
|
+
return True
|
|
1157
|
+
|
|
685
1158
|
def process_comments(self, comments_dict: Dict) -> Optional[str]:
|
|
686
1159
|
"""
|
|
687
1160
|
Processes comments from Wiz findings to match RegScale's comment format.
|
|
@@ -694,7 +1167,8 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
694
1167
|
|
|
695
1168
|
if comments := comments_dict.get("comments", {}).get("edges", []):
|
|
696
1169
|
formatted_comments = [
|
|
697
|
-
f"{edge.get('node', {}).get('author', {}).get('name', 'Unknown')}:
|
|
1170
|
+
f"{edge.get('node', {}).get('author', {}).get('name', 'Unknown')}: "
|
|
1171
|
+
f"{edge.get('node', {}).get('body', 'No comment')}"
|
|
698
1172
|
for edge in comments
|
|
699
1173
|
]
|
|
700
1174
|
# Join with newlines
|
|
@@ -712,10 +1186,10 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
712
1186
|
"""
|
|
713
1187
|
# Define asset lookup patterns for different vulnerability types
|
|
714
1188
|
asset_lookup_patterns = {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1189
|
+
WizVulnerabilityType.VULNERABILITY: "vulnerableAsset",
|
|
1190
|
+
WizVulnerabilityType.CONFIGURATION: "resource",
|
|
1191
|
+
WizVulnerabilityType.HOST_FINDING: "resource",
|
|
1192
|
+
WizVulnerabilityType.DATA_FINDING: "resource",
|
|
719
1193
|
WizVulnerabilityType.SECRET_FINDING: "resource",
|
|
720
1194
|
WizVulnerabilityType.NETWORK_EXPOSURE_FINDING: "exposedEntity",
|
|
721
1195
|
WizVulnerabilityType.END_OF_LIFE_FINDING: "vulnerableAsset",
|
|
@@ -733,8 +1207,81 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
733
1207
|
return graph_entity.get("id")
|
|
734
1208
|
|
|
735
1209
|
# Standard case - direct id access
|
|
736
|
-
asset_container = node.get(asset_lookup_key
|
|
737
|
-
|
|
1210
|
+
asset_container = node.get(asset_lookup_key) or {}
|
|
1211
|
+
asset_id = asset_container.get("id") if isinstance(asset_container, dict) else None
|
|
1212
|
+
|
|
1213
|
+
# Add debug logging to help diagnose missing assets
|
|
1214
|
+
if not asset_id:
|
|
1215
|
+
logger.debug(
|
|
1216
|
+
f"No asset ID found for {vulnerability_type.value} using key '{asset_lookup_key}'. "
|
|
1217
|
+
f"Available keys in node: {list(node.keys())}"
|
|
1218
|
+
)
|
|
1219
|
+
# Try alternative lookup patterns as fallback
|
|
1220
|
+
fallback_keys = ["vulnerableAsset", "resource", "exposedEntity", "entitySnapshot"]
|
|
1221
|
+
for fallback_key in fallback_keys:
|
|
1222
|
+
if fallback_key != asset_lookup_key and fallback_key in node:
|
|
1223
|
+
fallback_asset = node.get(fallback_key) or {}
|
|
1224
|
+
if isinstance(fallback_asset, dict) and (fallback_id := fallback_asset.get("id")):
|
|
1225
|
+
logger.debug(
|
|
1226
|
+
f"Found asset ID using fallback key '{fallback_key}' for {vulnerability_type.value}"
|
|
1227
|
+
)
|
|
1228
|
+
return fallback_id
|
|
1229
|
+
|
|
1230
|
+
return asset_id
|
|
1231
|
+
|
|
1232
|
+
def get_provider_unique_id_from_node(
|
|
1233
|
+
self, node: Dict[str, Any], vulnerability_type: WizVulnerabilityType
|
|
1234
|
+
) -> Optional[str]:
|
|
1235
|
+
"""
|
|
1236
|
+
Get the providerUniqueId from a node based on the vulnerability type.
|
|
1237
|
+
This provides more meaningful asset identification for eMASS exports.
|
|
1238
|
+
|
|
1239
|
+
:param Dict[str, Any] node: The Wiz finding node
|
|
1240
|
+
:param WizVulnerabilityType vulnerability_type: The type of vulnerability
|
|
1241
|
+
:return: The providerUniqueId or fallback to asset name/ID
|
|
1242
|
+
:rtype: Optional[str]
|
|
1243
|
+
"""
|
|
1244
|
+
# Define asset lookup patterns for different vulnerability types - aligned with get_asset_id_from_node
|
|
1245
|
+
asset_lookup_patterns = {
|
|
1246
|
+
WizVulnerabilityType.VULNERABILITY: "vulnerableAsset",
|
|
1247
|
+
WizVulnerabilityType.CONFIGURATION: "resource",
|
|
1248
|
+
WizVulnerabilityType.HOST_FINDING: "resource",
|
|
1249
|
+
WizVulnerabilityType.DATA_FINDING: "resource",
|
|
1250
|
+
WizVulnerabilityType.SECRET_FINDING: "resource",
|
|
1251
|
+
WizVulnerabilityType.NETWORK_EXPOSURE_FINDING: "exposedEntity",
|
|
1252
|
+
WizVulnerabilityType.END_OF_LIFE_FINDING: "vulnerableAsset",
|
|
1253
|
+
WizVulnerabilityType.EXTERNAL_ATTACH_SURFACE: "exposedEntity",
|
|
1254
|
+
WizVulnerabilityType.EXCESSIVE_ACCESS_FINDING: "scope",
|
|
1255
|
+
WizVulnerabilityType.ISSUE: "entitySnapshot",
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
asset_lookup_key = asset_lookup_patterns.get(vulnerability_type, "entitySnapshot")
|
|
1259
|
+
|
|
1260
|
+
if asset_lookup_key == "scope":
|
|
1261
|
+
# Handle special case for excessive access findings where ID is nested
|
|
1262
|
+
scope = node.get("scope", {})
|
|
1263
|
+
graph_entity = scope.get("graphEntity", {})
|
|
1264
|
+
# Try providerUniqueId first, fallback to name, then id
|
|
1265
|
+
return graph_entity.get("providerUniqueId") or graph_entity.get("name") or graph_entity.get("id")
|
|
1266
|
+
|
|
1267
|
+
# Standard case - get asset container and extract provider identifier
|
|
1268
|
+
asset_container = node.get(asset_lookup_key) or {}
|
|
1269
|
+
|
|
1270
|
+
# Ensure asset_container is a dict before accessing
|
|
1271
|
+
if not isinstance(asset_container, dict):
|
|
1272
|
+
return None
|
|
1273
|
+
|
|
1274
|
+
# For Issue queries, the field is called 'providerId' instead of 'providerUniqueId'
|
|
1275
|
+
if vulnerability_type == WizVulnerabilityType.ISSUE:
|
|
1276
|
+
return (
|
|
1277
|
+
asset_container.get("providerId")
|
|
1278
|
+
or asset_container.get("providerUniqueId")
|
|
1279
|
+
or asset_container.get("name")
|
|
1280
|
+
or asset_container.get("id")
|
|
1281
|
+
)
|
|
1282
|
+
|
|
1283
|
+
# For other queries, try providerUniqueId first
|
|
1284
|
+
return asset_container.get("providerUniqueId") or asset_container.get("name") or asset_container.get("id")
|
|
738
1285
|
|
|
739
1286
|
def parse_finding(
|
|
740
1287
|
self, node: Dict[str, Any], vulnerability_type: WizVulnerabilityType
|
|
@@ -751,43 +1298,30 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
751
1298
|
# Route to specific parsing method based on vulnerability type
|
|
752
1299
|
if vulnerability_type == WizVulnerabilityType.SECRET_FINDING:
|
|
753
1300
|
return self._parse_secret_finding(node)
|
|
754
|
-
|
|
1301
|
+
if vulnerability_type == WizVulnerabilityType.NETWORK_EXPOSURE_FINDING:
|
|
755
1302
|
return self._parse_network_exposure_finding(node)
|
|
756
|
-
|
|
1303
|
+
if vulnerability_type == WizVulnerabilityType.EXTERNAL_ATTACH_SURFACE:
|
|
757
1304
|
return self._parse_external_attack_surface_finding(node)
|
|
758
|
-
|
|
1305
|
+
if vulnerability_type == WizVulnerabilityType.EXCESSIVE_ACCESS_FINDING:
|
|
759
1306
|
return self._parse_excessive_access_finding(node)
|
|
760
|
-
|
|
1307
|
+
if vulnerability_type == WizVulnerabilityType.END_OF_LIFE_FINDING:
|
|
761
1308
|
return self._parse_end_of_life_finding(node)
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
return self._parse_generic_finding(node, vulnerability_type)
|
|
1309
|
+
# Fallback to generic parsing for any other types
|
|
1310
|
+
return self._parse_generic_finding(node, vulnerability_type)
|
|
765
1311
|
except (KeyError, TypeError, ValueError) as e:
|
|
766
1312
|
logger.error("Error parsing Wiz finding: %s", str(e), exc_info=True)
|
|
767
1313
|
return None
|
|
768
1314
|
|
|
769
|
-
def
|
|
1315
|
+
def _get_secret_finding_data(self, node: Dict[str, Any]) -> Dict[str, Any]:
|
|
770
1316
|
"""
|
|
771
|
-
|
|
1317
|
+
Extract data specific to secret findings.
|
|
772
1318
|
|
|
773
1319
|
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
774
|
-
:return:
|
|
775
|
-
:rtype:
|
|
1320
|
+
:return: Dictionary containing secret-specific data
|
|
1321
|
+
:rtype: Dict[str, Any]
|
|
776
1322
|
"""
|
|
777
|
-
asset_id = node.get("resource", {}).get("id")
|
|
778
|
-
if not asset_id:
|
|
779
|
-
return None
|
|
780
|
-
|
|
781
|
-
first_seen = node.get("firstSeenAt") or get_current_datetime()
|
|
782
|
-
first_seen = format_to_regscale_iso(first_seen)
|
|
783
|
-
severity = self.get_issue_severity(node.get("severity", "Low"))
|
|
784
|
-
due_date = regscale_models.Issue.get_due_date(severity, self.app.config, "wiz", first_seen)
|
|
785
|
-
|
|
786
|
-
# Create meaningful title for secret findings
|
|
787
1323
|
secret_type = node.get("type", "Unknown Secret")
|
|
788
1324
|
resource_name = node.get("resource", {}).get("name", "Unknown Resource")
|
|
789
|
-
title = f"Secret Detected: {secret_type} in {resource_name}"
|
|
790
|
-
|
|
791
1325
|
# Build description with secret details
|
|
792
1326
|
description_parts = [
|
|
793
1327
|
f"Secret type: {secret_type}",
|
|
@@ -799,52 +1333,152 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
799
1333
|
if rule := node.get("rule", {}):
|
|
800
1334
|
description_parts.append(f"Detection rule: {rule.get('name', 'Unknown')}")
|
|
801
1335
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
status=self.map_status_to_issue_status(node.get("status", "Open")),
|
|
811
|
-
asset_identifier=asset_id,
|
|
812
|
-
external_id=node.get("id"),
|
|
813
|
-
first_seen=first_seen,
|
|
814
|
-
date_created=first_seen,
|
|
815
|
-
last_seen=format_to_regscale_iso(node.get("lastSeenAt") or get_current_datetime()),
|
|
816
|
-
remediation=f"Remove or properly secure the {secret_type} secret found in {resource_name}",
|
|
817
|
-
plugin_name=f"Wiz Secret Detection - {secret_type}",
|
|
818
|
-
vulnerability_type=WizVulnerabilityType.SECRET_FINDING.value,
|
|
819
|
-
due_date=due_date,
|
|
820
|
-
date_last_updated=format_to_regscale_iso(get_current_datetime()),
|
|
821
|
-
identification="Secret Scanning",
|
|
822
|
-
)
|
|
1336
|
+
return {
|
|
1337
|
+
"category": "Wiz Secret Detection",
|
|
1338
|
+
"title": f"Secret Detected: {secret_type} in {resource_name}",
|
|
1339
|
+
"description": "\n".join(description_parts),
|
|
1340
|
+
"remediation": f"Remove or properly secure the {secret_type} secret found in {resource_name}",
|
|
1341
|
+
"plugin_name": f"Wiz Secret Detection - {secret_type}",
|
|
1342
|
+
"identification": "Secret Scanning",
|
|
1343
|
+
}
|
|
823
1344
|
|
|
824
|
-
def
|
|
825
|
-
"""
|
|
1345
|
+
def _parse_secret_finding(self, node: Dict[str, Any]) -> Optional[IntegrationFinding]:
|
|
1346
|
+
"""
|
|
1347
|
+
Parse secret finding from Wiz.
|
|
1348
|
+
|
|
1349
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1350
|
+
:return: The parsed IntegrationFinding or None if parsing fails
|
|
1351
|
+
:rtype: Optional[IntegrationFinding]
|
|
1352
|
+
"""
|
|
1353
|
+
finding_data = self._get_secret_finding_data(node)
|
|
1354
|
+
return self._create_integration_finding(node, WizVulnerabilityType.SECRET_FINDING, finding_data)
|
|
1355
|
+
|
|
1356
|
+
def _create_integration_finding(
|
|
1357
|
+
self, node: Dict[str, Any], vulnerability_type: WizVulnerabilityType, finding_data: Dict[str, Any]
|
|
1358
|
+
) -> Optional[IntegrationFinding]:
|
|
1359
|
+
"""
|
|
1360
|
+
Unified method to create IntegrationFinding objects from Wiz data.
|
|
826
1361
|
|
|
827
1362
|
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1363
|
+
:param WizVulnerabilityType vulnerability_type: The type of vulnerability
|
|
1364
|
+
:param Dict[str, Any] finding_data: Finding-specific data (title, description, etc.)
|
|
828
1365
|
:return: The parsed IntegrationFinding or None if parsing fails
|
|
829
1366
|
:rtype: Optional[IntegrationFinding]
|
|
830
1367
|
"""
|
|
831
|
-
|
|
1368
|
+
# Get asset identifier
|
|
1369
|
+
asset_id = self.get_asset_id_from_node(node, vulnerability_type)
|
|
832
1370
|
if not asset_id:
|
|
1371
|
+
logger.debug(
|
|
1372
|
+
f"Skipping {vulnerability_type.value} finding '{node.get('name', 'Unknown')}' "
|
|
1373
|
+
f"(ID: {node.get('id', 'Unknown')}) - no asset identifier found"
|
|
1374
|
+
)
|
|
833
1375
|
return None
|
|
834
1376
|
|
|
835
|
-
|
|
836
|
-
|
|
1377
|
+
# Get meaningful asset identifier for eMASS exports
|
|
1378
|
+
provider_unique_id = self.get_provider_unique_id_from_node(node, vulnerability_type)
|
|
837
1379
|
|
|
838
|
-
#
|
|
839
|
-
|
|
1380
|
+
# Parse dates
|
|
1381
|
+
first_seen = self._get_first_seen_date(node)
|
|
1382
|
+
last_seen = self._get_last_seen_date(node, first_seen)
|
|
1383
|
+
# Get severity and calculate due date
|
|
1384
|
+
severity = self.get_issue_severity(finding_data.get("severity") or node.get("severity", "Low"))
|
|
840
1385
|
due_date = regscale_models.Issue.get_due_date(severity, self.app.config, "wiz", first_seen)
|
|
841
1386
|
|
|
842
|
-
#
|
|
1387
|
+
# Get status with diagnostic logging
|
|
1388
|
+
wiz_status = node.get("status", "Open")
|
|
1389
|
+
logger.debug(f"Processing Wiz finding {node.get('id', 'Unknown')}: raw status from node = '{wiz_status}'")
|
|
1390
|
+
status = self.map_status_to_issue_status(wiz_status)
|
|
1391
|
+
|
|
1392
|
+
# Add diagnostic logging for unexpected issue closure
|
|
1393
|
+
if status == regscale_models.IssueStatus.Closed and wiz_status.upper() not in ["RESOLVED", "REJECTED"]:
|
|
1394
|
+
logger.warning(
|
|
1395
|
+
f"Unexpected issue closure: Wiz status '{wiz_status}' mapped to Closed status "
|
|
1396
|
+
f"for finding {node.get('id', 'Unknown')} - '{finding_data.get('title', 'Unknown')}'. "
|
|
1397
|
+
f"This may indicate a mapping configuration issue."
|
|
1398
|
+
)
|
|
1399
|
+
|
|
1400
|
+
# Process comments if available
|
|
1401
|
+
comments_dict = node.get("commentThread", {})
|
|
1402
|
+
formatted_comments = self.process_comments(comments_dict) if comments_dict else None
|
|
1403
|
+
|
|
1404
|
+
# Build IntegrationFinding with unified data structure
|
|
1405
|
+
integration_finding_data = {
|
|
1406
|
+
"control_labels": [],
|
|
1407
|
+
"category": finding_data.get("category", "Wiz Vulnerability"),
|
|
1408
|
+
"title": finding_data.get("title", node.get("name", "Unknown vulnerability")),
|
|
1409
|
+
"description": finding_data.get("description", node.get("description", "")),
|
|
1410
|
+
"severity": severity,
|
|
1411
|
+
"status": status,
|
|
1412
|
+
"asset_identifier": asset_id,
|
|
1413
|
+
"issue_asset_identifier_value": provider_unique_id,
|
|
1414
|
+
"external_id": finding_data.get("external_id", node.get("id")),
|
|
1415
|
+
"first_seen": first_seen,
|
|
1416
|
+
"date_created": first_seen,
|
|
1417
|
+
"last_seen": last_seen,
|
|
1418
|
+
"remediation": finding_data.get("remediation", node.get("description", "")),
|
|
1419
|
+
"plugin_name": finding_data.get("plugin_name", node.get("name", "Unknown")),
|
|
1420
|
+
"vulnerability_type": vulnerability_type.value,
|
|
1421
|
+
"due_date": due_date,
|
|
1422
|
+
"date_last_updated": format_to_regscale_iso(get_current_datetime()),
|
|
1423
|
+
"identification": finding_data.get("identification", "Vulnerability Assessment"),
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
# Add optional fields if present
|
|
1427
|
+
if formatted_comments:
|
|
1428
|
+
integration_finding_data["comments"] = formatted_comments
|
|
1429
|
+
integration_finding_data["poam_comments"] = formatted_comments
|
|
1430
|
+
|
|
1431
|
+
# Add CVE-specific fields for generic findings
|
|
1432
|
+
if finding_data.get("cve"):
|
|
1433
|
+
integration_finding_data["cve"] = finding_data["cve"]
|
|
1434
|
+
if finding_data.get("cvss_score"):
|
|
1435
|
+
integration_finding_data["cvss_score"] = finding_data["cvss_score"]
|
|
1436
|
+
integration_finding_data["cvss_v3_base_score"] = finding_data["cvss_score"]
|
|
1437
|
+
if finding_data.get("source_rule_id"):
|
|
1438
|
+
integration_finding_data["source_rule_id"] = finding_data["source_rule_id"]
|
|
1439
|
+
|
|
1440
|
+
return IntegrationFinding(**integration_finding_data)
|
|
1441
|
+
|
|
1442
|
+
def _get_first_seen_date(self, node: Dict[str, Any]) -> str:
|
|
1443
|
+
"""
|
|
1444
|
+
Get the first seen date from a Wiz node, with fallbacks.
|
|
1445
|
+
|
|
1446
|
+
:param Dict[str, Any] node: The Wiz finding node
|
|
1447
|
+
:return: ISO formatted first seen date
|
|
1448
|
+
:rtype: str
|
|
1449
|
+
"""
|
|
1450
|
+
first_seen = node.get("firstSeenAt") or node.get("firstDetectedAt") or get_current_datetime()
|
|
1451
|
+
return format_to_regscale_iso(first_seen)
|
|
1452
|
+
|
|
1453
|
+
def _get_last_seen_date(self, node: Dict[str, Any], first_seen_fallback: str) -> str:
|
|
1454
|
+
"""
|
|
1455
|
+
Get the last seen date from a Wiz node, with fallbacks.
|
|
1456
|
+
|
|
1457
|
+
:param Dict[str, Any] node: The Wiz finding node
|
|
1458
|
+
:param str first_seen_fallback: Fallback date if no last seen available
|
|
1459
|
+
:return: ISO formatted last seen date
|
|
1460
|
+
:rtype: str
|
|
1461
|
+
"""
|
|
1462
|
+
last_seen = (
|
|
1463
|
+
node.get("lastSeenAt")
|
|
1464
|
+
or node.get("lastDetectedAt")
|
|
1465
|
+
or node.get("analyzedAt")
|
|
1466
|
+
or first_seen_fallback
|
|
1467
|
+
or get_current_datetime()
|
|
1468
|
+
)
|
|
1469
|
+
return format_to_regscale_iso(last_seen)
|
|
1470
|
+
|
|
1471
|
+
def _get_network_exposure_finding_data(self, node: Dict[str, Any]) -> Dict[str, Any]:
|
|
1472
|
+
"""
|
|
1473
|
+
Extract data specific to network exposure findings.
|
|
1474
|
+
|
|
1475
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1476
|
+
:return: Dictionary containing network exposure-specific data
|
|
1477
|
+
:rtype: Dict[str, Any]
|
|
1478
|
+
"""
|
|
843
1479
|
exposed_entity = node.get("exposedEntity", {})
|
|
844
1480
|
entity_name = exposed_entity.get("name", "Unknown Entity")
|
|
845
1481
|
port_range = node.get("portRange", "Unknown Port")
|
|
846
|
-
title = f"Network Exposure: {entity_name} on {port_range}"
|
|
847
|
-
|
|
848
1482
|
# Build description with network details
|
|
849
1483
|
description_parts = [
|
|
850
1484
|
f"Exposed entity: {entity_name} ({exposed_entity.get('type', 'Unknown Type')})",
|
|
@@ -858,52 +1492,37 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
858
1492
|
if net_protocols := node.get("networkProtocols"):
|
|
859
1493
|
description_parts.append(f"Network protocols: {', '.join(net_protocols)}")
|
|
860
1494
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
asset_identifier=asset_id,
|
|
871
|
-
external_id=node.get("id"),
|
|
872
|
-
first_seen=first_seen,
|
|
873
|
-
date_created=first_seen,
|
|
874
|
-
last_seen=first_seen, # Network exposures may not have lastSeen
|
|
875
|
-
remediation=f"Review and restrict network access to {entity_name} on {port_range}",
|
|
876
|
-
plugin_name=f"Wiz Network Exposure - {port_range}",
|
|
877
|
-
vulnerability_type=WizVulnerabilityType.NETWORK_EXPOSURE_FINDING.value,
|
|
878
|
-
due_date=due_date,
|
|
879
|
-
date_last_updated=format_to_regscale_iso(get_current_datetime()),
|
|
880
|
-
identification="Network Security Assessment",
|
|
881
|
-
)
|
|
1495
|
+
return {
|
|
1496
|
+
"category": "Wiz Network Exposure",
|
|
1497
|
+
"title": f"Network Exposure: {entity_name} on {port_range}",
|
|
1498
|
+
"description": "\n".join(description_parts),
|
|
1499
|
+
"severity": "Medium", # Network exposures typically don't have explicit severity
|
|
1500
|
+
"remediation": f"Review and restrict network access to {entity_name} on {port_range}",
|
|
1501
|
+
"plugin_name": f"Wiz Network Exposure - {port_range}",
|
|
1502
|
+
"identification": "Network Security Assessment",
|
|
1503
|
+
}
|
|
882
1504
|
|
|
883
|
-
def
|
|
884
|
-
"""Parse
|
|
1505
|
+
def _parse_network_exposure_finding(self, node: Dict[str, Any]) -> Optional[IntegrationFinding]:
|
|
1506
|
+
"""Parse network exposure finding from Wiz.
|
|
885
1507
|
|
|
886
1508
|
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
887
1509
|
:return: The parsed IntegrationFinding or None if parsing fails
|
|
888
1510
|
:rtype: Optional[IntegrationFinding]
|
|
889
1511
|
"""
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
return None
|
|
1512
|
+
finding_data = self._get_network_exposure_finding_data(node)
|
|
1513
|
+
return self._create_integration_finding(node, WizVulnerabilityType.NETWORK_EXPOSURE_FINDING, finding_data)
|
|
893
1514
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
# External attack surface findings are typically high severity
|
|
898
|
-
severity = regscale_models.IssueSeverity.High
|
|
899
|
-
due_date = regscale_models.Issue.get_due_date(severity, self.app.config, "wiz", first_seen)
|
|
1515
|
+
def _get_external_attack_surface_finding_data(self, node: Dict[str, Any]) -> Dict[str, Any]:
|
|
1516
|
+
"""
|
|
1517
|
+
Extract data specific to external attack surface findings.
|
|
900
1518
|
|
|
901
|
-
|
|
1519
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1520
|
+
:return: Dictionary containing external attack surface-specific data
|
|
1521
|
+
:rtype: Dict[str, Any]
|
|
1522
|
+
"""
|
|
902
1523
|
exposed_entity = node.get("exposedEntity", {})
|
|
903
1524
|
entity_name = exposed_entity.get("name", "Unknown Entity")
|
|
904
1525
|
port_range = node.get("portRange", "Unknown Port")
|
|
905
|
-
title = f"External Attack Surface: {entity_name} exposed on {port_range}"
|
|
906
|
-
|
|
907
1526
|
# Build description with attack surface details
|
|
908
1527
|
description_parts = [
|
|
909
1528
|
f"Externally exposed entity: {entity_name} ({exposed_entity.get('type', 'Unknown Type')})",
|
|
@@ -917,49 +1536,34 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
917
1536
|
endpoint_names = [ep.get("name", "Unknown") for ep in endpoints[:3]] # Limit to first 3
|
|
918
1537
|
description_parts.append(f"Application endpoints: {', '.join(endpoint_names)}")
|
|
919
1538
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
asset_identifier=asset_id,
|
|
930
|
-
external_id=node.get("id"),
|
|
931
|
-
first_seen=first_seen,
|
|
932
|
-
date_created=first_seen,
|
|
933
|
-
last_seen=first_seen,
|
|
934
|
-
remediation=f"Review external exposure of {entity_name} and implement proper access controls",
|
|
935
|
-
plugin_name=f"Wiz External Attack Surface - {port_range}",
|
|
936
|
-
vulnerability_type=WizVulnerabilityType.EXTERNAL_ATTACH_SURFACE.value,
|
|
937
|
-
due_date=due_date,
|
|
938
|
-
date_last_updated=format_to_regscale_iso(get_current_datetime()),
|
|
939
|
-
identification="External Attack Surface Assessment",
|
|
940
|
-
)
|
|
1539
|
+
return {
|
|
1540
|
+
"category": "Wiz External Attack Surface",
|
|
1541
|
+
"title": f"External Attack Surface: {entity_name} exposed on {port_range}",
|
|
1542
|
+
"description": "\n".join(description_parts),
|
|
1543
|
+
"severity": "High", # External attack surface findings are typically high severity
|
|
1544
|
+
"remediation": f"Review external exposure of {entity_name} and implement proper access controls",
|
|
1545
|
+
"plugin_name": f"Wiz External Attack Surface - {port_range}",
|
|
1546
|
+
"identification": "External Attack Surface Assessment",
|
|
1547
|
+
}
|
|
941
1548
|
|
|
942
|
-
def
|
|
943
|
-
"""Parse
|
|
1549
|
+
def _parse_external_attack_surface_finding(self, node: Dict[str, Any]) -> Optional[IntegrationFinding]:
|
|
1550
|
+
"""Parse external attack surface finding from Wiz.
|
|
944
1551
|
|
|
945
1552
|
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
946
1553
|
:return: The parsed IntegrationFinding or None if parsing fails
|
|
947
1554
|
:rtype: Optional[IntegrationFinding]
|
|
948
1555
|
"""
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
if not asset_id:
|
|
952
|
-
return None
|
|
1556
|
+
finding_data = self._get_external_attack_surface_finding_data(node)
|
|
1557
|
+
return self._create_integration_finding(node, WizVulnerabilityType.EXTERNAL_ATTACH_SURFACE, finding_data)
|
|
953
1558
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
due_date = regscale_models.Issue.get_due_date(severity, self.app.config, "wiz", first_seen)
|
|
958
|
-
|
|
959
|
-
# Use the finding name directly as it's descriptive
|
|
960
|
-
title = node.get("name", "Excessive Access Detected")
|
|
961
|
-
description = node.get("description", "")
|
|
1559
|
+
def _get_excessive_access_finding_data(self, node: Dict[str, Any]) -> Dict[str, Any]:
|
|
1560
|
+
"""
|
|
1561
|
+
Extract data specific to excessive access findings.
|
|
962
1562
|
|
|
1563
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1564
|
+
:return: Dictionary containing excessive access-specific data
|
|
1565
|
+
:rtype: Dict[str, Any]
|
|
1566
|
+
"""
|
|
963
1567
|
# Add remediation details
|
|
964
1568
|
remediation_parts = [node.get("description", "")]
|
|
965
1569
|
if remediation_instructions := node.get("remediationInstructions"):
|
|
@@ -967,42 +1571,34 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
967
1571
|
if policy_name := node.get("builtInPolicyRemediationName"):
|
|
968
1572
|
remediation_parts.append(f"Built-in policy: {policy_name}")
|
|
969
1573
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
status=self.map_status_to_issue_status(node.get("status", "Open")),
|
|
979
|
-
asset_identifier=asset_id,
|
|
980
|
-
external_id=node.get("id"),
|
|
981
|
-
first_seen=first_seen,
|
|
982
|
-
date_created=first_seen,
|
|
983
|
-
last_seen=first_seen,
|
|
984
|
-
remediation=remediation,
|
|
985
|
-
plugin_name=f"Wiz Excessive Access - {node.get('remediationType', 'Unknown')}",
|
|
986
|
-
vulnerability_type=WizVulnerabilityType.EXCESSIVE_ACCESS_FINDING.value,
|
|
987
|
-
due_date=due_date,
|
|
988
|
-
date_last_updated=format_to_regscale_iso(get_current_datetime()),
|
|
989
|
-
identification="Access Control Assessment",
|
|
990
|
-
)
|
|
1574
|
+
return {
|
|
1575
|
+
"category": "Wiz Excessive Access",
|
|
1576
|
+
"title": node.get("name", "Excessive Access Detected"),
|
|
1577
|
+
"description": node.get("description", ""),
|
|
1578
|
+
"remediation": "\n".join(filter(None, remediation_parts)),
|
|
1579
|
+
"plugin_name": f"Wiz Excessive Access - {node.get('remediationType', 'Unknown')}",
|
|
1580
|
+
"identification": "Access Control Assessment",
|
|
1581
|
+
}
|
|
991
1582
|
|
|
992
|
-
def
|
|
993
|
-
"""Parse
|
|
994
|
-
asset_id = node.get("vulnerableAsset", {}).get("id")
|
|
995
|
-
if not asset_id:
|
|
996
|
-
return None
|
|
1583
|
+
def _parse_excessive_access_finding(self, node: Dict[str, Any]) -> Optional[IntegrationFinding]:
|
|
1584
|
+
"""Parse excessive access finding from Wiz.
|
|
997
1585
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1586
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1587
|
+
:return: The parsed IntegrationFinding or None if parsing fails
|
|
1588
|
+
:rtype: Optional[IntegrationFinding]
|
|
1589
|
+
"""
|
|
1590
|
+
finding_data = self._get_excessive_access_finding_data(node)
|
|
1591
|
+
return self._create_integration_finding(node, WizVulnerabilityType.EXCESSIVE_ACCESS_FINDING, finding_data)
|
|
1002
1592
|
|
|
1003
|
-
|
|
1593
|
+
def _get_end_of_life_finding_data(self, node: Dict[str, Any]) -> Dict[str, Any]:
|
|
1594
|
+
"""
|
|
1595
|
+
Extract data specific to end of life findings.
|
|
1596
|
+
|
|
1597
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1598
|
+
:return: Dictionary containing end of life-specific data
|
|
1599
|
+
:rtype: Dict[str, Any]
|
|
1600
|
+
"""
|
|
1004
1601
|
name = node.get("name", "Unknown Technology")
|
|
1005
|
-
title = f"End of Life: {name}"
|
|
1006
1602
|
|
|
1007
1603
|
# Build description with EOL details
|
|
1008
1604
|
description_parts = [node.get("description", "")]
|
|
@@ -1011,42 +1607,30 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1011
1607
|
if recommended_version := node.get("recommendedVersion"):
|
|
1012
1608
|
description_parts.append(f"Recommended version: {recommended_version}")
|
|
1013
1609
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
asset_identifier=asset_id,
|
|
1024
|
-
external_id=node.get("id"),
|
|
1025
|
-
first_seen=first_seen,
|
|
1026
|
-
date_created=first_seen,
|
|
1027
|
-
last_seen=format_to_regscale_iso(node.get("lastDetectedAt") or get_current_datetime()),
|
|
1028
|
-
remediation=f"Upgrade {name} to a supported version",
|
|
1029
|
-
plugin_name=f"Wiz End of Life - {name}",
|
|
1030
|
-
vulnerability_type=WizVulnerabilityType.END_OF_LIFE_FINDING.value,
|
|
1031
|
-
due_date=due_date,
|
|
1032
|
-
date_last_updated=format_to_regscale_iso(get_current_datetime()),
|
|
1033
|
-
identification="Technology Lifecycle Assessment",
|
|
1034
|
-
)
|
|
1610
|
+
return {
|
|
1611
|
+
"category": "Wiz End of Life",
|
|
1612
|
+
"title": f"End of Life: {name}",
|
|
1613
|
+
"description": "\n".join(filter(None, description_parts)),
|
|
1614
|
+
"severity": "High", # End of life findings are typically high severity
|
|
1615
|
+
"remediation": f"Upgrade {name} to a supported version",
|
|
1616
|
+
"plugin_name": f"Wiz End of Life - {name}",
|
|
1617
|
+
"identification": "Technology Lifecycle Assessment",
|
|
1618
|
+
}
|
|
1035
1619
|
|
|
1036
|
-
def
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
asset_id = self.get_asset_id_from_node(node, vulnerability_type)
|
|
1041
|
-
if not asset_id:
|
|
1042
|
-
return None
|
|
1620
|
+
def _parse_end_of_life_finding(self, node: Dict[str, Any]) -> Optional[IntegrationFinding]:
|
|
1621
|
+
"""Parse end of life finding from Wiz."""
|
|
1622
|
+
finding_data = self._get_end_of_life_finding_data(node)
|
|
1623
|
+
return self._create_integration_finding(node, WizVulnerabilityType.END_OF_LIFE_FINDING, finding_data)
|
|
1043
1624
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
due_date = regscale_models.Issue.get_due_date(severity, self.app.config, "wiz", first_seen)
|
|
1625
|
+
def _get_generic_finding_data(self, node: Dict[str, Any]) -> Dict[str, Any]:
|
|
1626
|
+
"""
|
|
1627
|
+
Extract data specific to generic findings.
|
|
1048
1628
|
|
|
1049
|
-
|
|
1629
|
+
:param Dict[str, Any] node: The Wiz finding node to parse
|
|
1630
|
+
:param WizVulnerabilityType vulnerability_type: The type of vulnerability
|
|
1631
|
+
:return: Dictionary containing generic finding-specific data
|
|
1632
|
+
:rtype: Dict[str, Any]
|
|
1633
|
+
"""
|
|
1050
1634
|
name: str = node.get("name", "")
|
|
1051
1635
|
cve = (
|
|
1052
1636
|
name
|
|
@@ -1054,36 +1638,25 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1054
1638
|
else node.get("cve", name)
|
|
1055
1639
|
)
|
|
1056
1640
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
cve=cve,
|
|
1077
|
-
plugin_name=cve,
|
|
1078
|
-
cvss_v3_base_score=node.get("score"),
|
|
1079
|
-
source_rule_id=node.get("sourceRule", {}).get("id"),
|
|
1080
|
-
vulnerability_type=vulnerability_type.value,
|
|
1081
|
-
due_date=due_date,
|
|
1082
|
-
date_last_updated=format_to_regscale_iso(get_current_datetime()),
|
|
1083
|
-
identification="Vulnerability Assessment",
|
|
1084
|
-
comments=formatted_comments,
|
|
1085
|
-
poam_comments=formatted_comments,
|
|
1086
|
-
)
|
|
1641
|
+
return {
|
|
1642
|
+
"category": "Wiz Vulnerability",
|
|
1643
|
+
"title": node.get("name", "Unknown vulnerability"),
|
|
1644
|
+
"description": node.get("description", ""),
|
|
1645
|
+
"external_id": f"{node.get('sourceRule', {'id': cve}).get('id')}",
|
|
1646
|
+
"remediation": node.get("description", ""),
|
|
1647
|
+
"plugin_name": cve,
|
|
1648
|
+
"identification": "Vulnerability Assessment",
|
|
1649
|
+
"cve": cve,
|
|
1650
|
+
"cvss_score": node.get("score"),
|
|
1651
|
+
"source_rule_id": node.get("sourceRule", {}).get("id"),
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
def _parse_generic_finding(
|
|
1655
|
+
self, node: Dict[str, Any], vulnerability_type: WizVulnerabilityType
|
|
1656
|
+
) -> Optional[IntegrationFinding]:
|
|
1657
|
+
"""Generic parsing method for fallback cases."""
|
|
1658
|
+
finding_data = self._get_generic_finding_data(node)
|
|
1659
|
+
return self._create_integration_finding(node, vulnerability_type, finding_data)
|
|
1087
1660
|
|
|
1088
1661
|
def get_compliance_settings(self):
|
|
1089
1662
|
"""
|
|
@@ -1161,12 +1734,22 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1161
1734
|
"""
|
|
1162
1735
|
label_lower = label.lower()
|
|
1163
1736
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1737
|
+
logger.debug(f"Checking compliance label matching: status='{status_lower}', label='{label_lower}'")
|
|
1738
|
+
|
|
1739
|
+
# Check for open status mappings (including IN_PROGRESS)
|
|
1740
|
+
if status_lower in ["open", "in_progress"] and label_lower in [
|
|
1741
|
+
"open",
|
|
1742
|
+
"active",
|
|
1743
|
+
"new",
|
|
1744
|
+
"in progress",
|
|
1745
|
+
"in_progress",
|
|
1746
|
+
]:
|
|
1747
|
+
logger.debug(f"Matched status '{status_lower}' with label '{label_lower}' -> IssueStatus.Open")
|
|
1166
1748
|
return IssueStatus.Open
|
|
1167
1749
|
|
|
1168
1750
|
# Check for closed status mappings
|
|
1169
1751
|
if status_lower in ["resolved", "rejected"] and label_lower in ["closed", "resolved", "rejected", "completed"]:
|
|
1752
|
+
logger.debug(f"Matched status '{status_lower}' with label '{label_lower}' -> IssueStatus.Closed")
|
|
1170
1753
|
return IssueStatus.Closed
|
|
1171
1754
|
|
|
1172
1755
|
return None
|
|
@@ -1181,12 +1764,20 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1181
1764
|
"""
|
|
1182
1765
|
status_lower = status.lower()
|
|
1183
1766
|
|
|
1184
|
-
|
|
1767
|
+
# Add debug logging to trace status mapping
|
|
1768
|
+
logger.debug(f"Mapping Wiz status: original='{status}', lowercase='{status_lower}'")
|
|
1769
|
+
|
|
1770
|
+
# Map open and in-progress statuses to Open
|
|
1771
|
+
if status_lower in ["open", "in_progress"]:
|
|
1772
|
+
logger.debug(f"Wiz status '{status}' mapped to IssueStatus.Open")
|
|
1185
1773
|
return IssueStatus.Open
|
|
1186
|
-
|
|
1774
|
+
# Map resolved and rejected statuses to Closed
|
|
1775
|
+
if status_lower in ["resolved", "rejected"]:
|
|
1776
|
+
logger.debug(f"Wiz status '{status}' mapped to IssueStatus.Closed")
|
|
1187
1777
|
return IssueStatus.Closed
|
|
1188
|
-
|
|
1189
|
-
|
|
1778
|
+
# Default to Open for any unknown status
|
|
1779
|
+
logger.debug(f"Unknown Wiz status '{status}' encountered, defaulting to Open")
|
|
1780
|
+
return IssueStatus.Open
|
|
1190
1781
|
|
|
1191
1782
|
def fetch_assets(self, *args, **kwargs) -> Iterator[IntegrationAsset]:
|
|
1192
1783
|
"""
|
|
@@ -1296,6 +1887,13 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1296
1887
|
|
|
1297
1888
|
return software_version, software_vendor, software_name
|
|
1298
1889
|
|
|
1890
|
+
@lru_cache()
|
|
1891
|
+
def get_user_id(self) -> str:
|
|
1892
|
+
"""Function to return the default user ID
|
|
1893
|
+
:return: The default user ID as a string
|
|
1894
|
+
"""
|
|
1895
|
+
return RegScaleModel.get_user_id()
|
|
1896
|
+
|
|
1299
1897
|
def parse_asset(self, node: Dict[str, Any]) -> Optional[IntegrationAsset]:
|
|
1300
1898
|
"""
|
|
1301
1899
|
Parses Wiz assets
|
|
@@ -1304,8 +1902,9 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1304
1902
|
:return: The parsed IntegrationAsset
|
|
1305
1903
|
:rtype: Optional[IntegrationAsset]
|
|
1306
1904
|
"""
|
|
1307
|
-
|
|
1905
|
+
|
|
1308
1906
|
wiz_entity = node.get("graphEntity", {})
|
|
1907
|
+
name = wiz_entity.get("providerUniqueId") or node.get("name", "")
|
|
1309
1908
|
if not wiz_entity:
|
|
1310
1909
|
logger.warning("No graph entity found for asset %s", name)
|
|
1311
1910
|
return None
|
|
@@ -1334,7 +1933,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1334
1933
|
other_tracking_number=node.get("id", ""),
|
|
1335
1934
|
identifier=node.get("id", ""),
|
|
1336
1935
|
asset_type=create_asset_type(node.get("type", "")),
|
|
1337
|
-
asset_owner_id=
|
|
1936
|
+
asset_owner_id=self.get_user_id(),
|
|
1338
1937
|
parent_id=self.plan_id,
|
|
1339
1938
|
parent_module=regscale_models.SecurityPlan.get_module_slug(),
|
|
1340
1939
|
asset_category=map_category(node),
|
|
@@ -1435,6 +2034,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1435
2034
|
def get_software_name(software_name_dict: dict, wiz_entity_properties: dict, node: dict) -> Optional[str]:
|
|
1436
2035
|
"""
|
|
1437
2036
|
Gets the software name from the software name dictionary or Wiz entity properties.
|
|
2037
|
+
If no software name is present, assigns a name based on the parent asset and assigned component type.
|
|
1438
2038
|
|
|
1439
2039
|
:param dict software_name_dict: Dictionary containing software name and vendor
|
|
1440
2040
|
:param dict wiz_entity_properties: Properties of the Wiz entity
|
|
@@ -1442,9 +2042,32 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1442
2042
|
:return: Software name
|
|
1443
2043
|
:rtype: Optional[str]
|
|
1444
2044
|
"""
|
|
1445
|
-
if map_category(node)
|
|
1446
|
-
return
|
|
1447
|
-
|
|
2045
|
+
if map_category(node) != regscale_models.AssetCategory.Software:
|
|
2046
|
+
return None
|
|
2047
|
+
|
|
2048
|
+
# First try CPE-derived software name
|
|
2049
|
+
if software_name := software_name_dict.get("software_name"):
|
|
2050
|
+
return software_name
|
|
2051
|
+
|
|
2052
|
+
# Then try nativeType if it exists and looks meaningful
|
|
2053
|
+
native_type = wiz_entity_properties.get("nativeType")
|
|
2054
|
+
if native_type and not native_type.startswith(("Microsoft.", "AWS::", "Google.")):
|
|
2055
|
+
return native_type
|
|
2056
|
+
|
|
2057
|
+
# Finally, generate a name based on parent asset and component type
|
|
2058
|
+
parent_name = node.get("name", "")
|
|
2059
|
+
component_type = node.get("type", "").replace("_", " ").title()
|
|
2060
|
+
|
|
2061
|
+
if not parent_name:
|
|
2062
|
+
return component_type
|
|
2063
|
+
|
|
2064
|
+
# Clean up parent name for better readability by removing
|
|
2065
|
+
# common prefixes/suffixes that aren't meaningful
|
|
2066
|
+
cleaned_parent = parent_name
|
|
2067
|
+
for prefix in ["1-", "temp-", "test-"]:
|
|
2068
|
+
if cleaned_parent.lower().startswith(prefix):
|
|
2069
|
+
cleaned_parent = cleaned_parent[len(prefix) :]
|
|
2070
|
+
return f"{cleaned_parent} - {component_type}" if cleaned_parent else component_type
|
|
1448
2071
|
|
|
1449
2072
|
# Pre-compiled regex for better performance (ReDoS-safe pattern)
|
|
1450
2073
|
_PACKAGE_PATTERN = re.compile(r"([^()]+) \(([^()]+)\)")
|
|
@@ -1481,37 +2104,27 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1481
2104
|
:return: List of nodes as dictionaries
|
|
1482
2105
|
:rtype: List[Dict]
|
|
1483
2106
|
"""
|
|
1484
|
-
|
|
1485
|
-
current_time = datetime.datetime.now()
|
|
1486
|
-
check_file_path(os.path.dirname(file_path))
|
|
1487
|
-
|
|
1488
|
-
if os.path.exists(file_path):
|
|
1489
|
-
file_mod_time = datetime.datetime.fromtimestamp(os.path.getmtime(file_path))
|
|
1490
|
-
if current_time - file_mod_time < fetch_interval:
|
|
1491
|
-
logger.info("File %s is newer than %s hours. Using cached data...", file_path, fetch_interval)
|
|
1492
|
-
with open(file_path, "r", encoding="utf-8") as file:
|
|
1493
|
-
return json.load(file)
|
|
1494
|
-
else:
|
|
1495
|
-
logger.info("File %s is older than %s hours. Fetching new data...", file_path, fetch_interval)
|
|
1496
|
-
else:
|
|
1497
|
-
logger.info("File %s does not exist. Fetching new data...", file_path)
|
|
2107
|
+
max_age_hours = WizVariables.wizFullPullLimitHours or 8
|
|
1498
2108
|
|
|
1499
|
-
|
|
2109
|
+
def fetch_fresh_data():
|
|
2110
|
+
# Ensure we have a valid token (should already be set by caller)
|
|
2111
|
+
if not self.wiz_token:
|
|
2112
|
+
error_and_exit("Wiz token is not set. Please authenticate before calling fetch_wiz_data_if_needed.")
|
|
1500
2113
|
|
|
1501
|
-
|
|
1502
|
-
|
|
2114
|
+
return fetch_wiz_data(
|
|
2115
|
+
query=query,
|
|
2116
|
+
variables=variables,
|
|
2117
|
+
api_endpoint_url=WizVariables.wizUrl,
|
|
2118
|
+
token=self.wiz_token,
|
|
2119
|
+
topic_key=topic_key,
|
|
2120
|
+
)
|
|
1503
2121
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
topic_key=topic_key,
|
|
2122
|
+
return FileOperations.load_cache_or_fetch(
|
|
2123
|
+
file_path=file_path,
|
|
2124
|
+
fetch_fn=fetch_fresh_data,
|
|
2125
|
+
max_age_hours=max_age_hours,
|
|
2126
|
+
save_cache=True,
|
|
1510
2127
|
)
|
|
1511
|
-
with open(file_path, "w", encoding="utf-8") as file:
|
|
1512
|
-
json.dump(nodes, file)
|
|
1513
|
-
|
|
1514
|
-
return nodes
|
|
1515
2128
|
|
|
1516
2129
|
def get_asset_by_identifier(self, identifier: str) -> Optional[regscale_models.Asset]:
|
|
1517
2130
|
"""
|
|
@@ -1546,7 +2159,11 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1546
2159
|
:param str identifier: The missing asset identifier
|
|
1547
2160
|
:rtype: None
|
|
1548
2161
|
"""
|
|
1549
|
-
|
|
2162
|
+
# Only log detailed diagnostics for the first occurrence of each asset
|
|
2163
|
+
if identifier in self.alerted_assets:
|
|
2164
|
+
return
|
|
2165
|
+
|
|
2166
|
+
logger.debug("Analyzing missing asset: %s", identifier)
|
|
1550
2167
|
|
|
1551
2168
|
# Define inventory files to search (constant moved up for clarity)
|
|
1552
2169
|
inventory_files = (
|
|
@@ -1575,39 +2192,11 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1575
2192
|
:return: Tuple of (asset_info, source_file) or (None, None)
|
|
1576
2193
|
:rtype: tuple[Optional[Dict], Optional[str]]
|
|
1577
2194
|
"""
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
asset_info = self._search_single_file(identifier, file_path)
|
|
1584
|
-
if asset_info:
|
|
1585
|
-
return asset_info, file_path
|
|
1586
|
-
except (json.JSONDecodeError, IOError) as e:
|
|
1587
|
-
logger.debug("Error reading %s: %s", file_path, e)
|
|
1588
|
-
continue
|
|
1589
|
-
|
|
1590
|
-
return None, None
|
|
1591
|
-
|
|
1592
|
-
def _search_single_file(self, identifier: str, file_path: str) -> Optional[Dict]:
|
|
1593
|
-
"""
|
|
1594
|
-
Search for asset in a single JSON file
|
|
1595
|
-
|
|
1596
|
-
:param str identifier: Asset identifier to search for
|
|
1597
|
-
:param str file_path: Path to JSON file
|
|
1598
|
-
:return: Asset data if found, None otherwise
|
|
1599
|
-
:rtype: Optional[Dict]
|
|
1600
|
-
"""
|
|
1601
|
-
logger.debug("Searching for asset %s in %s", identifier, file_path)
|
|
1602
|
-
|
|
1603
|
-
with open(file_path, "r", encoding="utf-8") as f:
|
|
1604
|
-
data = json.load(f)
|
|
1605
|
-
|
|
1606
|
-
if not isinstance(data, list):
|
|
1607
|
-
return None
|
|
1608
|
-
|
|
1609
|
-
# Use generator expression for memory efficiency
|
|
1610
|
-
return next((item for item in data if self._find_asset_in_node(item, identifier)), None)
|
|
2195
|
+
return FileOperations.search_json_files(
|
|
2196
|
+
identifier=identifier,
|
|
2197
|
+
file_paths=list(file_paths),
|
|
2198
|
+
match_fn=self._find_asset_in_node,
|
|
2199
|
+
)
|
|
1611
2200
|
|
|
1612
2201
|
def _log_found_asset_details(self, identifier: str, asset_info: Dict, source_file: str) -> None:
|
|
1613
2202
|
"""
|
|
@@ -1621,10 +2210,13 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1621
2210
|
asset_type = self._extract_asset_type_from_node(asset_info)
|
|
1622
2211
|
asset_name = self._extract_asset_name_from_node(asset_info)
|
|
1623
2212
|
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
2213
|
+
# Track missing asset types for summary reporting
|
|
2214
|
+
self._missing_asset_types.add(asset_type)
|
|
2215
|
+
|
|
2216
|
+
logger.info(
|
|
2217
|
+
"Missing asset found in cached data - ID: %s, Type: %s, Name: '%s', Source: %s\n"
|
|
2218
|
+
" Action: Consider adding '%s' to RECOMMENDED_WIZ_INVENTORY_TYPES in constants.py\n"
|
|
2219
|
+
" Then re-run: regscale wiz inventory --wiz_project_id <project_id> -id <plan_id>",
|
|
1628
2220
|
identifier,
|
|
1629
2221
|
asset_type,
|
|
1630
2222
|
asset_name,
|
|
@@ -1639,13 +2231,11 @@ class WizVulnerabilityIntegration(ScannerIntegration):
|
|
|
1639
2231
|
:param str identifier: Asset identifier
|
|
1640
2232
|
:rtype: None
|
|
1641
2233
|
"""
|
|
1642
|
-
logger.
|
|
1643
|
-
"
|
|
1644
|
-
"
|
|
1645
|
-
"
|
|
1646
|
-
"
|
|
1647
|
-
" - Asset type not included in current queries\n"
|
|
1648
|
-
" - Asset deleted from Wiz but finding still exists",
|
|
2234
|
+
logger.debug(
|
|
2235
|
+
"Asset not found in cached data - ID: %s. Possible reasons: "
|
|
2236
|
+
"(1) Asset from different Wiz project, "
|
|
2237
|
+
"(2) Asset type not included in queries, "
|
|
2238
|
+
"(3) Asset deleted from Wiz",
|
|
1649
2239
|
identifier,
|
|
1650
2240
|
)
|
|
1651
2241
|
|