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
|
@@ -60,6 +60,8 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
60
60
|
|
|
61
61
|
_pending_updates: ClassVar[Dict[str, Set[int]]] = {}
|
|
62
62
|
_pending_creations: ClassVar[Dict[str, Set[str]]] = {}
|
|
63
|
+
_session_created_ids: ClassVar[Set[int]] = set() # Track IDs created in current session for dedupe detection
|
|
64
|
+
_ignore_has_changed: bool = False
|
|
63
65
|
|
|
64
66
|
id: int = 0
|
|
65
67
|
extra_data: Dict[str, Any] = Field(default={}, exclude=True)
|
|
@@ -181,6 +183,7 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
181
183
|
def _get_cache_key(cls, obj: T, defaults: Optional[Dict[str, Any]] = None) -> str:
|
|
182
184
|
"""
|
|
183
185
|
Generate a cache key based on the object's unique fields using SHA256 hash.
|
|
186
|
+
Includes parentId to scope cache keys to individual security plans/parents.
|
|
184
187
|
|
|
185
188
|
:param T obj: The object to generate a key for
|
|
186
189
|
:param Optional[Dict[str, Any]] defaults: Dictionary of default values to apply to the object, defaults to None
|
|
@@ -188,6 +191,14 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
188
191
|
:rtype: str
|
|
189
192
|
"""
|
|
190
193
|
defaults = defaults or {}
|
|
194
|
+
|
|
195
|
+
# Get parent ID to scope cache keys to individual parents (e.g., security plans)
|
|
196
|
+
# This prevents assets/issues/etc. from being matched across different security plans
|
|
197
|
+
parent_id = getattr(obj, cls._parent_id_field, None)
|
|
198
|
+
# Check if parent_id is None and try defaults if available
|
|
199
|
+
if parent_id is None and defaults:
|
|
200
|
+
parent_id = defaults.get(cls._parent_id_field)
|
|
201
|
+
|
|
191
202
|
# Iterate over each set of unique fields
|
|
192
203
|
for fields in cls.get_unique_fields():
|
|
193
204
|
unique_fields = []
|
|
@@ -206,11 +217,12 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
206
217
|
# If all fields in the current set have values, use them to generate the cache key
|
|
207
218
|
if len(unique_fields) == len(fields):
|
|
208
219
|
unique_string = ":".join(unique_fields)
|
|
209
|
-
|
|
220
|
+
# Include parent_id in cache key to scope to individual security plans/parents
|
|
221
|
+
cache_key = f"{cls.__name__}:{parent_id}:{unique_string}"
|
|
210
222
|
return cache_key
|
|
211
223
|
|
|
212
224
|
# Fallback if no complete set of unique fields is found, use object ID
|
|
213
|
-
return f"{cls.__name__}:{obj.get_object_id()}"
|
|
225
|
+
return f"{cls.__name__}:{parent_id}:{obj.get_object_id()}"
|
|
214
226
|
|
|
215
227
|
@classmethod
|
|
216
228
|
def get_cached_object(cls, cache_key: str) -> Optional[T]:
|
|
@@ -502,31 +514,93 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
502
514
|
:return: A list of unique fields
|
|
503
515
|
:rtype: List[List[str]]
|
|
504
516
|
"""
|
|
505
|
-
sample_format = {"uniqueOverride": {"asset": ["ipAddress"]}}
|
|
506
517
|
config = Application().config
|
|
507
518
|
|
|
519
|
+
# First, ensure _unique_fields is in new format (List[List[str]])
|
|
520
|
+
cls._ensure_unique_fields_format()
|
|
521
|
+
|
|
508
522
|
try:
|
|
509
523
|
primary = config.get("uniqueOverride", {}).get(cls.__name__.lower())
|
|
510
524
|
if primary:
|
|
511
|
-
|
|
512
|
-
raise ValueError(
|
|
513
|
-
f"Invalid config format in uniqueOverride.{cls.__name__.lower()}, the configuration must be in a format like so:\n{dump(sample_format, default_flow_style=False)}"
|
|
514
|
-
)
|
|
515
|
-
if primary != cls._unique_fields:
|
|
516
|
-
if all(attr in cls.model_fields for attr in primary):
|
|
517
|
-
if primary not in cls._unique_fields:
|
|
518
|
-
cls._unique_fields.insert(1, primary)
|
|
519
|
-
else:
|
|
520
|
-
# Move primary to index 1 if it exists
|
|
521
|
-
cls._unique_fields.insert(1, cls._unique_fields.pop(cls._unique_fields.index(primary)))
|
|
522
|
-
else:
|
|
523
|
-
raise ValueError(
|
|
524
|
-
f"One or more invalid attribute(s) detected: {primary}, falling back on default unique fields for type: {cls.__name__.lower()}"
|
|
525
|
-
)
|
|
525
|
+
cls._process_primary_override(primary)
|
|
526
526
|
except ValueError as e:
|
|
527
527
|
logger.warning(e)
|
|
528
528
|
return cls._unique_fields
|
|
529
529
|
|
|
530
|
+
@classmethod
|
|
531
|
+
def _ensure_unique_fields_format(cls) -> None:
|
|
532
|
+
"""
|
|
533
|
+
Ensure _unique_fields is in the new format (List[List[str]]).
|
|
534
|
+
|
|
535
|
+
:rtype: None
|
|
536
|
+
"""
|
|
537
|
+
# Check if it's still in old format (List[str])
|
|
538
|
+
if cls._unique_fields and isinstance(cls._unique_fields[0], str):
|
|
539
|
+
# Convert old format to new format
|
|
540
|
+
cls._unique_fields = [cls._unique_fields] # type: ignore
|
|
541
|
+
|
|
542
|
+
@classmethod
|
|
543
|
+
def _process_primary_override(cls, primary: List[str]) -> None:
|
|
544
|
+
"""
|
|
545
|
+
Process the primary override configuration.
|
|
546
|
+
|
|
547
|
+
:param List[str] primary: The primary override fields
|
|
548
|
+
:raises ValueError: If the primary fields are invalid
|
|
549
|
+
:rtype: None
|
|
550
|
+
"""
|
|
551
|
+
cls._validate_primary_format(primary)
|
|
552
|
+
|
|
553
|
+
# Now cls._unique_fields is guaranteed to be List[List[str]]
|
|
554
|
+
# Check if primary is different from any existing unique field set
|
|
555
|
+
if primary not in cls._unique_fields:
|
|
556
|
+
cls._handle_new_primary_fields(primary)
|
|
557
|
+
|
|
558
|
+
@classmethod
|
|
559
|
+
def _validate_primary_format(cls, primary: List[str]) -> None:
|
|
560
|
+
"""
|
|
561
|
+
Validate the format of the primary override configuration.
|
|
562
|
+
|
|
563
|
+
:param List[str] primary: The primary override fields
|
|
564
|
+
:raises ValueError: If the primary format is invalid
|
|
565
|
+
:rtype: None
|
|
566
|
+
"""
|
|
567
|
+
if not isinstance(primary, list):
|
|
568
|
+
sample_format = {"uniqueOverride": {"asset": ["ipAddress"]}}
|
|
569
|
+
raise ValueError(
|
|
570
|
+
f"Invalid config format in uniqueOverride.{cls.__name__.lower()}, the configuration must be in a format like so:\n{dump(sample_format, default_flow_style=False)}"
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
@classmethod
|
|
574
|
+
def _handle_new_primary_fields(cls, primary: List[str]) -> None:
|
|
575
|
+
"""
|
|
576
|
+
Handle new primary fields that are not in existing unique fields.
|
|
577
|
+
|
|
578
|
+
:param List[str] primary: The primary override fields
|
|
579
|
+
:raises ValueError: If any attributes are invalid
|
|
580
|
+
:rtype: None
|
|
581
|
+
"""
|
|
582
|
+
if all(attr in cls.model_fields for attr in primary):
|
|
583
|
+
cls._insert_primary_fields(primary)
|
|
584
|
+
else:
|
|
585
|
+
raise ValueError(
|
|
586
|
+
f"One or more invalid attribute(s) detected: {primary}, falling back on default unique fields for type: {cls.__name__.lower()}"
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
@classmethod
|
|
590
|
+
def _insert_primary_fields(cls, primary: List[str]) -> None:
|
|
591
|
+
"""
|
|
592
|
+
Insert primary fields into the unique fields list.
|
|
593
|
+
|
|
594
|
+
:param List[str] primary: The primary override fields
|
|
595
|
+
:rtype: None
|
|
596
|
+
"""
|
|
597
|
+
# Check if primary already exists in the list
|
|
598
|
+
if primary not in cls._unique_fields:
|
|
599
|
+
cls._unique_fields.insert(1, primary)
|
|
600
|
+
else:
|
|
601
|
+
# Move primary to index 1 if it exists
|
|
602
|
+
cls._unique_fields.insert(1, cls._unique_fields.pop(cls._unique_fields.index(primary)))
|
|
603
|
+
|
|
530
604
|
@classmethod
|
|
531
605
|
def _get_endpoints(cls) -> ConfigDict:
|
|
532
606
|
"""
|
|
@@ -612,19 +686,37 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
612
686
|
raise NotImplementedError(f"_unique_fields not defined for {self.__class__.__name__}")
|
|
613
687
|
|
|
614
688
|
parent_id = getattr(self, parent_id_field or self._parent_id_field, None)
|
|
689
|
+
logger.debug(
|
|
690
|
+
f"find_by_unique for {self.__class__.__name__}: parent_id={parent_id} (type: {type(parent_id).__name__}), "
|
|
691
|
+
f"parent_id_field={parent_id_field or self._parent_id_field}"
|
|
692
|
+
)
|
|
615
693
|
if parent_id is None:
|
|
616
694
|
raise ValueError(f"Parent ID not found for {self.__class__.__name__}")
|
|
617
695
|
|
|
618
696
|
parent_module = getattr(self, "parentModule", getattr(self, "parent_module", ""))
|
|
697
|
+
logger.debug(f"find_by_unique for {self.__class__.__name__}: parent_module={parent_module}")
|
|
619
698
|
cache_key = self._get_cache_key(self)
|
|
620
699
|
|
|
621
700
|
with self._get_lock(cache_key):
|
|
622
701
|
# Check cache first
|
|
623
702
|
if cached_object := self.get_cached_object(cache_key):
|
|
703
|
+
logger.debug(f"find_by_unique for {self.__class__.__name__}: Found in cache")
|
|
624
704
|
return cached_object
|
|
625
705
|
|
|
626
706
|
# Get all instances from parent
|
|
707
|
+
logger.debug(
|
|
708
|
+
f"find_by_unique for {self.__class__.__name__}: Calling get_all_by_parent with "
|
|
709
|
+
f"parent_id={parent_id}, parent_module={parent_module}"
|
|
710
|
+
)
|
|
627
711
|
instances: List[T] = self.get_all_by_parent(parent_id=parent_id, parent_module=parent_module)
|
|
712
|
+
logger.debug(
|
|
713
|
+
f"find_by_unique for {self.__class__.__name__}: Retrieved {len(instances)} instances from parent_id={parent_id}"
|
|
714
|
+
)
|
|
715
|
+
if instances:
|
|
716
|
+
parent_ids = set(getattr(inst, self._parent_id_field, None) for inst in instances[:10])
|
|
717
|
+
logger.debug(
|
|
718
|
+
f"find_by_unique for {self.__class__.__name__}: Sample parent_ids in results: {parent_ids}"
|
|
719
|
+
)
|
|
628
720
|
|
|
629
721
|
# Try to find matching instance using unique fields
|
|
630
722
|
for keys in self._unique_fields:
|
|
@@ -642,8 +734,14 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
642
734
|
None,
|
|
643
735
|
)
|
|
644
736
|
if matching_instance:
|
|
737
|
+
matched_parent_id = getattr(matching_instance, self._parent_id_field, None)
|
|
738
|
+
logger.debug(
|
|
739
|
+
f"find_by_unique for {self.__class__.__name__}: Found match using fields {keys}, "
|
|
740
|
+
f"matched instance parent_id={matched_parent_id}, current parent_id={parent_id}"
|
|
741
|
+
)
|
|
645
742
|
return matching_instance
|
|
646
743
|
|
|
744
|
+
logger.debug(f"find_by_unique for {self.__class__.__name__}: No matching instance found")
|
|
647
745
|
return None
|
|
648
746
|
|
|
649
747
|
def get_or_create_with_status(self: T, bulk: bool = False) -> Tuple[bool, T]:
|
|
@@ -665,17 +763,45 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
665
763
|
self.cache_object(instance)
|
|
666
764
|
return False, instance
|
|
667
765
|
else:
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
766
|
+
try:
|
|
767
|
+
created_instance = self.create(bulk=bulk)
|
|
768
|
+
self.cache_object(created_instance)
|
|
769
|
+
return True, created_instance
|
|
770
|
+
except APIInsertionError as e:
|
|
771
|
+
# Check if this is a duplicate error (race condition in threading)
|
|
772
|
+
error_str = str(e).lower()
|
|
773
|
+
if "already exists" in error_str or "mapping already exists" in error_str:
|
|
774
|
+
logger.debug(
|
|
775
|
+
f"Race condition detected for {self.__class__.__name__}, retrying find_by_unique: {e}"
|
|
776
|
+
)
|
|
777
|
+
# Clear the cache to force a fresh lookup
|
|
778
|
+
self.clear_cache()
|
|
779
|
+
# Try to find the instance again - another thread may have created it
|
|
780
|
+
instance = self.find_by_unique()
|
|
781
|
+
if instance:
|
|
782
|
+
self.cache_object(instance)
|
|
783
|
+
logger.debug(
|
|
784
|
+
f"Successfully found existing {self.__class__.__name__} after duplicate creation error, ID: {instance.id}"
|
|
785
|
+
)
|
|
786
|
+
return False, instance
|
|
787
|
+
else:
|
|
788
|
+
# If we still can't find it, log error but don't stop the process
|
|
789
|
+
logger.error(
|
|
790
|
+
f"Failed to find {self.__class__.__name__} after duplicate creation error: {e}"
|
|
791
|
+
)
|
|
792
|
+
# Return None to indicate creation failed but don't raise
|
|
793
|
+
return False, None
|
|
794
|
+
else:
|
|
795
|
+
# Not a duplicate error, re-raise
|
|
796
|
+
logger.error(f"Failed to create object: {self.__class__.__name__} creation error: {e}")
|
|
671
797
|
|
|
672
|
-
def get_or_create(self: T, bulk: bool = False) -> T:
|
|
798
|
+
def get_or_create(self: T, bulk: bool = False) -> Optional[T]:
|
|
673
799
|
"""
|
|
674
800
|
Get or create an instance.
|
|
675
801
|
|
|
676
802
|
:param bool bulk: Whether to perform a bulk create operation, defaults to False
|
|
677
|
-
:return: The instance
|
|
678
|
-
:rtype: T
|
|
803
|
+
:return: The instance or None if creation failed due to race condition
|
|
804
|
+
:rtype: Optional[T]
|
|
679
805
|
"""
|
|
680
806
|
_, instance = self.get_or_create_with_status(bulk=bulk)
|
|
681
807
|
return instance
|
|
@@ -705,15 +831,15 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
705
831
|
bulk_create: bool = False,
|
|
706
832
|
bulk_update: bool = False,
|
|
707
833
|
defaults: Optional[Dict[str, Any]] = None,
|
|
708
|
-
) -> Tuple[
|
|
834
|
+
) -> Tuple[str, T]:
|
|
709
835
|
"""
|
|
710
836
|
Create or update an instance. Use cache methods to retrieve and store instances based on unique fields.
|
|
711
837
|
|
|
712
838
|
:param bool bulk_create: Whether to perform a bulk create, defaults to False
|
|
713
839
|
:param bool bulk_update: Whether to perform a bulk update, defaults to False
|
|
714
840
|
:param Optional[Dict[str, Any]] defaults: Dictionary of default values to apply to the instance if it is created, defaults to {}
|
|
715
|
-
:return:
|
|
716
|
-
:rtype: Tuple[
|
|
841
|
+
:return: Tuple of (status, instance) where status is "created", "updated", or "deduped"
|
|
842
|
+
:rtype: Tuple[str, T]
|
|
717
843
|
"""
|
|
718
844
|
logger.debug(f"Starting create_or_update for {self.__class__.__name__}: #{getattr(self, 'id', '')}")
|
|
719
845
|
|
|
@@ -727,44 +853,106 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
727
853
|
instance = cached_object or self.find_by_unique()
|
|
728
854
|
|
|
729
855
|
if instance:
|
|
730
|
-
|
|
731
|
-
logger.debug(f"Found {'cached' if cached_object else 'existing'} instance of {self.__class__.__name__}")
|
|
732
|
-
# Update the current object's ID with the found instance's ID
|
|
733
|
-
self.id = instance.id
|
|
734
|
-
# If the object has a 'dateCreated' attribute, update it
|
|
735
|
-
if hasattr(self, "dateCreated"):
|
|
736
|
-
self.dateCreated = instance.dateCreated # noqa
|
|
737
|
-
|
|
738
|
-
# Update the _original_data attribute with the instance data
|
|
739
|
-
self._original_data = instance.dict(exclude_unset=True)
|
|
740
|
-
|
|
741
|
-
# Check if the current object has any changes compared to the found instance
|
|
742
|
-
if self.has_changed():
|
|
743
|
-
logger.debug(f"Instance of {self.__class__.__name__} has changed, updating")
|
|
744
|
-
# Save the changes, potentially in bulk
|
|
745
|
-
updated_instance = self.save(bulk=bulk_update)
|
|
746
|
-
# Update the cache with the new instance
|
|
747
|
-
self.cache_object(updated_instance)
|
|
748
|
-
# Return the updated instance, optionally with a flag indicating it wasn't newly created
|
|
749
|
-
return False, updated_instance
|
|
750
|
-
|
|
751
|
-
# If no changes, return the existing instance
|
|
752
|
-
return False, instance
|
|
856
|
+
return self._handle_existing_instance(instance, cached_object, bulk_update)
|
|
753
857
|
|
|
754
858
|
# No existing instance was found, so create a new one
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
859
|
+
return self._handle_new_instance(defaults, bulk_create)
|
|
860
|
+
|
|
861
|
+
def _handle_existing_instance(self: T, instance: T, cached_object: Optional[T], bulk_update: bool) -> Tuple[str, T]:
|
|
862
|
+
"""
|
|
863
|
+
Handle processing of an existing instance found in cache or database.
|
|
864
|
+
|
|
865
|
+
:param T instance: The found instance
|
|
866
|
+
:param Optional[T] cached_object: The cached object if found in cache
|
|
867
|
+
:param bool bulk_update: Whether to perform a bulk update
|
|
868
|
+
:return: Tuple of (status, instance) where status is "updated" or "deduped"
|
|
869
|
+
:rtype: Tuple[str, T]
|
|
870
|
+
"""
|
|
871
|
+
# An existing instance was found (either in cache or database)
|
|
872
|
+
logger.debug(f"Found {'cached' if cached_object else 'existing'} instance of {self.__class__.__name__}")
|
|
873
|
+
|
|
874
|
+
# Update current object with instance data
|
|
875
|
+
self._sync_with_existing_instance(instance)
|
|
876
|
+
|
|
877
|
+
# Check if the current object has any changes compared to the found instance
|
|
878
|
+
if self.has_changed():
|
|
879
|
+
return self._update_existing_instance(bulk_update, is_cached=cached_object is not None)
|
|
880
|
+
|
|
881
|
+
# If no changes, determine if this is a dedupe or update
|
|
882
|
+
# Dedupe = found in cache OR was created during this session
|
|
883
|
+
is_dedupe = cached_object is not None or instance.id in self.__class__._session_created_ids
|
|
884
|
+
status = "deduped" if is_dedupe else "updated"
|
|
885
|
+
return status, instance
|
|
886
|
+
|
|
887
|
+
def _sync_with_existing_instance(self: T, instance: T) -> None:
|
|
888
|
+
"""
|
|
889
|
+
Synchronize current object with existing instance data.
|
|
890
|
+
|
|
891
|
+
:param T instance: The existing instance to sync with
|
|
892
|
+
:rtype: None
|
|
893
|
+
"""
|
|
894
|
+
# Update the current object's ID with the found instance's ID
|
|
895
|
+
self.id = instance.id
|
|
896
|
+
# If the object has a 'dateCreated' attribute, update it
|
|
897
|
+
if hasattr(self, "dateCreated"):
|
|
898
|
+
self.dateCreated = instance.dateCreated # noqa
|
|
899
|
+
|
|
900
|
+
# Update the _original_data attribute with the instance data
|
|
901
|
+
self._original_data = instance.dict(exclude_unset=True)
|
|
902
|
+
|
|
903
|
+
def _update_existing_instance(self: T, bulk_update: bool, is_cached: bool = False) -> Tuple[str, T]:
|
|
904
|
+
"""
|
|
905
|
+
Update an existing instance that has changes.
|
|
906
|
+
|
|
907
|
+
:param bool bulk_update: Whether to perform a bulk update
|
|
908
|
+
:param bool is_cached: Whether the instance was found in cache (dedupe) or from API (update)
|
|
909
|
+
:return: Tuple of (status, updated_instance) where status is "updated" or "deduped"
|
|
910
|
+
:rtype: Tuple[str, T]
|
|
911
|
+
"""
|
|
912
|
+
logger.debug(f"Instance of {self.__class__.__name__} has changed, updating")
|
|
913
|
+
# Save the changes, potentially in bulk
|
|
914
|
+
updated_instance = self.save(bulk=bulk_update)
|
|
915
|
+
# Update the cache with the new instance
|
|
916
|
+
self.cache_object(updated_instance)
|
|
917
|
+
# Determine if this is a dedupe: found in cache OR was created during this session
|
|
918
|
+
is_dedupe = is_cached or self.id in self.__class__._session_created_ids
|
|
919
|
+
status = "deduped" if is_dedupe else "updated"
|
|
920
|
+
return status, updated_instance
|
|
921
|
+
|
|
922
|
+
def _handle_new_instance(self: T, defaults: Optional[Dict[str, Any]], bulk_create: bool) -> Tuple[str, T]:
|
|
923
|
+
"""
|
|
924
|
+
Handle creation of a new instance when none exists.
|
|
925
|
+
|
|
926
|
+
:param Optional[Dict[str, Any]] defaults: Dictionary of default values to apply
|
|
927
|
+
:param bool bulk_create: Whether to perform a bulk create
|
|
928
|
+
:return: Tuple of (status, created_instance) where status is "created"
|
|
929
|
+
:rtype: Tuple[str, T]
|
|
930
|
+
"""
|
|
931
|
+
# apply defaults if they are provided
|
|
932
|
+
self._apply_defaults(defaults)
|
|
933
|
+
|
|
934
|
+
logger.debug(f"No existing instance found for {self.__class__.__name__}, creating new")
|
|
935
|
+
created_instance = self.create(bulk=bulk_create)
|
|
936
|
+
# Track this ID as created in this session for dedupe detection
|
|
937
|
+
self.__class__._session_created_ids.add(created_instance.id)
|
|
938
|
+
# Cache the newly created instance
|
|
939
|
+
self.cache_object(created_instance)
|
|
940
|
+
# Return the created instance with "created" status
|
|
941
|
+
return "created", created_instance
|
|
942
|
+
|
|
943
|
+
def _apply_defaults(self: T, defaults: Optional[Dict[str, Any]]) -> None:
|
|
944
|
+
"""
|
|
945
|
+
Apply default values to the instance.
|
|
946
|
+
|
|
947
|
+
:param Optional[Dict[str, Any]] defaults: Dictionary of default values to apply
|
|
948
|
+
:rtype: None
|
|
949
|
+
"""
|
|
950
|
+
if defaults:
|
|
951
|
+
for key, value in defaults.items():
|
|
952
|
+
# Handle callable values by executing them
|
|
953
|
+
if callable(value):
|
|
954
|
+
value = value()
|
|
955
|
+
setattr(self, key, value)
|
|
768
956
|
|
|
769
957
|
@classmethod
|
|
770
958
|
def _handle_list_response(
|
|
@@ -1037,24 +1225,33 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
1037
1225
|
:return: A list of objects
|
|
1038
1226
|
:rtype: List[T]
|
|
1039
1227
|
"""
|
|
1228
|
+
logger.debug(
|
|
1229
|
+
f"get_all_by_parent for {cls.__name__}: parent_id={parent_id}, parent_module={parent_module}, "
|
|
1230
|
+
f"search={'provided' if search else 'None'}"
|
|
1231
|
+
)
|
|
1040
1232
|
cache_key = f"{parent_id}:{cls.__name__}"
|
|
1041
1233
|
|
|
1042
1234
|
with cls._get_lock(cache_key):
|
|
1043
1235
|
cached_objects = cls._parent_cache.get(cache_key)
|
|
1044
1236
|
# Check for None and empty list
|
|
1045
1237
|
if cached_objects is not None and len(cached_objects) > 0:
|
|
1238
|
+
logger.debug(f"get_all_by_parent for {cls.__name__}: Returning {len(cached_objects)} cached objects")
|
|
1046
1239
|
return cached_objects
|
|
1047
1240
|
|
|
1048
|
-
if "get_all_by_search" in cls._get_endpoints() and parent_id and parent_module and not search:
|
|
1049
|
-
logger.debug(
|
|
1241
|
+
if "get_all_by_search" in cls._get_endpoints() and parent_id is not None and parent_module and not search:
|
|
1242
|
+
logger.debug(
|
|
1243
|
+
f"get_all_by_search for {cls.__name__}: Creating Search with parentID={parent_id}, module={parent_module}"
|
|
1244
|
+
)
|
|
1050
1245
|
search = Search(parentID=parent_id, module=parent_module)
|
|
1051
1246
|
if search:
|
|
1247
|
+
logger.debug(f"get_all_by_parent for {cls.__name__}: Using search endpoint")
|
|
1052
1248
|
objects: List[T] = cls._handle_looping_response(search)
|
|
1053
1249
|
else:
|
|
1054
1250
|
try:
|
|
1055
1251
|
endpoint = cls.get_endpoint("get_all_by_parent").format(
|
|
1056
1252
|
intParentID=parent_id, strModule=parent_module
|
|
1057
1253
|
)
|
|
1254
|
+
logger.debug(f"get_all_by_parent for {cls.__name__}: Using endpoint: {endpoint}")
|
|
1058
1255
|
objects: List[T] = cls._handle_list_response(
|
|
1059
1256
|
cls._get_api_handler().get(endpoint=endpoint), parent_id=parent_id, parent_module=parent_module
|
|
1060
1257
|
)
|
|
@@ -1062,6 +1259,11 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
1062
1259
|
logger.error(f"Failed to get endpoint: {e}", exc_info=True)
|
|
1063
1260
|
objects = []
|
|
1064
1261
|
|
|
1262
|
+
logger.debug(f"get_all_by_parent for {cls.__name__}: Retrieved {len(objects)} objects from API")
|
|
1263
|
+
if objects:
|
|
1264
|
+
sample_parent_ids = set(getattr(obj, cls._parent_id_field, None) for obj in objects[:10])
|
|
1265
|
+
logger.debug(f"get_all_by_parent for {cls.__name__}: Sample parent_ids in results: {sample_parent_ids}")
|
|
1266
|
+
|
|
1065
1267
|
cls.cache_list_objects(cache_key=cache_key, objects=objects)
|
|
1066
1268
|
|
|
1067
1269
|
return objects
|
|
@@ -1111,17 +1313,18 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
1111
1313
|
return ConfigDict()
|
|
1112
1314
|
|
|
1113
1315
|
@classmethod
|
|
1114
|
-
def get_endpoint(cls, endpoint_type: str) -> str:
|
|
1316
|
+
def get_endpoint(cls, endpoint_type: str, suppress_error: bool = False) -> str:
|
|
1115
1317
|
"""
|
|
1116
1318
|
Get the endpoint for a specific type.
|
|
1117
1319
|
|
|
1118
1320
|
:param str endpoint_type: The type of endpoint
|
|
1321
|
+
:param bool suppress_error: Whether to suppress the error if the endpoint is not found, defaults to False
|
|
1119
1322
|
:raises ValueError: If the endpoint type is not found
|
|
1120
1323
|
:return: The endpoint
|
|
1121
1324
|
:rtype: str
|
|
1122
1325
|
"""
|
|
1123
1326
|
endpoint = cls._get_endpoints().get(endpoint_type, "na") # noqa
|
|
1124
|
-
if not endpoint or endpoint == "na":
|
|
1327
|
+
if not endpoint or endpoint == "na" and not suppress_error:
|
|
1125
1328
|
logger.error(f"{cls.__name__} does not have endpoint {endpoint_type}")
|
|
1126
1329
|
raise ValueError(f"Endpoint {endpoint_type} not found")
|
|
1127
1330
|
endpoint = str(endpoint).replace("{model_slug}", cls.get_module_slug())
|
|
@@ -1163,7 +1366,11 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
1163
1366
|
"""
|
|
1164
1367
|
# Check if the model has change tracking and if there are changes
|
|
1165
1368
|
has_change_tracking = hasattr(self, "has_changed") and callable(getattr(self, "has_changed", None))
|
|
1166
|
-
|
|
1369
|
+
|
|
1370
|
+
if hasattr(self, "_ignore_has_changed") and self._ignore_has_changed:
|
|
1371
|
+
should_save = True
|
|
1372
|
+
else:
|
|
1373
|
+
should_save = not has_change_tracking or self.has_changed()
|
|
1167
1374
|
|
|
1168
1375
|
if should_save:
|
|
1169
1376
|
if bulk:
|
|
@@ -1269,7 +1476,15 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
1269
1476
|
endpoint = self.get_endpoint("insert")
|
|
1270
1477
|
response = self._get_api_handler().post(endpoint=endpoint, data=self.dict(), headers=self._get_headers())
|
|
1271
1478
|
if response and response.ok:
|
|
1272
|
-
|
|
1479
|
+
response_data = response.json()
|
|
1480
|
+
|
|
1481
|
+
# Handle special case for ComponentMapping which may have nested response structure
|
|
1482
|
+
if self.__class__.__name__ == "ComponentMapping" and "componentMapping" in response_data:
|
|
1483
|
+
component_mapping_data = response_data["componentMapping"]
|
|
1484
|
+
obj = self.__class__(**component_mapping_data)
|
|
1485
|
+
else:
|
|
1486
|
+
obj = self.__class__(**response_data)
|
|
1487
|
+
|
|
1273
1488
|
self.cache_object(obj)
|
|
1274
1489
|
return obj
|
|
1275
1490
|
else:
|
|
@@ -1407,11 +1622,22 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
1407
1622
|
f"[#f68d1f]Updating {total_items} RegScale {cls.__name__}s...",
|
|
1408
1623
|
total=total_items,
|
|
1409
1624
|
)
|
|
1625
|
+
endpoint = cls.get_endpoint("batch_update", suppress_error=True)
|
|
1626
|
+
if not endpoint or endpoint == "na":
|
|
1627
|
+
logger.debug(f"No batch_update endpoint found for {cls.__name__}, using save method instead")
|
|
1628
|
+
for item in items:
|
|
1629
|
+
updated_item = item.save()
|
|
1630
|
+
cls.cache_object(updated_item)
|
|
1631
|
+
results.append(updated_item)
|
|
1632
|
+
if progress and update_job is not None:
|
|
1633
|
+
progress.advance(update_job, advance=1)
|
|
1634
|
+
cls._check_and_remove_progress_object(progress_context, remove_progress_bar, update_job)
|
|
1635
|
+
return results
|
|
1410
1636
|
for i in range(0, total_items, batch_size):
|
|
1411
1637
|
batch = items[i : i + batch_size]
|
|
1412
1638
|
batch_results = cls._handle_list_response(
|
|
1413
1639
|
cls._get_api_handler().put(
|
|
1414
|
-
endpoint=
|
|
1640
|
+
endpoint=endpoint,
|
|
1415
1641
|
data=[item.model_dump() for item in batch if item],
|
|
1416
1642
|
)
|
|
1417
1643
|
)
|
|
@@ -1424,10 +1650,10 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
1424
1650
|
cls._check_and_remove_progress_object(progress_context, remove_progress_bar, update_job)
|
|
1425
1651
|
|
|
1426
1652
|
if progress_context:
|
|
1427
|
-
process_batch(progress_context)
|
|
1653
|
+
process_batch(progress=progress_context, remove_progress_bar=remove_progress)
|
|
1428
1654
|
else:
|
|
1429
1655
|
with create_progress_object() as create_progress:
|
|
1430
|
-
process_batch(create_progress)
|
|
1656
|
+
process_batch(progress=create_progress, remove_progress_bar=remove_progress)
|
|
1431
1657
|
|
|
1432
1658
|
return results
|
|
1433
1659
|
|
|
@@ -1468,6 +1694,17 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
1468
1694
|
logger.warning(f"{cls.__name__}: No matching record found for ID: {cls.__name__} {object_id}")
|
|
1469
1695
|
return None
|
|
1470
1696
|
|
|
1697
|
+
@classmethod
|
|
1698
|
+
def get(cls, id: Union[str, int]) -> Optional[T]:
|
|
1699
|
+
"""
|
|
1700
|
+
Get a RegScale object by ID. shortcut for get_object.
|
|
1701
|
+
|
|
1702
|
+
:param Union[str, int] id: The ID of the object
|
|
1703
|
+
:return: The object or None if not found
|
|
1704
|
+
:rtype: Optional[T]
|
|
1705
|
+
"""
|
|
1706
|
+
return cls.get_object(object_id=id)
|
|
1707
|
+
|
|
1471
1708
|
@classmethod
|
|
1472
1709
|
def get_objects_and_attachments_by_parent(
|
|
1473
1710
|
cls, parent_id: int, parent_module: str
|
|
@@ -1480,7 +1717,7 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
1480
1717
|
:return: A tuple of a list of objects and a list of attachments
|
|
1481
1718
|
:rtype: Tuple[List[T], dict[int, List["File"]]]
|
|
1482
1719
|
"""
|
|
1483
|
-
from regscale.models import File
|
|
1720
|
+
from regscale.models.regscale_models import File
|
|
1484
1721
|
|
|
1485
1722
|
# get the existing issues for the parent record that are already in RegScale
|
|
1486
1723
|
logger.info("Fetching full %s list from RegScale %s #%i.", cls.__name__, parent_module, parent_id)
|
|
@@ -43,6 +43,7 @@ class SecurityPlan(RegScaleModel):
|
|
|
43
43
|
environment: Optional[str] = ""
|
|
44
44
|
lawsAndRegulations: Optional[str] = ""
|
|
45
45
|
authorizationBoundary: Optional[str] = ""
|
|
46
|
+
authorizationTerminationDate: Optional[str] = ""
|
|
46
47
|
networkArchitecture: Optional[str] = ""
|
|
47
48
|
dataFlow: Optional[str] = ""
|
|
48
49
|
overallCategorization: Optional[str] = ""
|
|
@@ -107,6 +108,7 @@ class SecurityPlan(RegScaleModel):
|
|
|
107
108
|
fedrampDateSubmitted: Optional[str] = ""
|
|
108
109
|
fedrampDateAuthorized: Optional[str] = ""
|
|
109
110
|
fedrampId: Optional[str] = ""
|
|
111
|
+
complianceSettings: Optional[str] = None
|
|
110
112
|
complianceSettingsId: Optional[int] = 1
|
|
111
113
|
tenantsId: int = 1
|
|
112
114
|
|
|
@@ -119,11 +121,11 @@ class SecurityPlan(RegScaleModel):
|
|
|
119
121
|
:return: The ComplianceSettings ID if the RegScale version is compatible, None otherwise
|
|
120
122
|
:rtype: Optional[int]
|
|
121
123
|
"""
|
|
122
|
-
from
|
|
124
|
+
from regscale.utils.version import RegscaleVersion
|
|
123
125
|
|
|
124
126
|
regscale_version = cls._get_api_handler().regscale_version
|
|
125
127
|
|
|
126
|
-
if len(regscale_version) >= 10 or
|
|
128
|
+
if len(regscale_version) >= 10 or RegscaleVersion.compare_versions(regscale_version, "6.13.0.0"):
|
|
127
129
|
return v
|
|
128
130
|
else:
|
|
129
131
|
return None
|
|
@@ -67,8 +67,8 @@ class Vulnerability(RegScaleModel):
|
|
|
67
67
|
lastSeen: Optional[str] = None
|
|
68
68
|
firstSeen: Optional[str] = None
|
|
69
69
|
daysOpen: Optional[int] = None
|
|
70
|
-
dns: Optional[str] =
|
|
71
|
-
ipAddress: Optional[str] =
|
|
70
|
+
dns: Optional[str] = ""
|
|
71
|
+
ipAddress: Optional[str] = ""
|
|
72
72
|
mitigated: Optional[bool] = None
|
|
73
73
|
operatingSystem: Optional[str] = None
|
|
74
74
|
port: Optional[Union[str, int]] = None
|
|
@@ -83,7 +83,7 @@ class Vulnerability(RegScaleModel):
|
|
|
83
83
|
cvsSv3BaseScore: Optional[Union[float, int]] = None
|
|
84
84
|
description: Optional[str] = None
|
|
85
85
|
plugInText: Optional[str] = None
|
|
86
|
-
tenantsId: int = Field(
|
|
86
|
+
tenantsId: int = Field(default_factory=RegScaleModel.get_tenant_id)
|
|
87
87
|
isPublic: bool = Field(default=False)
|
|
88
88
|
dateClosed: Optional[str] = None
|
|
89
89
|
status: Optional[Union[str, IssueStatus]] = Field(default_factory=lambda: IssueStatus.Open)
|