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
|
@@ -8,11 +8,13 @@ that follow common patterns across different compliance tools (Wiz, Tenable, Sic
|
|
|
8
8
|
"""
|
|
9
9
|
import logging
|
|
10
10
|
import re
|
|
11
|
+
import time
|
|
11
12
|
from abc import ABC, abstractmethod
|
|
12
13
|
from collections import defaultdict
|
|
13
14
|
from typing import Dict, List, Optional, Any, Iterator
|
|
14
15
|
|
|
15
16
|
from regscale.core.app.utils.app_utils import get_current_datetime, regscale_string_to_datetime
|
|
17
|
+
from regscale.integrations.control_matcher import ControlMatcher
|
|
16
18
|
from regscale.integrations.scanner_integration import (
|
|
17
19
|
ScannerIntegration,
|
|
18
20
|
IntegrationAsset,
|
|
@@ -25,6 +27,8 @@ from regscale.models.regscale_models import (
|
|
|
25
27
|
ControlImplementation,
|
|
26
28
|
Assessment,
|
|
27
29
|
ImplementationObjective,
|
|
30
|
+
SecurityPlan,
|
|
31
|
+
ComplianceSettings,
|
|
28
32
|
)
|
|
29
33
|
|
|
30
34
|
logger = logging.getLogger("regscale")
|
|
@@ -102,9 +106,37 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
102
106
|
- Creating assessments and updating control status
|
|
103
107
|
"""
|
|
104
108
|
|
|
109
|
+
# String literal constants
|
|
110
|
+
NOT_APPLICABLE_LABEL = "Not Applicable"
|
|
111
|
+
NOT_APPLICABLE_LOWER = "not applicable"
|
|
112
|
+
NOT_APPLICABLE_UNDERSCORE = "not_applicable"
|
|
113
|
+
|
|
105
114
|
# Status mapping constants
|
|
106
|
-
PASS_STATUSES = ["PASS", "PASSED", "pass", "passed"]
|
|
107
|
-
FAIL_STATUSES = [
|
|
115
|
+
PASS_STATUSES = ["PASS", "PASSED", "Pass", "Passed", "pass", "passed", "COMPLIANT", "Compliant", "compliant"]
|
|
116
|
+
FAIL_STATUSES = [
|
|
117
|
+
"FAIL",
|
|
118
|
+
"FAILED",
|
|
119
|
+
"Fail",
|
|
120
|
+
"Failed",
|
|
121
|
+
"fail",
|
|
122
|
+
"failed",
|
|
123
|
+
"NONCOMPLIANT",
|
|
124
|
+
"NonCompliant",
|
|
125
|
+
"noncompliant",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
NOT_APPLICABLE_STATUSES = ["NOT_APPLICABLE", NOT_APPLICABLE_LABEL, "not_applicable", "NA", "N/A"]
|
|
129
|
+
INCONCLUSIVE_STATUSES = [
|
|
130
|
+
"INCONCLUSIVE",
|
|
131
|
+
"Inconclusive",
|
|
132
|
+
"inconclusive",
|
|
133
|
+
"UNKNOWN",
|
|
134
|
+
"Unknown",
|
|
135
|
+
"unknown",
|
|
136
|
+
"MANUAL",
|
|
137
|
+
"Manual",
|
|
138
|
+
"manual",
|
|
139
|
+
]
|
|
108
140
|
|
|
109
141
|
def __init__(
|
|
110
142
|
self,
|
|
@@ -140,6 +172,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
140
172
|
self.failed_compliance_items: List[ComplianceItem] = []
|
|
141
173
|
self.passing_controls: Dict[str, ComplianceItem] = {}
|
|
142
174
|
self.failing_controls: Dict[str, ComplianceItem] = {}
|
|
175
|
+
self.not_applicable_controls: Dict[str, ComplianceItem] = {}
|
|
143
176
|
|
|
144
177
|
# Asset mapping for compliance to asset correlation
|
|
145
178
|
self.asset_compliance_map: Dict[str, List[ComplianceItem]] = defaultdict(list)
|
|
@@ -160,6 +193,20 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
160
193
|
# Set scan date
|
|
161
194
|
self.scan_date = get_current_datetime()
|
|
162
195
|
|
|
196
|
+
# Cache for compliance settings
|
|
197
|
+
self._compliance_settings = None
|
|
198
|
+
self._security_plan = None
|
|
199
|
+
self._security_plan_loaded = False # Track if we've attempted to load
|
|
200
|
+
self._compliance_settings_loaded = False # Track if we've attempted to load
|
|
201
|
+
self._status_mapping_cache = {} # Cache for status mappings to avoid repeated calculations
|
|
202
|
+
|
|
203
|
+
# Initialize control matcher for robust control ID matching
|
|
204
|
+
self._control_matcher = ControlMatcher()
|
|
205
|
+
|
|
206
|
+
# Performance optimization: cache for control lookups
|
|
207
|
+
# Key: control ID variation (e.g., 'ac-2(1)') -> (ControlImplementation, SecurityControl)
|
|
208
|
+
self._control_lookup_cache: Dict[str, tuple[ControlImplementation, SecurityControl]] = {}
|
|
209
|
+
|
|
163
210
|
def is_poam(self, finding: IntegrationFinding) -> bool: # type: ignore[override]
|
|
164
211
|
"""
|
|
165
212
|
Determines if an issue should be considered a POAM for compliance integrations.
|
|
@@ -201,7 +248,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
201
248
|
self._load_existing_assessments()
|
|
202
249
|
|
|
203
250
|
self._cache_loaded = True
|
|
204
|
-
logger.info("
|
|
251
|
+
logger.info("Loaded existing records cache to prevent duplicates:")
|
|
205
252
|
logger.info(f" - Assets: {len(self._existing_assets_cache)}")
|
|
206
253
|
logger.info(f" - Issues: {len(self._existing_issues_cache)}")
|
|
207
254
|
logger.info(f" - Assessments: {len(self._existing_assessments_cache)}")
|
|
@@ -225,9 +272,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
225
272
|
)
|
|
226
273
|
|
|
227
274
|
for asset in existing_assets:
|
|
228
|
-
# Cache by
|
|
229
|
-
if hasattr(asset, "externalId") and asset.externalId:
|
|
230
|
-
self._existing_assets_cache[asset.externalId] = asset
|
|
275
|
+
# Cache by identifier and other_tracking_number for flexible lookup
|
|
231
276
|
if hasattr(asset, "identifier") and asset.identifier:
|
|
232
277
|
self._existing_assets_cache[asset.identifier] = asset
|
|
233
278
|
if hasattr(asset, "otherTrackingNumber") and asset.otherTrackingNumber:
|
|
@@ -253,7 +298,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
253
298
|
parent_id=self.plan_id, parent_module=self.parent_module
|
|
254
299
|
)
|
|
255
300
|
all_issues.update(plan_issues)
|
|
256
|
-
logger.debug(f"
|
|
301
|
+
logger.debug(f"Found {len(plan_issues)} issues directly under plan {self.plan_id}")
|
|
257
302
|
|
|
258
303
|
# Method 2: Get issues associated with control implementations (matches scanner integration logic)
|
|
259
304
|
try:
|
|
@@ -274,24 +319,22 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
274
319
|
except Exception as e:
|
|
275
320
|
logger.debug(f"Could not load issue {issue_id}: {e}")
|
|
276
321
|
|
|
277
|
-
logger.debug(f"
|
|
322
|
+
logger.debug(f"Found {impl_issues_count} additional issues via control implementations")
|
|
278
323
|
except Exception as e:
|
|
279
324
|
logger.debug(f"Could not load issues by control implementation: {e}")
|
|
280
325
|
|
|
281
|
-
logger.debug(f"
|
|
326
|
+
logger.debug(f"Total unique issues found: {len(all_issues)} for plan {self.plan_id}")
|
|
282
327
|
|
|
283
328
|
wiz_issues = 0
|
|
284
329
|
for issue in all_issues:
|
|
285
330
|
# Cache by external_id and other_identifier for flexible lookup
|
|
286
|
-
if hasattr(issue, "externalId") and issue.externalId:
|
|
287
|
-
self._existing_issues_cache[issue.externalId] = issue
|
|
288
|
-
if "wiz-policy" in issue.externalId.lower():
|
|
289
|
-
wiz_issues += 1
|
|
290
|
-
logger.debug(f"📋 Cached Wiz issue: {issue.id} -> external_id: {issue.externalId}")
|
|
291
331
|
if hasattr(issue, "otherIdentifier") and issue.otherIdentifier:
|
|
292
332
|
self._existing_issues_cache[issue.otherIdentifier] = issue
|
|
333
|
+
if "wiz-policy" in issue.otherIdentifier.lower():
|
|
334
|
+
wiz_issues += 1
|
|
335
|
+
logger.debug(f"Cached Wiz issue: {issue.id} -> other_identifier: {issue.otherIdentifier}")
|
|
293
336
|
|
|
294
|
-
logger.debug(f"
|
|
337
|
+
logger.debug(f"Cached {wiz_issues} Wiz policy issues out of {len(all_issues)} total issues")
|
|
295
338
|
|
|
296
339
|
except Exception as e:
|
|
297
340
|
logger.debug(f"Error loading existing issues: {e}")
|
|
@@ -378,6 +421,50 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
378
421
|
cache_key = f"{implementation_id}_{day_key}"
|
|
379
422
|
return self._existing_assessments_cache.get(cache_key)
|
|
380
423
|
|
|
424
|
+
def check_for_existing_evidence(self, file_name_pattern: str) -> bool:
|
|
425
|
+
"""
|
|
426
|
+
Check if an evidence file matching the pattern already exists in RegScale.
|
|
427
|
+
|
|
428
|
+
This method fetches existing files for the plan and checks if any match
|
|
429
|
+
the provided pattern, helping prevent duplicate evidence uploads.
|
|
430
|
+
|
|
431
|
+
:param str file_name_pattern: Pattern to match against existing file names
|
|
432
|
+
:return: True if a matching file exists, False otherwise
|
|
433
|
+
:rtype: bool
|
|
434
|
+
"""
|
|
435
|
+
try:
|
|
436
|
+
# Import here to avoid circular dependency
|
|
437
|
+
from regscale.models.regscale_models import File
|
|
438
|
+
|
|
439
|
+
# Get all existing files for the plan
|
|
440
|
+
existing_files = File.get_files_for_parent_from_regscale(
|
|
441
|
+
parent_id=self.plan_id, parent_module=self.parent_module
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Check if any file matches the pattern
|
|
445
|
+
for file_obj in existing_files:
|
|
446
|
+
if hasattr(file_obj, "trustedDisplayName") and file_obj.trustedDisplayName:
|
|
447
|
+
# Check if the pattern is in the file name
|
|
448
|
+
if file_name_pattern in file_obj.trustedDisplayName:
|
|
449
|
+
logger.debug(
|
|
450
|
+
"Found existing evidence file matching pattern '%s': %s",
|
|
451
|
+
file_name_pattern,
|
|
452
|
+
file_obj.trustedDisplayName,
|
|
453
|
+
)
|
|
454
|
+
return True
|
|
455
|
+
|
|
456
|
+
logger.debug("No existing evidence files found matching pattern '%s'", file_name_pattern)
|
|
457
|
+
return False
|
|
458
|
+
|
|
459
|
+
except Exception as e:
|
|
460
|
+
logger.warning(
|
|
461
|
+
"Unable to check for existing evidence files (pattern: '%s'): %s. Proceeding with upload.",
|
|
462
|
+
file_name_pattern,
|
|
463
|
+
e,
|
|
464
|
+
)
|
|
465
|
+
# Return False to allow upload to proceed if check fails
|
|
466
|
+
return False
|
|
467
|
+
|
|
381
468
|
@abstractmethod
|
|
382
469
|
def fetch_compliance_data(self) -> List[Any]:
|
|
383
470
|
"""
|
|
@@ -411,17 +498,33 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
411
498
|
"""
|
|
412
499
|
logger.info("Processing compliance data...")
|
|
413
500
|
|
|
414
|
-
|
|
501
|
+
self._reset_compliance_state()
|
|
502
|
+
allowed_controls = self._build_allowed_controls_set()
|
|
503
|
+
raw_compliance_data = self.fetch_compliance_data()
|
|
504
|
+
|
|
505
|
+
processing_stats = self._process_raw_compliance_items(raw_compliance_data, allowed_controls)
|
|
506
|
+
self._log_processing_summary(raw_compliance_data, processing_stats)
|
|
507
|
+
|
|
508
|
+
# Perform control-level categorization based on aggregated results
|
|
509
|
+
self._categorize_controls_by_aggregation()
|
|
510
|
+
self._log_final_results()
|
|
511
|
+
|
|
512
|
+
def _reset_compliance_state(self) -> None:
|
|
513
|
+
"""Reset state to avoid double counting on repeated calls."""
|
|
415
514
|
self.all_compliance_items = []
|
|
416
515
|
self.failed_compliance_items = []
|
|
417
516
|
self.passing_controls = {}
|
|
418
517
|
self.failing_controls = {}
|
|
518
|
+
self.not_applicable_controls = {}
|
|
419
519
|
self.asset_compliance_map.clear()
|
|
420
520
|
|
|
421
|
-
|
|
521
|
+
def _build_allowed_controls_set(self) -> set[str]:
|
|
522
|
+
"""Build allowed control IDs from plan/catalog controls to restrict scope."""
|
|
422
523
|
allowed_controls_normalized: set[str] = set()
|
|
423
524
|
try:
|
|
424
525
|
controls = self._get_controls()
|
|
526
|
+
logger.debug(f"Loaded {len(controls)} controls from plan/catalog")
|
|
527
|
+
|
|
425
528
|
for ctl in controls:
|
|
426
529
|
cid = (ctl.get("controlId") or "").strip()
|
|
427
530
|
if not cid:
|
|
@@ -429,56 +532,242 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
429
532
|
base, sub = self._normalize_control_id(cid)
|
|
430
533
|
normalized = f"{base}({sub})" if sub else base
|
|
431
534
|
allowed_controls_normalized.add(normalized)
|
|
432
|
-
|
|
433
|
-
|
|
535
|
+
|
|
536
|
+
logger.debug(f"Built allowed_controls_normalized set with {len(allowed_controls_normalized)} entries")
|
|
537
|
+
if allowed_controls_normalized:
|
|
538
|
+
sample = sorted(allowed_controls_normalized)[:5]
|
|
539
|
+
logger.debug(f"Sample allowed controls: {sample}")
|
|
540
|
+
except Exception as e:
|
|
541
|
+
logger.warning(f"Could not load controls from plan/catalog: {e}")
|
|
434
542
|
allowed_controls_normalized = set()
|
|
435
543
|
|
|
436
|
-
|
|
437
|
-
|
|
544
|
+
return allowed_controls_normalized
|
|
545
|
+
|
|
546
|
+
def _process_raw_compliance_items(self, raw_compliance_data: list, allowed_controls: set) -> dict:
|
|
547
|
+
"""Process raw compliance items and return processing statistics.
|
|
548
|
+
:param list raw_compliance_data: Raw compliance data from external system
|
|
549
|
+
:param set allowed_controls: Allowed control IDs
|
|
550
|
+
:return: Processed compliance items
|
|
551
|
+
:rtype: dict
|
|
552
|
+
"""
|
|
553
|
+
stats = {"skipped_no_control": 0, "skipped_no_resource": 0, "skipped_not_in_plan": 0, "processed_count": 0}
|
|
438
554
|
|
|
439
|
-
# Convert to ComplianceItem objects
|
|
440
555
|
for raw_item in raw_compliance_data:
|
|
441
556
|
try:
|
|
442
557
|
compliance_item = self.create_compliance_item(raw_item)
|
|
443
|
-
|
|
444
|
-
if not getattr(compliance_item, "control_id", "") or not getattr(compliance_item, "resource_id", ""):
|
|
558
|
+
if not self._process_single_compliance_item(compliance_item, allowed_controls, stats):
|
|
445
559
|
continue
|
|
446
|
-
|
|
447
|
-
# If we have an allowed set, restrict to only controls in current plan/catalog
|
|
448
|
-
if allowed_controls_normalized:
|
|
449
|
-
base, sub = self._normalize_control_id(getattr(compliance_item, "control_id", ""))
|
|
450
|
-
norm_item = f"{base}({sub})" if sub else base
|
|
451
|
-
if norm_item not in allowed_controls_normalized:
|
|
452
|
-
continue
|
|
453
|
-
self.all_compliance_items.append(compliance_item)
|
|
454
|
-
|
|
455
|
-
# Build asset mapping
|
|
456
|
-
self.asset_compliance_map[compliance_item.resource_id].append(compliance_item)
|
|
457
|
-
|
|
458
|
-
# Categorize by result
|
|
459
|
-
if compliance_item.compliance_result in self.FAIL_STATUSES:
|
|
460
|
-
self.failed_compliance_items.append(compliance_item)
|
|
461
|
-
# Track failing controls (control can fail if ANY asset fails)
|
|
462
|
-
control_key = compliance_item.control_id.lower()
|
|
463
|
-
self.failing_controls[control_key] = compliance_item
|
|
464
|
-
# Remove from passing if it was there
|
|
465
|
-
self.passing_controls.pop(control_key, None)
|
|
466
|
-
|
|
467
|
-
elif compliance_item.compliance_result in self.PASS_STATUSES:
|
|
468
|
-
control_key = compliance_item.control_id.lower()
|
|
469
|
-
# Only mark as passing if not already failing
|
|
470
|
-
if control_key not in self.failing_controls:
|
|
471
|
-
self.passing_controls[control_key] = compliance_item
|
|
472
|
-
|
|
473
560
|
except Exception as e:
|
|
474
561
|
logger.error(f"Error processing compliance item: {e}")
|
|
475
562
|
continue
|
|
476
563
|
|
|
564
|
+
return stats
|
|
565
|
+
|
|
566
|
+
def _process_single_compliance_item(self, compliance_item: Any, allowed_controls: set, stats: dict) -> bool:
|
|
567
|
+
"""Process a single compliance item and update statistics. Returns True if processed successfully."""
|
|
568
|
+
control_id = getattr(compliance_item, "control_id", "")
|
|
569
|
+
resource_id = getattr(compliance_item, "resource_id", "")
|
|
570
|
+
|
|
571
|
+
if not control_id:
|
|
572
|
+
stats["skipped_no_control"] += 1
|
|
573
|
+
return False
|
|
574
|
+
if not resource_id:
|
|
575
|
+
stats["skipped_no_resource"] += 1
|
|
576
|
+
return False
|
|
577
|
+
|
|
578
|
+
if not self._should_process_item(compliance_item, control_id, allowed_controls, stats):
|
|
579
|
+
return False
|
|
580
|
+
|
|
581
|
+
self._add_processed_item(compliance_item, stats)
|
|
582
|
+
return True
|
|
583
|
+
|
|
584
|
+
def _should_process_item(self, compliance_item: Any, control_id: str, allowed_controls: set, stats: dict) -> bool:
|
|
585
|
+
"""Determine if an item should be processed based on control filtering."""
|
|
586
|
+
if not allowed_controls:
|
|
587
|
+
return True
|
|
588
|
+
|
|
589
|
+
base, sub = self._normalize_control_id(control_id)
|
|
590
|
+
norm_item = f"{base}({sub})" if sub else base
|
|
591
|
+
|
|
592
|
+
if norm_item in allowed_controls:
|
|
593
|
+
return True
|
|
594
|
+
|
|
595
|
+
# Allow PASS controls through even if they don't have existing implementations
|
|
596
|
+
if compliance_item.compliance_result in self.PASS_STATUSES:
|
|
597
|
+
return True
|
|
598
|
+
|
|
599
|
+
stats["skipped_not_in_plan"] += 1
|
|
600
|
+
if stats["skipped_not_in_plan"] <= 3:
|
|
601
|
+
logger.debug(f"Skipping control {norm_item} - not in plan (result: {compliance_item.compliance_result})")
|
|
602
|
+
return False
|
|
603
|
+
|
|
604
|
+
def _add_processed_item(self, compliance_item: Any, stats: dict) -> None:
|
|
605
|
+
"""Add a processed item to collections and update statistics."""
|
|
606
|
+
self.all_compliance_items.append(compliance_item)
|
|
607
|
+
stats["processed_count"] += 1
|
|
608
|
+
|
|
609
|
+
# Build asset mapping
|
|
610
|
+
self.asset_compliance_map[compliance_item.resource_id].append(compliance_item)
|
|
611
|
+
|
|
612
|
+
# Categorize by result
|
|
613
|
+
if compliance_item.compliance_result in self.FAIL_STATUSES:
|
|
614
|
+
self.failed_compliance_items.append(compliance_item)
|
|
615
|
+
logger.debug(
|
|
616
|
+
f"Added failing compliance item: control={compliance_item.control_id}, "
|
|
617
|
+
f"result={compliance_item.compliance_result}, resource={compliance_item.resource_id}"
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
def _log_processing_summary(self, raw_compliance_data: list, stats: dict) -> None:
|
|
621
|
+
"""Log summary of compliance data processing."""
|
|
622
|
+
logger.debug("Compliance item processing summary:")
|
|
623
|
+
logger.debug(f" - Total raw items: {len(raw_compliance_data)}")
|
|
624
|
+
logger.debug(f" - Skipped (no control_id): {stats['skipped_no_control']}")
|
|
625
|
+
logger.debug(f" - Skipped (no resource_id): {stats['skipped_no_resource']}")
|
|
626
|
+
logger.debug(f" - Skipped (not in plan): {stats['skipped_not_in_plan']}")
|
|
627
|
+
logger.debug(f" - Processed successfully: {stats['processed_count']}")
|
|
628
|
+
|
|
629
|
+
def _log_final_results(self) -> None:
|
|
630
|
+
"""Log final processing results."""
|
|
477
631
|
logger.debug(
|
|
478
632
|
f"Processed {len(self.all_compliance_items)} compliance items: "
|
|
479
633
|
f"{len(self.all_compliance_items) - len(self.failed_compliance_items)} passing, "
|
|
480
634
|
f"{len(self.failed_compliance_items)} failing"
|
|
481
635
|
)
|
|
636
|
+
logger.debug(
|
|
637
|
+
f"Control categorization: {len(self.passing_controls)} passing controls, "
|
|
638
|
+
f"{len(self.failing_controls)} failing controls"
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
def _categorize_controls_by_aggregation(self) -> None:
|
|
642
|
+
"""
|
|
643
|
+
Categorize controls as passing or failing based on aggregated results across all compliance items.
|
|
644
|
+
|
|
645
|
+
This method uses project-scoped aggregation logic instead of the previous "any fail = control fails"
|
|
646
|
+
approach. For project-scoped integrations (like Wiz), this provides more accurate control status.
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
# Group all compliance items by control ID
|
|
650
|
+
control_items = self._group_items_by_control()
|
|
651
|
+
|
|
652
|
+
# Analyze each control's results
|
|
653
|
+
for control_key, items in control_items.items():
|
|
654
|
+
self._categorize_single_control(control_key, items)
|
|
655
|
+
|
|
656
|
+
def _group_items_by_control(self) -> dict:
|
|
657
|
+
"""Group compliance items by control ID."""
|
|
658
|
+
from collections import defaultdict
|
|
659
|
+
|
|
660
|
+
control_items = defaultdict(list)
|
|
661
|
+
for item in self.all_compliance_items:
|
|
662
|
+
control_key = item.control_id.lower()
|
|
663
|
+
control_items[control_key].append(item)
|
|
664
|
+
|
|
665
|
+
return control_items
|
|
666
|
+
|
|
667
|
+
def _categorize_single_control(self, control_key: str, items: list) -> None:
|
|
668
|
+
"""Categorize a single control based on its compliance items."""
|
|
669
|
+
from collections import Counter
|
|
670
|
+
|
|
671
|
+
results = [item.compliance_result for item in items]
|
|
672
|
+
result_counts = Counter(results)
|
|
673
|
+
total_items = len(results)
|
|
674
|
+
|
|
675
|
+
fail_count, pass_count, not_applicable_count = self._count_results(result_counts)
|
|
676
|
+
|
|
677
|
+
if fail_count == 0 and pass_count > 0:
|
|
678
|
+
self._mark_control_as_passing(control_key, items, pass_count, fail_count)
|
|
679
|
+
elif fail_count > 0:
|
|
680
|
+
self._handle_control_with_failures(control_key, items, fail_count, pass_count, total_items)
|
|
681
|
+
elif not_applicable_count > 0 and pass_count == 0 and fail_count == 0:
|
|
682
|
+
self._mark_control_as_not_applicable(control_key, items, not_applicable_count)
|
|
683
|
+
else:
|
|
684
|
+
logger.debug(f"Control {control_key} has unclear results: {dict(result_counts)}")
|
|
685
|
+
|
|
686
|
+
def _count_results(self, result_counts: dict) -> tuple[int, int, int]:
|
|
687
|
+
"""Count pass, fail, and not applicable results from result counts."""
|
|
688
|
+
fail_statuses_lower = [status.lower() for status in self.FAIL_STATUSES]
|
|
689
|
+
pass_statuses_lower = [status.lower() for status in self.PASS_STATUSES]
|
|
690
|
+
not_applicable_statuses_lower = [status.lower() for status in self.NOT_APPLICABLE_STATUSES]
|
|
691
|
+
|
|
692
|
+
fail_count = 0
|
|
693
|
+
pass_count = 0
|
|
694
|
+
not_applicable_count = 0
|
|
695
|
+
|
|
696
|
+
for result, count in result_counts.items():
|
|
697
|
+
if result is None: # Skip None results (controls without evidence)
|
|
698
|
+
continue
|
|
699
|
+
result_lower = result.lower()
|
|
700
|
+
if result_lower in fail_statuses_lower:
|
|
701
|
+
fail_count += count
|
|
702
|
+
elif result_lower in pass_statuses_lower:
|
|
703
|
+
pass_count += count
|
|
704
|
+
elif result_lower in not_applicable_statuses_lower:
|
|
705
|
+
not_applicable_count += count
|
|
706
|
+
|
|
707
|
+
return fail_count, pass_count, not_applicable_count
|
|
708
|
+
|
|
709
|
+
def _count_pass_fail_results(self, result_counts: dict) -> tuple[int, int]:
|
|
710
|
+
"""Count pass and fail results from result counts (legacy method)."""
|
|
711
|
+
fail_count, pass_count, _ = self._count_results(result_counts)
|
|
712
|
+
return fail_count, pass_count
|
|
713
|
+
|
|
714
|
+
def _mark_control_as_passing(self, control_key: str, items: list, pass_count: int, fail_count: int) -> None:
|
|
715
|
+
"""Mark a control as passing."""
|
|
716
|
+
self.passing_controls[control_key] = items[0] # Use first item as representative
|
|
717
|
+
logger.debug(f"Control {control_key} marked as PASSING: {pass_count}P/{fail_count}F")
|
|
718
|
+
|
|
719
|
+
def _mark_control_as_not_applicable(self, control_key: str, items: list, not_applicable_count: int) -> None:
|
|
720
|
+
"""Mark a control as not applicable."""
|
|
721
|
+
self.not_applicable_controls[control_key] = items[0] # Use first item as representative
|
|
722
|
+
logger.debug(f"Control {control_key} marked as NOT_APPLICABLE: {not_applicable_count} items")
|
|
723
|
+
|
|
724
|
+
def _handle_control_with_failures(
|
|
725
|
+
self, control_key: str, items: list, fail_count: int, pass_count: int, total_items: int
|
|
726
|
+
) -> None:
|
|
727
|
+
"""Handle a control that has some failures."""
|
|
728
|
+
fail_ratio = fail_count / total_items
|
|
729
|
+
failure_threshold = getattr(self, "control_failure_threshold", 0.2)
|
|
730
|
+
|
|
731
|
+
if fail_ratio > failure_threshold:
|
|
732
|
+
self._mark_control_as_failing(control_key, items, pass_count, fail_count, fail_ratio, failure_threshold)
|
|
733
|
+
else:
|
|
734
|
+
self._mark_control_as_passing_with_warnings(
|
|
735
|
+
control_key, items, pass_count, fail_count, fail_ratio, failure_threshold
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
def _mark_control_as_failing(
|
|
739
|
+
self,
|
|
740
|
+
control_key: str,
|
|
741
|
+
items: list,
|
|
742
|
+
pass_count: int,
|
|
743
|
+
fail_count: int,
|
|
744
|
+
fail_ratio: float,
|
|
745
|
+
failure_threshold: float,
|
|
746
|
+
) -> None:
|
|
747
|
+
"""Mark a control as failing due to significant failures."""
|
|
748
|
+
fail_statuses_lower = [status.lower() for status in self.FAIL_STATUSES]
|
|
749
|
+
failing_item = next(item for item in items if item.compliance_result.lower() in fail_statuses_lower)
|
|
750
|
+
self.failing_controls[control_key] = failing_item
|
|
751
|
+
logger.debug(
|
|
752
|
+
f"Control {control_key} marked as FAILING: {pass_count}P/{fail_count}F "
|
|
753
|
+
f"({fail_ratio:.1%} fail rate > {failure_threshold:.1%} threshold)"
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
def _mark_control_as_passing_with_warnings(
|
|
757
|
+
self,
|
|
758
|
+
control_key: str,
|
|
759
|
+
items: list,
|
|
760
|
+
pass_count: int,
|
|
761
|
+
fail_count: int,
|
|
762
|
+
fail_ratio: float,
|
|
763
|
+
failure_threshold: float,
|
|
764
|
+
) -> None:
|
|
765
|
+
"""Mark a control as passing despite low failure rate."""
|
|
766
|
+
self.passing_controls[control_key] = items[0]
|
|
767
|
+
logger.debug(
|
|
768
|
+
f"Control {control_key} marked as PASSING (low fail rate): {pass_count}P/{fail_count}F "
|
|
769
|
+
f"({fail_ratio:.1%} fail rate < {failure_threshold:.1%} threshold)"
|
|
770
|
+
)
|
|
482
771
|
|
|
483
772
|
def create_asset_from_compliance_item(self, compliance_item: ComplianceItem) -> Optional[IntegrationAsset]:
|
|
484
773
|
"""
|
|
@@ -528,6 +817,14 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
528
817
|
control_labels = [compliance_item.control_id] if compliance_item.control_id else []
|
|
529
818
|
severity = self._map_severity(compliance_item.severity)
|
|
530
819
|
|
|
820
|
+
# Extract ARNs if available (for AWS Audit Manager and other integrations)
|
|
821
|
+
arns = None
|
|
822
|
+
if hasattr(compliance_item, "resource_arns"):
|
|
823
|
+
arns = compliance_item.resource_arns
|
|
824
|
+
# Only use ARNs if they're non-empty
|
|
825
|
+
if not arns:
|
|
826
|
+
arns = None
|
|
827
|
+
|
|
531
828
|
finding = IntegrationFinding(
|
|
532
829
|
control_labels=control_labels,
|
|
533
830
|
title=f"Compliance Violation: {compliance_item.control_id}",
|
|
@@ -542,10 +839,11 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
542
839
|
last_seen=self.scan_date,
|
|
543
840
|
scan_date=self.scan_date,
|
|
544
841
|
asset_identifier=compliance_item.resource_id,
|
|
842
|
+
issue_asset_identifier_value=arns,
|
|
545
843
|
vulnerability_type="Compliance Violation",
|
|
546
844
|
rule_id=compliance_item.control_id,
|
|
547
845
|
baseline=compliance_item.framework,
|
|
548
|
-
affected_controls=
|
|
846
|
+
affected_controls=compliance_item.control_id,
|
|
549
847
|
)
|
|
550
848
|
|
|
551
849
|
# Ensure affected controls are set to the normalized control label (e.g., RA-5, AC-2(1))
|
|
@@ -590,31 +888,60 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
590
888
|
|
|
591
889
|
def fetch_findings(self, *args, **kwargs) -> Iterator[IntegrationFinding]:
|
|
592
890
|
"""
|
|
593
|
-
Fetch findings from failed compliance items.
|
|
891
|
+
Fetch findings from failed compliance items to create RegScale issues.
|
|
594
892
|
|
|
595
893
|
:param args: Variable positional arguments
|
|
596
894
|
:param kwargs: Variable keyword arguments
|
|
597
|
-
:return: Iterator of integration findings
|
|
895
|
+
:return: Iterator of integration findings (will be converted to RegScale Issue objects)
|
|
598
896
|
:rtype: Iterator[IntegrationFinding]
|
|
599
897
|
"""
|
|
600
|
-
logger.info("
|
|
898
|
+
logger.info(f"Preparing to create issues from {len(self.failed_compliance_items)} failed compliance items...")
|
|
899
|
+
|
|
900
|
+
# Debug: Show sample of failed items
|
|
901
|
+
if self.failed_compliance_items:
|
|
902
|
+
sample_size = min(5, len(self.failed_compliance_items))
|
|
903
|
+
sample = self.failed_compliance_items[:sample_size]
|
|
904
|
+
logger.debug(f"Sample failed items (first {sample_size}):")
|
|
905
|
+
for item in sample:
|
|
906
|
+
logger.debug(
|
|
907
|
+
f" - Control: {item.control_id}, Result: {item.compliance_result}, Resource: {item.resource_id}"
|
|
908
|
+
)
|
|
601
909
|
|
|
602
910
|
total = len(self.failed_compliance_items)
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
911
|
+
# Backwards compatibility: check if finding_progress exists and has add_task method
|
|
912
|
+
task_id = None
|
|
913
|
+
if self.finding_progress is not None and hasattr(self.finding_progress, "add_task"):
|
|
914
|
+
task_id = self.finding_progress.add_task(
|
|
915
|
+
f"[#f68d1f]Creating issues from {total} failed compliance item(s)...",
|
|
916
|
+
total=total or None,
|
|
917
|
+
)
|
|
607
918
|
|
|
919
|
+
findings_created = 0
|
|
608
920
|
for compliance_item in self.failed_compliance_items:
|
|
609
921
|
finding = self.create_finding_from_compliance_item(compliance_item)
|
|
610
922
|
if finding:
|
|
611
|
-
|
|
923
|
+
findings_created += 1
|
|
924
|
+
# Backwards compatibility: check if finding_progress exists and has advance method
|
|
925
|
+
if (
|
|
926
|
+
task_id is not None
|
|
927
|
+
and self.finding_progress is not None
|
|
928
|
+
and hasattr(self.finding_progress, "advance")
|
|
929
|
+
):
|
|
930
|
+
self.finding_progress.advance(task_id, 1)
|
|
612
931
|
yield finding
|
|
613
932
|
|
|
614
933
|
# Ensure task completes if total is known
|
|
615
|
-
if
|
|
934
|
+
# Backwards compatibility: check if finding_progress exists and has update method
|
|
935
|
+
if (
|
|
936
|
+
total
|
|
937
|
+
and task_id is not None
|
|
938
|
+
and self.finding_progress is not None
|
|
939
|
+
and hasattr(self.finding_progress, "update")
|
|
940
|
+
):
|
|
616
941
|
self.finding_progress.update(task_id, completed=total)
|
|
617
942
|
|
|
943
|
+
logger.info(f"Prepared {findings_created} issue records from {total} failed compliance items")
|
|
944
|
+
|
|
618
945
|
def sync_compliance(self) -> None:
|
|
619
946
|
"""
|
|
620
947
|
Main method to sync compliance data.
|
|
@@ -660,6 +987,11 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
660
987
|
assets_processed = self.update_regscale_assets(iter(assets))
|
|
661
988
|
self._log_asset_results(assets_processed)
|
|
662
989
|
|
|
990
|
+
# Refresh the asset map after creating/updating assets to ensure
|
|
991
|
+
# the map contains all assets for issue creation
|
|
992
|
+
logger.debug("Refreshing asset map after asset sync...")
|
|
993
|
+
self.asset_map_by_identifier.update(self.get_asset_map())
|
|
994
|
+
|
|
663
995
|
def _log_asset_results(self, assets_processed: int) -> None:
|
|
664
996
|
"""
|
|
665
997
|
Log asset processing results.
|
|
@@ -707,8 +1039,32 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
707
1039
|
logger.debug("No findings to process into issues")
|
|
708
1040
|
return
|
|
709
1041
|
|
|
710
|
-
|
|
711
|
-
|
|
1042
|
+
# Ensure asset map is populated before processing issues
|
|
1043
|
+
# This handles cases where assets were created in previous runs
|
|
1044
|
+
if not self.asset_map_by_identifier:
|
|
1045
|
+
logger.debug("Loading asset map before issue processing...")
|
|
1046
|
+
self.asset_map_by_identifier.update(self.get_asset_map())
|
|
1047
|
+
|
|
1048
|
+
findings_processed, findings_skipped = self._process_findings_to_issues(findings)
|
|
1049
|
+
|
|
1050
|
+
# CRITICAL FIX: Flush bulk issue operations to database
|
|
1051
|
+
# This ensures all issues created/updated in bulk mode are persisted
|
|
1052
|
+
logger.debug(f"Calling bulk_save for {findings_processed} processed findings ({findings_skipped} skipped)...")
|
|
1053
|
+
issue_results = regscale_models.Issue.bulk_save()
|
|
1054
|
+
logger.debug(
|
|
1055
|
+
f"Bulk save completed - created: {issue_results.get('created_count', 0)}, updated: {issue_results.get('updated_count', 0)}"
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
# Update result counts with actual database operations
|
|
1059
|
+
if hasattr(self, "_results"):
|
|
1060
|
+
if "issues" not in self._results:
|
|
1061
|
+
self._results["issues"] = {}
|
|
1062
|
+
self._results["issues"].update(issue_results)
|
|
1063
|
+
|
|
1064
|
+
# Use actual database results for logging
|
|
1065
|
+
issues_created = issue_results.get("created_count", 0)
|
|
1066
|
+
issues_updated = issue_results.get("updated_count", 0)
|
|
1067
|
+
self._log_issue_results_accurate(issues_created, issues_updated, findings_processed, findings_skipped)
|
|
712
1068
|
|
|
713
1069
|
def _process_findings_to_issues(self, findings: List[IntegrationFinding]) -> tuple[int, int]:
|
|
714
1070
|
"""
|
|
@@ -720,14 +1076,20 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
720
1076
|
issues_created = 0
|
|
721
1077
|
issues_skipped = 0
|
|
722
1078
|
|
|
723
|
-
|
|
1079
|
+
logger.debug(f"Processing {len(findings)} findings into issues...")
|
|
1080
|
+
for i, finding in enumerate(findings):
|
|
724
1081
|
try:
|
|
1082
|
+
logger.debug(
|
|
1083
|
+
f"Processing finding {i + 1}/{len(findings)}: external_id='{finding.external_id}', asset_identifier='{finding.asset_identifier}"
|
|
1084
|
+
)
|
|
725
1085
|
if self._process_single_finding(finding):
|
|
726
1086
|
issues_created += 1
|
|
1087
|
+
logger.debug(f" -> Finding {i + 1} processed successfully")
|
|
727
1088
|
else:
|
|
728
1089
|
issues_skipped += 1
|
|
1090
|
+
logger.debug(f" -> Finding {i + 1} skipped")
|
|
729
1091
|
except Exception as e:
|
|
730
|
-
logger.error(f"Error processing finding: {e}")
|
|
1092
|
+
logger.error(f"Error processing finding {i + 1}: {e}")
|
|
731
1093
|
issues_skipped += 1
|
|
732
1094
|
|
|
733
1095
|
return issues_created, issues_skipped
|
|
@@ -739,14 +1101,25 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
739
1101
|
:param finding: Finding to process
|
|
740
1102
|
:return: True if issue was created/updated, False if skipped
|
|
741
1103
|
"""
|
|
1104
|
+
logger.debug(
|
|
1105
|
+
f" -> Processing finding: external_id='{finding.external_id}', asset_identifier='{finding.asset_identifier}'"
|
|
1106
|
+
)
|
|
1107
|
+
|
|
742
1108
|
asset = self._get_or_create_asset_for_finding(finding)
|
|
743
1109
|
if not asset:
|
|
1110
|
+
logger.debug(f" -> Asset not found/created for identifier '{finding.asset_identifier}', skipping finding")
|
|
744
1111
|
self._log_asset_not_found_error(finding)
|
|
745
1112
|
return False
|
|
746
1113
|
|
|
1114
|
+
logger.debug(f" -> Found/created asset {asset.id} for identifier '{finding.asset_identifier}'")
|
|
747
1115
|
issue_title = self.get_issue_title(finding)
|
|
748
1116
|
issue = self.create_or_update_issue_from_finding(title=issue_title, finding=finding)
|
|
749
|
-
|
|
1117
|
+
success = issue is not None
|
|
1118
|
+
if success and issue:
|
|
1119
|
+
logger.debug(f" -> Successfully processed finding -> issue {issue.id}")
|
|
1120
|
+
else:
|
|
1121
|
+
logger.debug(" -> Failed to create/update issue for finding")
|
|
1122
|
+
return success
|
|
750
1123
|
|
|
751
1124
|
def _get_or_create_asset_for_finding(self, finding: IntegrationFinding) -> Optional[regscale_models.Asset]:
|
|
752
1125
|
"""
|
|
@@ -778,6 +1151,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
778
1151
|
def _log_issue_results(self, issues_created: int, issues_skipped: int) -> None:
|
|
779
1152
|
"""
|
|
780
1153
|
Log issue processing results.
|
|
1154
|
+
DEPRECATED: Use _log_issue_results_accurate for accurate reporting.
|
|
781
1155
|
|
|
782
1156
|
:param int issues_created: Number of issues created/updated
|
|
783
1157
|
:param int issues_skipped: Number of issues skipped
|
|
@@ -791,6 +1165,36 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
791
1165
|
else:
|
|
792
1166
|
logger.debug("No issues processed")
|
|
793
1167
|
|
|
1168
|
+
def _log_issue_results_accurate(
|
|
1169
|
+
self, issues_created: int, issues_updated: int, findings_processed: int, findings_skipped: int
|
|
1170
|
+
) -> None:
|
|
1171
|
+
"""
|
|
1172
|
+
Log accurate issue processing results based on actual database operations.
|
|
1173
|
+
|
|
1174
|
+
:param int issues_created: Number of new issues created in database
|
|
1175
|
+
:param int issues_updated: Number of existing issues updated in database
|
|
1176
|
+
:param int findings_processed: Number of findings that were processed
|
|
1177
|
+
:param int findings_skipped: Number of findings that were skipped
|
|
1178
|
+
:return: None
|
|
1179
|
+
:rtype: None
|
|
1180
|
+
"""
|
|
1181
|
+
total_db_operations = issues_created + issues_updated
|
|
1182
|
+
|
|
1183
|
+
if total_db_operations > 0:
|
|
1184
|
+
logger.info(
|
|
1185
|
+
f"Processed {findings_processed} findings into issues: {issues_created} new issues created, {issues_updated} existing issues updated"
|
|
1186
|
+
)
|
|
1187
|
+
if findings_skipped > 0:
|
|
1188
|
+
logger.info(f"Skipped {findings_skipped} findings (assets not found)")
|
|
1189
|
+
elif findings_skipped > 0:
|
|
1190
|
+
logger.warning(
|
|
1191
|
+
f"Issues processed: 0 created/updated, {findings_skipped} findings skipped (assets not found)"
|
|
1192
|
+
)
|
|
1193
|
+
else:
|
|
1194
|
+
logger.debug(
|
|
1195
|
+
f"Processed {findings_processed} findings but no database changes were needed (all issues up-to-date)"
|
|
1196
|
+
)
|
|
1197
|
+
|
|
794
1198
|
def _finalize_scan_history(self, scan_history: regscale_models.ScanHistory) -> None:
|
|
795
1199
|
"""
|
|
796
1200
|
Finalize scan history with error handling.
|
|
@@ -869,6 +1273,9 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
869
1273
|
logger.warning("No control implementations found for assessment processing")
|
|
870
1274
|
return
|
|
871
1275
|
|
|
1276
|
+
# Build control lookup cache for fast O(1) matching
|
|
1277
|
+
self._build_control_lookup_cache(implementations)
|
|
1278
|
+
|
|
872
1279
|
all_control_ids = set(self.passing_controls.keys()) | set(self.failing_controls.keys())
|
|
873
1280
|
logger.info(f"Processing assessments for {len(all_control_ids)} controls with compliance data")
|
|
874
1281
|
logger.info(f"Control IDs with data: {sorted(list(all_control_ids))}")
|
|
@@ -909,6 +1316,56 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
909
1316
|
logger.info(f"Found {len(implementations)} control implementations")
|
|
910
1317
|
return implementations
|
|
911
1318
|
|
|
1319
|
+
def _build_control_lookup_cache(self, implementations: List[ControlImplementation]) -> None:
|
|
1320
|
+
"""
|
|
1321
|
+
Build a lookup cache mapping control ID variations to implementations and security controls.
|
|
1322
|
+
|
|
1323
|
+
This dramatically improves performance by:
|
|
1324
|
+
1. Fetching all SecurityControl objects once (instead of once per match attempt)
|
|
1325
|
+
2. Pre-computing all control ID variations
|
|
1326
|
+
3. Creating a dictionary for O(1) lookup instead of O(n) iteration
|
|
1327
|
+
|
|
1328
|
+
For 1011 implementations x 71 controls = 71,781 iterations in the old code.
|
|
1329
|
+
New code: 1011 fetches + 71 dictionary lookups = ~1082 operations (67x faster!)
|
|
1330
|
+
|
|
1331
|
+
:param List[ControlImplementation] implementations: List of control implementations to cache
|
|
1332
|
+
:return: None
|
|
1333
|
+
:rtype: None
|
|
1334
|
+
"""
|
|
1335
|
+
if self._control_lookup_cache:
|
|
1336
|
+
# Cache already built
|
|
1337
|
+
return
|
|
1338
|
+
|
|
1339
|
+
logger.debug(f"Building control lookup cache for {len(implementations)} implementations...")
|
|
1340
|
+
start_time = time.time()
|
|
1341
|
+
|
|
1342
|
+
for implementation in implementations:
|
|
1343
|
+
try:
|
|
1344
|
+
security_control = SecurityControl.get_object(object_id=implementation.controlID)
|
|
1345
|
+
if not security_control or not security_control.controlId:
|
|
1346
|
+
continue
|
|
1347
|
+
|
|
1348
|
+
# Generate all variations of this control ID for flexible matching
|
|
1349
|
+
control_variations = self._control_matcher._get_control_id_variations(security_control.controlId)
|
|
1350
|
+
|
|
1351
|
+
# Map each variation to this implementation + security control pair
|
|
1352
|
+
for variation in control_variations:
|
|
1353
|
+
# Store the first implementation found for each variation
|
|
1354
|
+
# (if multiple implementations have the same control, use the first one)
|
|
1355
|
+
if variation not in self._control_lookup_cache:
|
|
1356
|
+
self._control_lookup_cache[variation] = (implementation, security_control)
|
|
1357
|
+
|
|
1358
|
+
except Exception as e: # noqa: BLE001
|
|
1359
|
+
logger.error(
|
|
1360
|
+
f"Error caching implementation {implementation.id} with controlID {implementation.controlID}: {e}"
|
|
1361
|
+
)
|
|
1362
|
+
continue
|
|
1363
|
+
|
|
1364
|
+
elapsed = time.time() - start_time
|
|
1365
|
+
logger.info(
|
|
1366
|
+
f"Built control lookup cache with {len(self._control_lookup_cache)} control ID variations in {elapsed:.2f}s"
|
|
1367
|
+
)
|
|
1368
|
+
|
|
912
1369
|
def _log_sample_controls(self, implementations: List[ControlImplementation]) -> None:
|
|
913
1370
|
"""
|
|
914
1371
|
Log sample control IDs for debugging purposes.
|
|
@@ -961,6 +1418,11 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
961
1418
|
|
|
962
1419
|
if impl.id in processed_impl_today and self._find_existing_assessment_cached(impl.id, self.scan_date):
|
|
963
1420
|
logger.debug(f"Skipping duplicate assessment for implementation {impl.id} (already processed today)")
|
|
1421
|
+
# IMPORTANT: Still update the control implementation status even when skipping assessment creation
|
|
1422
|
+
# This ensures status is updated on subsequent runs
|
|
1423
|
+
if self.update_control_status:
|
|
1424
|
+
logger.debug(f"Updating control implementation status for {control_id} (existing assessment)")
|
|
1425
|
+
self._update_implementation_status(impl, result)
|
|
964
1426
|
else:
|
|
965
1427
|
self._create_control_assessment(
|
|
966
1428
|
implementation=impl,
|
|
@@ -972,7 +1434,6 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
972
1434
|
processed_impl_today.add(impl.id)
|
|
973
1435
|
|
|
974
1436
|
self._record_control_mapping(control_id, impl.id)
|
|
975
|
-
self._map_assets_to_control_component(sec_control, items)
|
|
976
1437
|
return 1
|
|
977
1438
|
except Exception as e: # noqa: BLE001
|
|
978
1439
|
logger.error(f"Error processing control assessment for '{control_id}': {e}")
|
|
@@ -987,41 +1448,31 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
987
1448
|
"""
|
|
988
1449
|
Find matching implementation and security control for a control ID.
|
|
989
1450
|
|
|
1451
|
+
Uses ControlMatcher for robust control ID matching with leading zero normalization.
|
|
1452
|
+
Performance optimized with pre-built lookup cache for O(1) matching.
|
|
1453
|
+
|
|
990
1454
|
:param str control_id: Control identifier to match
|
|
991
|
-
:param List[ControlImplementation] implementations: Available implementations
|
|
1455
|
+
:param List[ControlImplementation] implementations: Available implementations (used for fallback only)
|
|
992
1456
|
:return: Tuple of matching implementation and security control, or (None, None)
|
|
993
1457
|
:rtype: tuple[Optional[ControlImplementation], Optional[SecurityControl]]
|
|
994
1458
|
"""
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
security_control_id = security_control.controlId
|
|
1006
|
-
if not security_control_id:
|
|
1007
|
-
logger.debug(f"Security control {security_control.id} has no controlId")
|
|
1008
|
-
continue
|
|
1459
|
+
# Generate all variations of the search control ID for matching
|
|
1460
|
+
search_variations = self._control_matcher._get_control_id_variations(control_id)
|
|
1461
|
+
if not search_variations:
|
|
1462
|
+
logger.debug(f"Could not generate control ID variations for: {control_id}")
|
|
1463
|
+
return None, None
|
|
1464
|
+
|
|
1465
|
+
# Try to find a match using the pre-built lookup cache (O(1) lookup)
|
|
1466
|
+
for variation in search_variations:
|
|
1467
|
+
if variation in self._control_lookup_cache:
|
|
1468
|
+
implementation, security_control = self._control_lookup_cache[variation]
|
|
1009
1469
|
logger.debug(
|
|
1010
|
-
f"
|
|
1011
|
-
)
|
|
1012
|
-
if self._control_ids_match(control_id, security_control_id):
|
|
1013
|
-
matching_implementation = implementation
|
|
1014
|
-
matching_security_control = security_control
|
|
1015
|
-
logger.info(
|
|
1016
|
-
f"✅ MATCH FOUND: '{security_control_id}' == '{control_id}' (implementation: {implementation.id})"
|
|
1017
|
-
)
|
|
1018
|
-
break
|
|
1019
|
-
except Exception as e: # noqa: BLE001
|
|
1020
|
-
logger.error(
|
|
1021
|
-
f"Error processing implementation {implementation.id} with controlID {implementation.controlID}: {e}"
|
|
1470
|
+
f"✅ MATCH FOUND: '{security_control.controlId}' == '{control_id}' (implementation: {implementation.id})"
|
|
1022
1471
|
)
|
|
1023
|
-
|
|
1024
|
-
|
|
1472
|
+
return implementation, security_control
|
|
1473
|
+
|
|
1474
|
+
# No match found in cache
|
|
1475
|
+
return None, None
|
|
1025
1476
|
|
|
1026
1477
|
def _log_no_match(self, control_id: str, implementations: List[ControlImplementation]) -> None:
|
|
1027
1478
|
"""
|
|
@@ -1048,7 +1499,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1048
1499
|
Determine overall assessment result for a control.
|
|
1049
1500
|
|
|
1050
1501
|
:param str control_id: Control identifier to check
|
|
1051
|
-
:return: Assessment result ('Pass' or '
|
|
1502
|
+
:return: Assessment result ('Pass', 'Fail', or 'Not Applicable')
|
|
1052
1503
|
:rtype: str
|
|
1053
1504
|
"""
|
|
1054
1505
|
is_failing = (
|
|
@@ -1056,7 +1507,18 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1056
1507
|
or control_id.lower() in self.failing_controls
|
|
1057
1508
|
or control_id.upper() in self.failing_controls
|
|
1058
1509
|
)
|
|
1059
|
-
|
|
1510
|
+
if is_failing:
|
|
1511
|
+
return "Fail"
|
|
1512
|
+
|
|
1513
|
+
is_not_applicable = (
|
|
1514
|
+
control_id in self.not_applicable_controls
|
|
1515
|
+
or control_id.lower() in self.not_applicable_controls
|
|
1516
|
+
or control_id.upper() in self.not_applicable_controls
|
|
1517
|
+
)
|
|
1518
|
+
if is_not_applicable:
|
|
1519
|
+
return self.NOT_APPLICABLE_LABEL
|
|
1520
|
+
|
|
1521
|
+
return "Pass"
|
|
1060
1522
|
|
|
1061
1523
|
def _get_control_compliance_items(self, control_id: str) -> List[ComplianceItem]:
|
|
1062
1524
|
"""
|
|
@@ -1093,48 +1555,11 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1093
1555
|
except Exception:
|
|
1094
1556
|
pass
|
|
1095
1557
|
|
|
1096
|
-
def _map_assets_to_control_component(self, sec_control: SecurityControl, items: List[ComplianceItem]) -> None:
|
|
1097
|
-
"""
|
|
1098
|
-
Map assets to control-specific components for organization.
|
|
1099
|
-
|
|
1100
|
-
:param SecurityControl sec_control: Security control to create component for
|
|
1101
|
-
:param List[ComplianceItem] items: Compliance items with asset references
|
|
1102
|
-
:return: None
|
|
1103
|
-
:rtype: None
|
|
1104
|
-
"""
|
|
1105
|
-
try:
|
|
1106
|
-
component_title = f"Control {sec_control.controlId}"
|
|
1107
|
-
component = self.components_by_title.get(component_title) if hasattr(self, "components_by_title") else None
|
|
1108
|
-
if not component:
|
|
1109
|
-
component = regscale_models.Component(
|
|
1110
|
-
title=component_title,
|
|
1111
|
-
componentType=regscale_models.ComponentType.Hardware,
|
|
1112
|
-
securityPlansId=self.plan_id,
|
|
1113
|
-
description=component_title,
|
|
1114
|
-
componentOwnerId=self.get_assessor_id(),
|
|
1115
|
-
).get_or_create()
|
|
1116
|
-
regscale_models.ComponentMapping(
|
|
1117
|
-
componentId=component.id,
|
|
1118
|
-
securityPlanId=self.plan_id,
|
|
1119
|
-
).get_or_create()
|
|
1120
|
-
if hasattr(self, "components_by_title"):
|
|
1121
|
-
self.components_by_title[component_title] = component
|
|
1122
|
-
|
|
1123
|
-
for item in items:
|
|
1124
|
-
asset = self.get_asset_by_identifier(getattr(item, "resource_id", ""))
|
|
1125
|
-
if not asset:
|
|
1126
|
-
continue
|
|
1127
|
-
regscale_models.AssetMapping(
|
|
1128
|
-
assetId=asset.id,
|
|
1129
|
-
componentId=component.id,
|
|
1130
|
-
).get_or_create_with_status()
|
|
1131
|
-
except Exception as map_exc: # noqa: BLE001
|
|
1132
|
-
logger.debug(f"Control-to-asset mapping skipped due to: {map_exc}")
|
|
1133
|
-
|
|
1134
1558
|
@staticmethod
|
|
1135
1559
|
def _parse_control_id(control_id: str) -> tuple[str, Optional[str]]:
|
|
1136
1560
|
"""
|
|
1137
1561
|
Parse a control id like 'AC-2(1)', 'AC-2 (1)', 'AC-2-1' into (base, sub).
|
|
1562
|
+
Normalizes leading zeros (e.g., AC-01 becomes AC-1).
|
|
1138
1563
|
|
|
1139
1564
|
Returns (base, None) when no subcontrol.
|
|
1140
1565
|
|
|
@@ -1148,8 +1573,22 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1148
1573
|
if not m:
|
|
1149
1574
|
return cid.upper(), None
|
|
1150
1575
|
base = m.group(1).upper()
|
|
1576
|
+
# Normalize leading zeros in base control number (e.g., AC-01 -> AC-1)
|
|
1577
|
+
if "-" in base:
|
|
1578
|
+
prefix, number = base.split("-", 1)
|
|
1579
|
+
try:
|
|
1580
|
+
normalized_number = str(int(number))
|
|
1581
|
+
base = f"{prefix}-{normalized_number}"
|
|
1582
|
+
except ValueError:
|
|
1583
|
+
pass # Keep original if conversion fails
|
|
1151
1584
|
# Subcontrol may be captured in group 2, 3, or 4 depending on the branch matched
|
|
1152
1585
|
sub = m.group(2) or m.group(3) or m.group(4)
|
|
1586
|
+
# Normalize leading zeros in subcontrol (e.g., 01 -> 1)
|
|
1587
|
+
if sub:
|
|
1588
|
+
try:
|
|
1589
|
+
sub = str(int(sub))
|
|
1590
|
+
except ValueError:
|
|
1591
|
+
pass # Keep original if conversion fails
|
|
1153
1592
|
return base, sub
|
|
1154
1593
|
|
|
1155
1594
|
@classmethod
|
|
@@ -1181,6 +1620,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1181
1620
|
def _normalize_control_id(control_id: str) -> tuple[str, Optional[str]]:
|
|
1182
1621
|
"""
|
|
1183
1622
|
Normalize control id to a canonical tuple (BASE, SUB) for set membership.
|
|
1623
|
+
Normalizes leading zeros (e.g., AC-01 becomes AC-1).
|
|
1184
1624
|
|
|
1185
1625
|
:param str control_id: Control identifier to normalize
|
|
1186
1626
|
:return: Tuple of (base_control, subcontrol) in canonical form
|
|
@@ -1192,7 +1632,21 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1192
1632
|
if not m:
|
|
1193
1633
|
return cid.upper(), None
|
|
1194
1634
|
base = m.group(1).upper()
|
|
1635
|
+
# Normalize leading zeros in base control number (e.g., AC-01 -> AC-1)
|
|
1636
|
+
if "-" in base:
|
|
1637
|
+
prefix, number = base.split("-", 1)
|
|
1638
|
+
try:
|
|
1639
|
+
normalized_number = str(int(number))
|
|
1640
|
+
base = f"{prefix}-{normalized_number}"
|
|
1641
|
+
except ValueError:
|
|
1642
|
+
pass # Keep original if conversion fails
|
|
1195
1643
|
sub = m.group(2) or m.group(3) or m.group(4)
|
|
1644
|
+
# Normalize leading zeros in subcontrol (e.g., 01 -> 1)
|
|
1645
|
+
if sub:
|
|
1646
|
+
try:
|
|
1647
|
+
sub = str(int(sub))
|
|
1648
|
+
except ValueError:
|
|
1649
|
+
pass # Keep original if conversion fails
|
|
1196
1650
|
return base, sub
|
|
1197
1651
|
|
|
1198
1652
|
def _create_control_assessment(
|
|
@@ -1259,8 +1713,8 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1259
1713
|
pass
|
|
1260
1714
|
else:
|
|
1261
1715
|
# Create new assessment
|
|
1716
|
+
# leadAssessorId will be set automatically from the token via the Assessment model's default_factory
|
|
1262
1717
|
assessment = Assessment(
|
|
1263
|
-
leadAssessorId=implementation.createdById,
|
|
1264
1718
|
title=f"{self.title} compliance assessment for {control_id.upper()}",
|
|
1265
1719
|
assessmentType="Control Testing",
|
|
1266
1720
|
plannedStart=get_current_datetime(),
|
|
@@ -1335,9 +1789,30 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1335
1789
|
] # NOSONAR
|
|
1336
1790
|
|
|
1337
1791
|
if compliance_items:
|
|
1338
|
-
# Group by result for summary
|
|
1339
|
-
|
|
1340
|
-
|
|
1792
|
+
# Group by result for more detailed summary
|
|
1793
|
+
compliant_count = len([item for item in compliance_items if item.compliance_result in self.PASS_STATUSES])
|
|
1794
|
+
noncompliant_count = len(
|
|
1795
|
+
[item for item in compliance_items if item.compliance_result in self.FAIL_STATUSES]
|
|
1796
|
+
)
|
|
1797
|
+
inconclusive_count = len(
|
|
1798
|
+
[item for item in compliance_items if item.compliance_result in self.INCONCLUSIVE_STATUSES]
|
|
1799
|
+
)
|
|
1800
|
+
not_applicable_count = len(
|
|
1801
|
+
[item for item in compliance_items if item.compliance_result in self.NOT_APPLICABLE_STATUSES]
|
|
1802
|
+
)
|
|
1803
|
+
|
|
1804
|
+
# Calculate confidence score based on evidence quality
|
|
1805
|
+
total_evaluated = compliant_count + noncompliant_count + inconclusive_count
|
|
1806
|
+
confidence_score = 0
|
|
1807
|
+
if total_evaluated > 0:
|
|
1808
|
+
# Confidence is based on the ratio of conclusive evidence (compliant + noncompliant) to total
|
|
1809
|
+
conclusive_count = compliant_count + noncompliant_count
|
|
1810
|
+
confidence_score = round((conclusive_count / total_evaluated) * 100, 2)
|
|
1811
|
+
|
|
1812
|
+
# Calculate compliance score
|
|
1813
|
+
compliance_score = 0
|
|
1814
|
+
if (compliant_count + noncompliant_count) > 0:
|
|
1815
|
+
compliance_score = round((compliant_count / (compliant_count + noncompliant_count)) * 100, 2)
|
|
1341
1816
|
|
|
1342
1817
|
# Count unique resources across all policy assessments for this control
|
|
1343
1818
|
unique_resources = set()
|
|
@@ -1346,7 +1821,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1346
1821
|
for item in compliance_items:
|
|
1347
1822
|
unique_resources.add(item.resource_id)
|
|
1348
1823
|
# Get policy name for aggregation
|
|
1349
|
-
if hasattr(item, "description"):
|
|
1824
|
+
if hasattr(item, "description") and item.description: # Check for non-empty description
|
|
1350
1825
|
unique_policies.add(
|
|
1351
1826
|
item.description[:50] + "..." if len(item.description) > 50 else item.description
|
|
1352
1827
|
)
|
|
@@ -1358,21 +1833,374 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1358
1833
|
f"""
|
|
1359
1834
|
<div style="margin-top: 20px;">
|
|
1360
1835
|
<h4>Aggregated Assessment Summary</h4>
|
|
1836
|
+
<p><strong>Control {control_id} Analysis:</strong>
|
|
1837
|
+
status=<span style="color: {result_color}; font-weight: bold;">{result.upper()}</span>,
|
|
1838
|
+
score={compliance_score:.2f},
|
|
1839
|
+
confidence={confidence_score:.0f},
|
|
1840
|
+
compliant={compliant_count},
|
|
1841
|
+
noncompliant={noncompliant_count},
|
|
1842
|
+
inconclusive={inconclusive_count}
|
|
1843
|
+
{f', not_applicable={not_applicable_count}' if not_applicable_count > 0 else ''}
|
|
1844
|
+
</p>
|
|
1845
|
+
<hr style="border: 0; border-top: 1px solid #e0e0e0; margin: 10px 0;">
|
|
1361
1846
|
<p><strong>Policy Assessments:</strong> {len(compliance_items)} total</p>
|
|
1362
1847
|
<p><strong>Unique Policies Tested:</strong> {len(unique_policies)}</p>
|
|
1363
1848
|
<p><strong>Unique Resources Assessed:</strong> {len(unique_resources)}</p>
|
|
1364
|
-
<
|
|
1365
|
-
<
|
|
1366
|
-
<p
|
|
1849
|
+
<hr style="border: 0; border-top: 1px solid #e0e0e0; margin: 10px 0;">
|
|
1850
|
+
<h5>Evidence Breakdown:</h5>
|
|
1851
|
+
<p style="margin-left: 20px;">
|
|
1852
|
+
<span style="color: #2e7d32;">✓ Compliant:</span> {compliant_count}<br>
|
|
1853
|
+
<span style="color: #d32f2f;">✗ Non-Compliant:</span> {noncompliant_count}<br>
|
|
1854
|
+
<span style="color: #ff9800;">⚠ Inconclusive:</span> {inconclusive_count}
|
|
1855
|
+
{f'<br><span style="color: #9e9e9e;">- Not Applicable:</span> {not_applicable_count}' if not_applicable_count > 0 else ''}
|
|
1856
|
+
</p>
|
|
1857
|
+
<hr style="border: 0; border-top: 1px solid #e0e0e0; margin: 10px 0;">
|
|
1858
|
+
<p><strong>Compliance Score:</strong> {compliance_score:.2f}%
|
|
1859
|
+
<span style="font-size: 0.9em; color: #666;">(compliant / (compliant + noncompliant))</span></p>
|
|
1860
|
+
<p><strong>Confidence Level:</strong> {confidence_score:.0f}%
|
|
1861
|
+
<span style="font-size: 0.9em; color: #666;">(conclusive evidence ratio)</span></p>
|
|
1862
|
+
<p><strong>Overall Control Result:</strong>
|
|
1863
|
+
<span style="color: {result_color}; font-weight: bold;">{result}</span></p>
|
|
1367
1864
|
</div>
|
|
1368
1865
|
"""
|
|
1369
1866
|
)
|
|
1370
1867
|
|
|
1868
|
+
# Add detailed failure information
|
|
1869
|
+
if result == "Fail" and noncompliant_count > 0:
|
|
1870
|
+
failed_items = [item for item in compliance_items if item.compliance_result in self.FAIL_STATUSES]
|
|
1871
|
+
html_parts.append(self._create_failure_details_section(failed_items))
|
|
1872
|
+
|
|
1873
|
+
return "\n".join(html_parts)
|
|
1874
|
+
|
|
1875
|
+
def _create_failure_details_section(self, failed_items: List[ComplianceItem]) -> str:
|
|
1876
|
+
"""
|
|
1877
|
+
Create detailed failure information section for failed compliance items.
|
|
1878
|
+
|
|
1879
|
+
:param List[ComplianceItem] failed_items: List of failed compliance items
|
|
1880
|
+
:return: HTML section with detailed failure information
|
|
1881
|
+
:rtype: str
|
|
1882
|
+
"""
|
|
1883
|
+
html_parts = [self._get_failure_section_header()]
|
|
1884
|
+
|
|
1885
|
+
for idx, item in enumerate(failed_items, 1):
|
|
1886
|
+
html_parts.append(self._process_failed_item(idx, item))
|
|
1887
|
+
|
|
1888
|
+
html_parts.append("</div>")
|
|
1889
|
+
return "\n".join(html_parts)
|
|
1890
|
+
|
|
1891
|
+
def _get_failure_section_header(self) -> str:
|
|
1892
|
+
"""Get the HTML header for failure details section."""
|
|
1893
|
+
return """
|
|
1894
|
+
<div style="margin-top: 20px; padding: 15px; background-color: #fff3e0;
|
|
1895
|
+
border-left: 4px solid #ff9800; border-radius: 5px;">
|
|
1896
|
+
<h4 style="color: #e65100; margin-top: 0;">Failed Evidence Details</h4>
|
|
1897
|
+
"""
|
|
1898
|
+
|
|
1899
|
+
def _process_failed_item(self, idx: int, item: ComplianceItem) -> str:
|
|
1900
|
+
"""
|
|
1901
|
+
Process a single failed item and return HTML.
|
|
1902
|
+
|
|
1903
|
+
:param int idx: The index of the failed item
|
|
1904
|
+
:param ComplianceItem item: The failed compliance item
|
|
1905
|
+
:return: HTML for the failed item
|
|
1906
|
+
:rtype: str
|
|
1907
|
+
"""
|
|
1908
|
+
if self._has_aws_evidence(item):
|
|
1909
|
+
return self._process_aws_item_with_evidence(idx, item)
|
|
1910
|
+
return self._process_non_aws_item(idx, item)
|
|
1911
|
+
|
|
1912
|
+
def _has_aws_evidence(self, item: ComplianceItem) -> bool:
|
|
1913
|
+
"""Check if item has AWS Audit Manager evidence."""
|
|
1914
|
+
return hasattr(item, "evidence_items") and item.evidence_items
|
|
1915
|
+
|
|
1916
|
+
def _process_aws_item_with_evidence(self, idx: int, item: ComplianceItem) -> str:
|
|
1917
|
+
"""Process AWS item with evidence details."""
|
|
1918
|
+
evidence_categories = self._categorize_evidence(item)
|
|
1919
|
+
|
|
1920
|
+
if not evidence_categories["failed"]:
|
|
1921
|
+
return ""
|
|
1922
|
+
|
|
1923
|
+
html_parts = []
|
|
1924
|
+
html_parts.append(self._create_failed_check_header(idx, item, evidence_categories))
|
|
1925
|
+
html_parts.append(self._create_failed_evidence_details(evidence_categories["failed"]))
|
|
1926
|
+
html_parts.append(self._add_remediation_guidance(item))
|
|
1927
|
+
html_parts.append("</div>")
|
|
1928
|
+
|
|
1371
1929
|
return "\n".join(html_parts)
|
|
1372
1930
|
|
|
1931
|
+
def _categorize_evidence(self, item: ComplianceItem) -> Dict[str, List[Any]]:
|
|
1932
|
+
"""
|
|
1933
|
+
Categorize evidence items by compliance status.
|
|
1934
|
+
|
|
1935
|
+
:param ComplianceItem item: The compliance item with evidence
|
|
1936
|
+
:return: Dictionary with categorized evidence
|
|
1937
|
+
:rtype: Dict[str, List[Any]]
|
|
1938
|
+
"""
|
|
1939
|
+
categories = {"failed": [], "compliant": [], "inconclusive": []}
|
|
1940
|
+
|
|
1941
|
+
for evidence in item.evidence_items:
|
|
1942
|
+
compliance_check = self._get_evidence_compliance_check(item, evidence)
|
|
1943
|
+
|
|
1944
|
+
if compliance_check == "FAILED":
|
|
1945
|
+
categories["failed"].append(evidence)
|
|
1946
|
+
elif compliance_check == "COMPLIANT":
|
|
1947
|
+
categories["compliant"].append(evidence)
|
|
1948
|
+
else:
|
|
1949
|
+
categories["inconclusive"].append(evidence)
|
|
1950
|
+
|
|
1951
|
+
return categories
|
|
1952
|
+
|
|
1953
|
+
def _get_evidence_compliance_check(self, item: ComplianceItem, evidence: Any) -> Optional[str]:
|
|
1954
|
+
"""Get compliance check result for evidence."""
|
|
1955
|
+
if hasattr(item, "_get_evidence_compliance"):
|
|
1956
|
+
return item._get_evidence_compliance(evidence)
|
|
1957
|
+
return None
|
|
1958
|
+
|
|
1959
|
+
def _create_failed_check_header(
|
|
1960
|
+
self, idx: int, item: ComplianceItem, evidence_categories: Dict[str, List[Any]]
|
|
1961
|
+
) -> str:
|
|
1962
|
+
"""Create HTML header for failed check."""
|
|
1963
|
+
return f"""
|
|
1964
|
+
<div style="margin-top: 15px; padding: 10px; background-color: #ffebee; border-radius: 3px;">
|
|
1965
|
+
<h5 style="color: #c62828; margin-top: 0;">
|
|
1966
|
+
Failed Check #{idx}: {item.control_id}
|
|
1967
|
+
</h5>
|
|
1968
|
+
<p><strong>Resource:</strong> {getattr(item, 'resource_name', item.resource_id)}</p>
|
|
1969
|
+
<p><strong>Evidence Summary:</strong>
|
|
1970
|
+
{len(evidence_categories["failed"])} failed, {len(evidence_categories["compliant"])} compliant,
|
|
1971
|
+
{len(evidence_categories["inconclusive"])} inconclusive</p>
|
|
1972
|
+
"""
|
|
1973
|
+
|
|
1974
|
+
def _create_failed_evidence_details(self, failed_evidence: List[Any]) -> str:
|
|
1975
|
+
"""Create HTML for failed evidence details."""
|
|
1976
|
+
html_parts = ['<div style="margin-top: 10px;"><strong>Failed Evidence:</strong><ul>']
|
|
1977
|
+
|
|
1978
|
+
# Limit to 10 failed evidence items
|
|
1979
|
+
for evidence in failed_evidence[:10]:
|
|
1980
|
+
html_parts.append(self._format_single_evidence(evidence))
|
|
1981
|
+
|
|
1982
|
+
html_parts.append("</ul></div>")
|
|
1983
|
+
return "\n".join(html_parts)
|
|
1984
|
+
|
|
1985
|
+
def _format_single_evidence(self, evidence: Dict[str, Any]) -> str:
|
|
1986
|
+
"""Format a single evidence item as HTML."""
|
|
1987
|
+
evidence_html = []
|
|
1988
|
+
evidence_source = evidence.get("dataSource", "Unknown source")
|
|
1989
|
+
evidence_id = evidence.get("id", "")[:50]
|
|
1990
|
+
|
|
1991
|
+
evidence_html.append(f"<li><strong>Source:</strong> {evidence_source}")
|
|
1992
|
+
|
|
1993
|
+
if evidence_id:
|
|
1994
|
+
evidence_html.append(f"<br><strong>Evidence ID:</strong> {evidence_id}")
|
|
1995
|
+
|
|
1996
|
+
resources_info = self._get_resources_info(evidence)
|
|
1997
|
+
if resources_info:
|
|
1998
|
+
evidence_html.append(f'<br><strong>Resources:</strong><ul><li>{"</li><li>".join(resources_info)}</li></ul>')
|
|
1999
|
+
|
|
2000
|
+
evidence_html.append("</li>")
|
|
2001
|
+
return "\n".join(evidence_html)
|
|
2002
|
+
|
|
2003
|
+
def _get_resources_info(self, evidence: Dict[str, Any]) -> List[str]:
|
|
2004
|
+
"""Extract resource information from evidence."""
|
|
2005
|
+
resources_info = []
|
|
2006
|
+
resources_included = evidence.get("resourcesIncluded", [])
|
|
2007
|
+
|
|
2008
|
+
# Limit to 5 resources per evidence
|
|
2009
|
+
for resource in resources_included[:5]:
|
|
2010
|
+
resource_str = self._format_resource(resource)
|
|
2011
|
+
if resource_str:
|
|
2012
|
+
resources_info.append(resource_str)
|
|
2013
|
+
|
|
2014
|
+
return resources_info
|
|
2015
|
+
|
|
2016
|
+
def _format_resource(self, resource: Dict[str, Any]) -> Optional[str]:
|
|
2017
|
+
"""Format a single resource as a string."""
|
|
2018
|
+
resource_type = resource.get("type", "Unknown")
|
|
2019
|
+
resource_value = resource.get("value", "")[:100]
|
|
2020
|
+
resource_check = resource.get("complianceCheck", "N/A")
|
|
2021
|
+
|
|
2022
|
+
if resource_value:
|
|
2023
|
+
return f"{resource_type}: {resource_value} (Status: {resource_check})"
|
|
2024
|
+
return None
|
|
2025
|
+
|
|
2026
|
+
def _add_remediation_guidance(self, item: ComplianceItem) -> str:
|
|
2027
|
+
"""Add remediation guidance if available."""
|
|
2028
|
+
if not (hasattr(item, "action_plan_instructions") and item.action_plan_instructions):
|
|
2029
|
+
return ""
|
|
2030
|
+
|
|
2031
|
+
instructions = item.action_plan_instructions[:500]
|
|
2032
|
+
truncated = "..." if len(item.action_plan_instructions) > 500 else ""
|
|
2033
|
+
|
|
2034
|
+
return f"""
|
|
2035
|
+
<div style="margin-top: 10px; padding: 8px; background-color: #e3f2fd;
|
|
2036
|
+
border-left: 3px solid #1976d2; border-radius: 3px;">
|
|
2037
|
+
<strong>Remediation Guidance:</strong><br>
|
|
2038
|
+
{instructions}{truncated}
|
|
2039
|
+
</div>
|
|
2040
|
+
"""
|
|
2041
|
+
|
|
2042
|
+
def _process_non_aws_item(self, idx: int, item: ComplianceItem) -> str:
|
|
2043
|
+
"""Process non-AWS items or items without evidence."""
|
|
2044
|
+
description = self._get_item_description(item)
|
|
2045
|
+
|
|
2046
|
+
return f"""
|
|
2047
|
+
<div style="margin-top: 15px; padding: 10px; background-color: #ffebee; border-radius: 3px;">
|
|
2048
|
+
<h5 style="color: #c62828; margin-top: 0;">Failed Check #{idx}: {item.control_id}</h5>
|
|
2049
|
+
<p><strong>Resource:</strong> {getattr(item, 'resource_name', item.resource_id)}</p>
|
|
2050
|
+
<p><strong>Description:</strong> {description}</p>
|
|
2051
|
+
</div>
|
|
2052
|
+
"""
|
|
2053
|
+
|
|
2054
|
+
def _get_item_description(self, item: ComplianceItem) -> str:
|
|
2055
|
+
"""Get truncated description from item."""
|
|
2056
|
+
if hasattr(item, "description"):
|
|
2057
|
+
return item.description[:200]
|
|
2058
|
+
return "N/A"
|
|
2059
|
+
|
|
2060
|
+
def _get_security_plan(self) -> Optional[regscale_models.SecurityPlan]:
|
|
2061
|
+
"""
|
|
2062
|
+
Get the security plan for this integration.
|
|
2063
|
+
|
|
2064
|
+
:return: SecurityPlan instance or None
|
|
2065
|
+
:rtype: Optional[regscale_models.SecurityPlan]
|
|
2066
|
+
"""
|
|
2067
|
+
if not self._security_plan_loaded:
|
|
2068
|
+
self._security_plan_loaded = True # Mark as attempted to prevent repeated calls
|
|
2069
|
+
try:
|
|
2070
|
+
logger.info(f"[SECURITY PLAN] Retrieving security plan with ID: {self.plan_id}")
|
|
2071
|
+
self._security_plan = SecurityPlan.get_object(object_id=self.plan_id)
|
|
2072
|
+
if self._security_plan:
|
|
2073
|
+
logger.info(
|
|
2074
|
+
f"[SECURITY PLAN] Retrieved security plan: {getattr(self._security_plan, 'systemName', 'N/A')}"
|
|
2075
|
+
)
|
|
2076
|
+
logger.info(
|
|
2077
|
+
f"[SECURITY PLAN] complianceSettingsId: {getattr(self._security_plan, 'complianceSettingsId', None)}"
|
|
2078
|
+
)
|
|
2079
|
+
else:
|
|
2080
|
+
logger.warning(f"[SECURITY PLAN] No security plan found with ID: {self.plan_id}")
|
|
2081
|
+
except Exception as e:
|
|
2082
|
+
logger.error(f"[SECURITY PLAN] Error getting security plan {self.plan_id}: {e}")
|
|
2083
|
+
# Don't set to None - keep as None but mark as loaded to prevent retry
|
|
2084
|
+
return self._security_plan
|
|
2085
|
+
|
|
2086
|
+
def _get_compliance_settings(self) -> Optional[regscale_models.ComplianceSettings]:
|
|
2087
|
+
"""
|
|
2088
|
+
Get compliance settings for the security plan.
|
|
2089
|
+
|
|
2090
|
+
:return: ComplianceSettings instance or None
|
|
2091
|
+
:rtype: Optional[regscale_models.ComplianceSettings]
|
|
2092
|
+
"""
|
|
2093
|
+
if not self._compliance_settings_loaded:
|
|
2094
|
+
self._compliance_settings_loaded = True # Mark as attempted to prevent repeated calls
|
|
2095
|
+
try:
|
|
2096
|
+
security_plan = self._get_security_plan()
|
|
2097
|
+
logger.debug(f"[COMPLIANCE SETTINGS] Security plan retrieved: {security_plan is not None}")
|
|
2098
|
+
if security_plan:
|
|
2099
|
+
logger.debug(
|
|
2100
|
+
f"[COMPLIANCE SETTINGS] Security plan systemName: {getattr(security_plan, 'systemName', 'N/A')}"
|
|
2101
|
+
)
|
|
2102
|
+
logger.debug(f"[COMPLIANCE SETTINGS] Security plan ID: {getattr(security_plan, 'id', 'N/A')}")
|
|
2103
|
+
logger.debug(
|
|
2104
|
+
f"[COMPLIANCE SETTINGS] Has complianceSettingsId attribute: {hasattr(security_plan, 'complianceSettingsId')}"
|
|
2105
|
+
)
|
|
2106
|
+
logger.debug(
|
|
2107
|
+
f"[COMPLIANCE SETTINGS] complianceSettingsId value: {getattr(security_plan, 'complianceSettingsId', 'None')}"
|
|
2108
|
+
)
|
|
2109
|
+
|
|
2110
|
+
if self._has_valid_compliance_settings_id(security_plan):
|
|
2111
|
+
self._compliance_settings = self._fetch_compliance_settings(security_plan)
|
|
2112
|
+
else:
|
|
2113
|
+
self._log_missing_compliance_settings_reason(security_plan)
|
|
2114
|
+
except Exception as e:
|
|
2115
|
+
logger.debug(f"Error getting compliance settings: {e}")
|
|
2116
|
+
import traceback
|
|
2117
|
+
|
|
2118
|
+
logger.debug(f"Full traceback: {traceback.format_exc()}")
|
|
2119
|
+
# Don't set to None - keep as None but mark as loaded to prevent retry
|
|
2120
|
+
return self._compliance_settings
|
|
2121
|
+
|
|
2122
|
+
def _has_valid_compliance_settings_id(self, security_plan) -> bool:
|
|
2123
|
+
"""Check if security plan has valid compliance settings ID."""
|
|
2124
|
+
return security_plan and hasattr(security_plan, "complianceSettingsId") and security_plan.complianceSettingsId
|
|
2125
|
+
|
|
2126
|
+
def _fetch_compliance_settings(self, security_plan) -> Optional[regscale_models.ComplianceSettings]:
|
|
2127
|
+
"""Fetch and log compliance settings."""
|
|
2128
|
+
logger.debug(f"Retrieving compliance settings with ID: {security_plan.complianceSettingsId}")
|
|
2129
|
+
compliance_settings = ComplianceSettings.get_object(object_id=security_plan.complianceSettingsId)
|
|
2130
|
+
|
|
2131
|
+
if compliance_settings:
|
|
2132
|
+
logger.debug(f"Using compliance settings: {compliance_settings.title}")
|
|
2133
|
+
logger.debug(
|
|
2134
|
+
f"Compliance settings has field groups: {bool(getattr(compliance_settings, 'complianceSettingsFieldGroups', None))}"
|
|
2135
|
+
)
|
|
2136
|
+
else:
|
|
2137
|
+
logger.debug(f"No compliance settings found for ID: {security_plan.complianceSettingsId}")
|
|
2138
|
+
|
|
2139
|
+
return compliance_settings
|
|
2140
|
+
|
|
2141
|
+
def _log_missing_compliance_settings_reason(self, security_plan) -> None:
|
|
2142
|
+
"""Log specific reason why compliance settings are not available."""
|
|
2143
|
+
if not security_plan:
|
|
2144
|
+
logger.debug("Security plan not found")
|
|
2145
|
+
elif not hasattr(security_plan, "complianceSettingsId"):
|
|
2146
|
+
logger.debug("Security plan does not have complianceSettingsId attribute")
|
|
2147
|
+
elif not security_plan.complianceSettingsId:
|
|
2148
|
+
logger.debug("Security plan has no complianceSettingsId set")
|
|
2149
|
+
|
|
2150
|
+
def _get_implementation_status_from_result(self, result: str, override: Optional[str] = None) -> str:
|
|
2151
|
+
"""
|
|
2152
|
+
Get implementation status based on assessment result using enum-based mappings.
|
|
2153
|
+
Results are cached to avoid repeated calculations for the same input.
|
|
2154
|
+
|
|
2155
|
+
:param str result: Assessment result ('Pass', 'Fail', 'Not Applicable', etc.)
|
|
2156
|
+
:param Optional[str] override: Optional override value to use instead of the mapping
|
|
2157
|
+
:return: Implementation status string
|
|
2158
|
+
:rtype: str
|
|
2159
|
+
"""
|
|
2160
|
+
# Use override directly if provided
|
|
2161
|
+
if override:
|
|
2162
|
+
return override
|
|
2163
|
+
|
|
2164
|
+
# Handle None or empty result
|
|
2165
|
+
if not result:
|
|
2166
|
+
logger.warning("Received None or empty result for status mapping, defaulting to 'Unknown'")
|
|
2167
|
+
return "Unknown"
|
|
2168
|
+
|
|
2169
|
+
# Check cache first to avoid repeated calculations
|
|
2170
|
+
cache_key = result.lower().strip()
|
|
2171
|
+
if cache_key in self._status_mapping_cache:
|
|
2172
|
+
cached_status = self._status_mapping_cache[cache_key]
|
|
2173
|
+
logger.debug(f"[STATUS MAPPING] Using cached mapping for '{result}': '{cached_status}'")
|
|
2174
|
+
return cached_status
|
|
2175
|
+
|
|
2176
|
+
logger.info(f"[STATUS MAPPING] Getting implementation status for result: {result}")
|
|
2177
|
+
|
|
2178
|
+
# Try to use the compliance settings mapping function
|
|
2179
|
+
compliance_settings = self._get_compliance_settings()
|
|
2180
|
+
if compliance_settings:
|
|
2181
|
+
logger.info(f"[STATUS MAPPING] Using compliance settings '{compliance_settings.title}' for mapping")
|
|
2182
|
+
try:
|
|
2183
|
+
mapped_status = compliance_settings.get_implementation_status_for_result(result, None)
|
|
2184
|
+
logger.info(f"[STATUS MAPPING] Mapped '{result}' to '{mapped_status}' using compliance settings")
|
|
2185
|
+
# Cache the result
|
|
2186
|
+
self._status_mapping_cache[cache_key] = mapped_status
|
|
2187
|
+
return mapped_status
|
|
2188
|
+
except Exception as e:
|
|
2189
|
+
logger.warning(f"[STATUS MAPPING] Error using compliance settings mapping: {e}")
|
|
2190
|
+
|
|
2191
|
+
# Fallback: Use the class method directly if no compliance settings instance
|
|
2192
|
+
framework = self._detect_compliance_framework()
|
|
2193
|
+
logger.info(f"[STATUS MAPPING] Using framework '{framework}' for fallback mapping")
|
|
2194
|
+
mapped_status = ComplianceSettings.get_status_mapping(framework, result, None)
|
|
2195
|
+
logger.info(f"[STATUS MAPPING] Mapped '{result}' to '{mapped_status}' using fallback")
|
|
2196
|
+
# Cache the result
|
|
2197
|
+
self._status_mapping_cache[cache_key] = mapped_status
|
|
2198
|
+
return mapped_status
|
|
2199
|
+
|
|
1373
2200
|
def _update_implementation_status(self, implementation: ControlImplementation, result: str) -> None:
|
|
1374
2201
|
"""
|
|
1375
2202
|
Update control implementation status based on assessment result.
|
|
2203
|
+
Uses compliance settings from the security plan if available, otherwise falls back to defaults.
|
|
1376
2204
|
|
|
1377
2205
|
:param ControlImplementation implementation: Control implementation to update
|
|
1378
2206
|
:param str result: Assessment result ('Pass' or 'Fail')
|
|
@@ -1380,15 +2208,30 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1380
2208
|
:rtype: None
|
|
1381
2209
|
"""
|
|
1382
2210
|
try:
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
else:
|
|
1386
|
-
new_status = "In Remediation"
|
|
2211
|
+
# Get status from compliance settings or fallback to default
|
|
2212
|
+
new_status = self._get_implementation_status_from_result(result)
|
|
1387
2213
|
|
|
1388
2214
|
# Update implementation status
|
|
1389
2215
|
implementation.status = new_status
|
|
1390
2216
|
implementation.dateLastAssessed = get_current_datetime()
|
|
1391
2217
|
implementation.lastAssessmentResult = result
|
|
2218
|
+
|
|
2219
|
+
# Ensure required fields are set if empty
|
|
2220
|
+
if not implementation.responsibility:
|
|
2221
|
+
implementation.responsibility = ControlImplementation.get_default_responsibility(
|
|
2222
|
+
parent_id=implementation.parentId
|
|
2223
|
+
)
|
|
2224
|
+
logger.debug(
|
|
2225
|
+
f"Setting default responsibility for implementation {implementation.id}: {implementation.responsibility}"
|
|
2226
|
+
)
|
|
2227
|
+
|
|
2228
|
+
if not implementation.implementation:
|
|
2229
|
+
control_id = (
|
|
2230
|
+
getattr(implementation.control, "controlId", "control") if implementation.control else "control"
|
|
2231
|
+
)
|
|
2232
|
+
implementation.implementation = f"Implementation details for {control_id} will be documented."
|
|
2233
|
+
logger.debug(f"Setting default implementation statement for implementation {implementation.id}")
|
|
2234
|
+
|
|
1392
2235
|
implementation.save()
|
|
1393
2236
|
|
|
1394
2237
|
# Update objectives if they exist
|
|
@@ -1401,7 +2244,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1401
2244
|
objective.status = new_status
|
|
1402
2245
|
objective.save()
|
|
1403
2246
|
|
|
1404
|
-
logger.debug(f"Updated implementation status to {new_status}")
|
|
2247
|
+
logger.debug(f"Updated implementation status to {new_status} (from compliance settings)")
|
|
1405
2248
|
|
|
1406
2249
|
except Exception as e:
|
|
1407
2250
|
logger.error(f"Error updating implementation status: {e}")
|
|
@@ -1558,6 +2401,8 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1558
2401
|
"""
|
|
1559
2402
|
Create or update an issue from a finding, using cache to prevent duplicates.
|
|
1560
2403
|
|
|
2404
|
+
Properly handles milestone creation for compliance integrations.
|
|
2405
|
+
|
|
1561
2406
|
:param str title: Issue title
|
|
1562
2407
|
:param IntegrationFinding finding: The finding to create issue from
|
|
1563
2408
|
:return: Created or updated issue
|
|
@@ -1568,17 +2413,21 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1568
2413
|
|
|
1569
2414
|
# Check for existing issue by external_id first
|
|
1570
2415
|
external_id = finding.external_id
|
|
2416
|
+
logger.debug(f"Looking for existing issue with external_id: '{external_id}'")
|
|
1571
2417
|
existing_issue = self._find_existing_issue_cached(external_id)
|
|
1572
2418
|
|
|
1573
2419
|
if existing_issue:
|
|
1574
2420
|
logger.debug(
|
|
1575
|
-
f"Found existing issue {existing_issue.id} for external_id {external_id}, updating instead of creating"
|
|
2421
|
+
f"Found existing issue {existing_issue.id} (other_identifier: '{existing_issue.otherIdentifier}') for lookup external_id '{external_id}', updating instead of creating"
|
|
1576
2422
|
)
|
|
1577
2423
|
|
|
2424
|
+
# Store original status for milestone comparison
|
|
2425
|
+
original_status = existing_issue.status
|
|
2426
|
+
|
|
1578
2427
|
# Update existing issue with new finding data
|
|
1579
2428
|
existing_issue.title = title
|
|
1580
2429
|
existing_issue.description = finding.description
|
|
1581
|
-
existing_issue.
|
|
2430
|
+
existing_issue.severityLevel = finding.severity
|
|
1582
2431
|
existing_issue.status = finding.status
|
|
1583
2432
|
# Ensure affectedControls is updated from the finding's control id
|
|
1584
2433
|
try:
|
|
@@ -1598,10 +2447,46 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1598
2447
|
except Exception:
|
|
1599
2448
|
pass
|
|
1600
2449
|
existing_issue.dateLastUpdated = self.scan_date
|
|
2450
|
+
# Set organization ID based on Issue Owner or SSP Owner hierarchy
|
|
2451
|
+
existing_issue.orgId = self.determine_issue_organization_id(existing_issue.issueOwnerId)
|
|
1601
2452
|
existing_issue.save()
|
|
1602
2453
|
|
|
2454
|
+
# Create milestone if status changed
|
|
2455
|
+
# Reconstruct original issue state for comparison
|
|
2456
|
+
original_issue = regscale_models.Issue()
|
|
2457
|
+
original_issue.status = original_status
|
|
2458
|
+
self._create_milestones_for_updated_issue(existing_issue, finding, original_issue)
|
|
2459
|
+
|
|
1603
2460
|
return existing_issue
|
|
1604
2461
|
else:
|
|
1605
2462
|
# No existing issue found, create new one using parent method
|
|
1606
2463
|
logger.debug(f"No existing issue found for external_id {external_id}, creating new issue")
|
|
1607
2464
|
return super().create_or_update_issue_from_finding(title, finding)
|
|
2465
|
+
|
|
2466
|
+
def _create_milestones_for_updated_issue(
|
|
2467
|
+
self,
|
|
2468
|
+
issue: regscale_models.Issue,
|
|
2469
|
+
finding: IntegrationFinding,
|
|
2470
|
+
original_issue: regscale_models.Issue,
|
|
2471
|
+
) -> None:
|
|
2472
|
+
"""
|
|
2473
|
+
Create milestones for an updated issue in compliance integration.
|
|
2474
|
+
|
|
2475
|
+
This method handles both status transition milestones and backfilling of missing
|
|
2476
|
+
creation milestones for existing issues.
|
|
2477
|
+
|
|
2478
|
+
:param regscale_models.Issue issue: The updated issue
|
|
2479
|
+
:param IntegrationFinding finding: The finding data
|
|
2480
|
+
:param regscale_models.Issue original_issue: Original state for comparison
|
|
2481
|
+
"""
|
|
2482
|
+
milestone_manager = self.get_milestone_manager()
|
|
2483
|
+
|
|
2484
|
+
# First, ensure the issue has a creation milestone (backfill if missing)
|
|
2485
|
+
milestone_manager.ensure_creation_milestone_exists(issue=issue, finding=finding)
|
|
2486
|
+
|
|
2487
|
+
# Then, handle status transition milestones
|
|
2488
|
+
milestone_manager.create_milestones_for_issue(
|
|
2489
|
+
issue=issue,
|
|
2490
|
+
finding=finding,
|
|
2491
|
+
existing_issue=original_issue,
|
|
2492
|
+
)
|