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
|
@@ -31,6 +31,7 @@ def login(
|
|
|
31
31
|
app: Optional["Application"] = None,
|
|
32
32
|
token: Optional[str] = None,
|
|
33
33
|
mfa_token: Optional[str] = "",
|
|
34
|
+
app_id: Optional[int] = 1,
|
|
34
35
|
) -> str:
|
|
35
36
|
"""
|
|
36
37
|
Wrapper for Login to RegScale
|
|
@@ -41,6 +42,7 @@ def login(
|
|
|
41
42
|
:param Optional[Application] app: Application object, defaults to None
|
|
42
43
|
:param Optional[str] token: a valid JWT token to pass, defaults to None
|
|
43
44
|
:param Optional[str] mfa_token: a valid MFA token to pass, defaults to ""
|
|
45
|
+
:param Optional[int] app_id: The app ID to login with
|
|
44
46
|
:raises: ValueError if no domain value found in init.yaml
|
|
45
47
|
:raises: TypeError if token or user id doesn't match expected data type
|
|
46
48
|
:raises: SSLCertVerificationError if unable to validate SSL certificate
|
|
@@ -103,9 +105,10 @@ def login(
|
|
|
103
105
|
password=str_password,
|
|
104
106
|
domain=host,
|
|
105
107
|
mfa_token=mfa_token,
|
|
108
|
+
app_id=app_id,
|
|
106
109
|
)
|
|
107
110
|
else:
|
|
108
|
-
regscale_auth = RegScaleAuth.authenticate(Api(), mfa_token=mfa_token)
|
|
111
|
+
regscale_auth = RegScaleAuth.authenticate(Api(), mfa_token=mfa_token, app_id=app_id)
|
|
109
112
|
if config and config["domain"] is None:
|
|
110
113
|
raise ValueError("No domain set in the init.yaml configuration file.")
|
|
111
114
|
if config and config["domain"] == "":
|
|
@@ -136,7 +139,6 @@ def login(
|
|
|
136
139
|
logger.info("New RegScale Token has been updated and saved in init.yaml")
|
|
137
140
|
# Truncate token for logging purposes
|
|
138
141
|
logger.debug("Token: %s", regscale_auth.token[:20])
|
|
139
|
-
config["domain"] = host
|
|
140
142
|
app.save_config(config)
|
|
141
143
|
return regscale_auth.token
|
|
142
144
|
|
|
@@ -348,16 +348,22 @@ def upload_data(path: Path, obj_type: str) -> None:
|
|
|
348
348
|
if os.path.isfile(os.path.join(path, all_workbook_filename)):
|
|
349
349
|
if not os.path.isfile(os.path.join(path, old_workbook_filename)):
|
|
350
350
|
return app.logger.error("Missing pre-change copy file, unable to determine if changes were made. Aborting!")
|
|
351
|
+
|
|
352
|
+
# Get the sheet name from the Excel file
|
|
353
|
+
workbook_path = os.path.join(path, all_workbook_filename)
|
|
354
|
+
with pd.ExcelFile(workbook_path) as xls:
|
|
355
|
+
sheet_name = xls.sheet_names[0] if xls.sheet_names else "Sheet1"
|
|
356
|
+
|
|
351
357
|
df1 = pd.read_excel(os.path.join(path, old_workbook_filename), sheet_name=0, index_col="Id")
|
|
352
358
|
|
|
353
|
-
df2 = pd.read_excel(
|
|
359
|
+
df2 = pd.read_excel(workbook_path, sheet_name=0, index_col="Id")
|
|
354
360
|
|
|
355
361
|
if df1.equals(df2):
|
|
356
362
|
error_and_exit("No differences detected.")
|
|
357
363
|
|
|
358
|
-
app.logger.
|
|
364
|
+
app.logger.info("Changes detected in workbook. Processing updates...")
|
|
359
365
|
# Need to strip out any net new rows before doing this comparison
|
|
360
|
-
df3 = strip_any_net_new_rows(app, df2, all_workbook_filename, obj_type, path, new_workbook_filename)
|
|
366
|
+
df3 = strip_any_net_new_rows(app, df2, all_workbook_filename, obj_type, path, new_workbook_filename, sheet_name)
|
|
361
367
|
try:
|
|
362
368
|
changes = compare_dataframes(df1, df3)
|
|
363
369
|
except ValueError:
|
|
@@ -483,7 +489,13 @@ def upload_new_data(app: Application, path: Path, obj_type: str, workbook_filena
|
|
|
483
489
|
|
|
484
490
|
|
|
485
491
|
def strip_any_net_new_rows(
|
|
486
|
-
app: Application,
|
|
492
|
+
app: Application,
|
|
493
|
+
df: "pd.DataFrame",
|
|
494
|
+
workbook_filename: str,
|
|
495
|
+
obj_type: str,
|
|
496
|
+
path: Path,
|
|
497
|
+
new_workbook_filename: str,
|
|
498
|
+
sheet_name: Optional[str] = None,
|
|
487
499
|
) -> "pd.DataFrame":
|
|
488
500
|
"""
|
|
489
501
|
This method scans the loaded workbook for any new rows and strips them out to insert separately.
|
|
@@ -494,6 +506,7 @@ def strip_any_net_new_rows(
|
|
|
494
506
|
:param str obj_type: The model type to load the records as
|
|
495
507
|
:param Path path: The path where the Excel file can be found
|
|
496
508
|
:param str new_workbook_filename: The file name of the Excel spreadsheet with new records.
|
|
509
|
+
:param Optional[str] sheet_name: The name of the worksheet being processed
|
|
497
510
|
:return: pd.DataFrame The updated DataFrame, minus any new rows
|
|
498
511
|
:rtype: pd.DataFrame
|
|
499
512
|
"""
|
|
@@ -502,14 +515,14 @@ def strip_any_net_new_rows(
|
|
|
502
515
|
df_updates = []
|
|
503
516
|
df_inserts = []
|
|
504
517
|
indexes = []
|
|
505
|
-
columns =
|
|
518
|
+
columns = list(df.columns)
|
|
506
519
|
obj = get_obj(obj_type)
|
|
507
520
|
for x in df.index:
|
|
508
521
|
if math.isnan(x):
|
|
509
522
|
data_rec = {}
|
|
510
523
|
for y in columns:
|
|
511
524
|
data_rec[y] = df.at[x, y]
|
|
512
|
-
df_inserts.append(convert_new_record_to_model(data_rec, obj_type, path, workbook_filename))
|
|
525
|
+
df_inserts.append(convert_new_record_to_model(data_rec, obj_type, path, workbook_filename, sheet_name))
|
|
513
526
|
else:
|
|
514
527
|
indexes.append(x)
|
|
515
528
|
data_rec = []
|
|
@@ -519,7 +532,8 @@ def strip_any_net_new_rows(
|
|
|
519
532
|
new_df = pd.DataFrame(df_updates, index=indexes, columns=columns)
|
|
520
533
|
if len(df_inserts) > 0:
|
|
521
534
|
if obj.is_new_excel_record_allowed():
|
|
522
|
-
|
|
535
|
+
# Use workbook_filename (the actual file containing the data) instead of new_workbook_filename
|
|
536
|
+
post_and_save_models(app, df_inserts, path, obj_type, workbook_filename)
|
|
523
537
|
else:
|
|
524
538
|
app.logger.warning(
|
|
525
539
|
"New rows have been found in the Excel spreadsheet being loaded. New records for this model are not allowed."
|
|
@@ -528,18 +542,9 @@ def strip_any_net_new_rows(
|
|
|
528
542
|
return new_df
|
|
529
543
|
|
|
530
544
|
|
|
531
|
-
def
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
:param pd.DataFrame df:
|
|
536
|
-
:return: list of column names
|
|
537
|
-
:rtype: list
|
|
538
|
-
"""
|
|
539
|
-
return [y for y in df.columns]
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
def convert_new_record_to_model(data_rec: dict, obj_type: str, path: Path, workbook_filename: str) -> object:
|
|
545
|
+
def convert_new_record_to_model(
|
|
546
|
+
data_rec: dict, obj_type: str, path: Path, workbook_filename: str, sheet_name: Optional[str] = None
|
|
547
|
+
) -> object:
|
|
543
548
|
"""
|
|
544
549
|
This method takes the new record found in the Excel file of existing records, and converts it
|
|
545
550
|
into a model object for inserting into the database.
|
|
@@ -548,6 +553,7 @@ def convert_new_record_to_model(data_rec: dict, obj_type: str, path: Path, workb
|
|
|
548
553
|
:param str obj_type: The model type to load the records as
|
|
549
554
|
:param Path path: The path where the Excel file can be found
|
|
550
555
|
:param str workbook_filename: The file name of the Excel spreadsheet
|
|
556
|
+
:param Optional[str] sheet_name: The name of the worksheet being processed
|
|
551
557
|
:return: object
|
|
552
558
|
:rtype: object
|
|
553
559
|
:raises ValueError:
|
|
@@ -571,9 +577,29 @@ def convert_new_record_to_model(data_rec: dict, obj_type: str, path: Path, workb
|
|
|
571
577
|
elif cur_field.data_type == "str":
|
|
572
578
|
if not isinstance(new_obj[cur_field.field_name], str):
|
|
573
579
|
new_obj[cur_field.field_name] = str(new_obj[cur_field.field_name])
|
|
580
|
+
|
|
581
|
+
parse_parent_data(new_obj, sheet_name)
|
|
582
|
+
|
|
574
583
|
return cast_dict_as_model(new_obj, obj_type)
|
|
575
584
|
|
|
576
585
|
|
|
586
|
+
def parse_parent_data(new_obj: dict, sheet_name: str) -> None:
|
|
587
|
+
"""
|
|
588
|
+
Parse parentId and parentModule from worksheet name.
|
|
589
|
+
|
|
590
|
+
:param dict new_obj: The new object to parse the parent info for
|
|
591
|
+
:param str sheet_name: The worksheet name to parse
|
|
592
|
+
:rtype: None
|
|
593
|
+
"""
|
|
594
|
+
# Parse parentId and parentModule from sheet name if available
|
|
595
|
+
if sheet_name:
|
|
596
|
+
parent_id, parent_module = parse_parent_info_from_sheet_name(sheet_name)
|
|
597
|
+
if parent_id is not None:
|
|
598
|
+
new_obj["parentId"] = parent_id
|
|
599
|
+
if parent_module is not None:
|
|
600
|
+
new_obj["parentModule"] = parent_module
|
|
601
|
+
|
|
602
|
+
|
|
577
603
|
def generate_default_value_for_field(field_name: str, data_type: str) -> Any:
|
|
578
604
|
"""
|
|
579
605
|
Generate a default value for a required field.
|
|
@@ -597,10 +623,47 @@ def generate_default_value_for_field(field_name: str, data_type: str) -> Any:
|
|
|
597
623
|
return 0.0
|
|
598
624
|
|
|
599
625
|
|
|
626
|
+
def parse_parent_info_from_sheet_name(sheet_name: str) -> tuple[Optional[int], Optional[str]]:
|
|
627
|
+
"""
|
|
628
|
+
Parse parentId and parentModule from worksheet name.
|
|
629
|
+
|
|
630
|
+
Expected format: Issue(46_securityplans
|
|
631
|
+
Where:
|
|
632
|
+
- Issue( is the model prefix
|
|
633
|
+
- 46 is the parentId
|
|
634
|
+
- securityplans is the parentModule
|
|
635
|
+
|
|
636
|
+
:param str sheet_name: The worksheet name to parse
|
|
637
|
+
:return: Tuple of (parentId, parentModule), or (None, None) if pattern doesn't match
|
|
638
|
+
:rtype: tuple[Optional[int], Optional[str]]
|
|
639
|
+
"""
|
|
640
|
+
if not sheet_name or "(" not in sheet_name or "_" not in sheet_name:
|
|
641
|
+
return None, None
|
|
642
|
+
|
|
643
|
+
try:
|
|
644
|
+
# Find the opening parenthesis
|
|
645
|
+
paren_index = sheet_name.index("(")
|
|
646
|
+
# Get the part after the parenthesis
|
|
647
|
+
after_paren = sheet_name[paren_index + 1 :]
|
|
648
|
+
|
|
649
|
+
# Split by underscore
|
|
650
|
+
if "_" in after_paren:
|
|
651
|
+
parts = after_paren.split("_", 1) # Split on first underscore only
|
|
652
|
+
parent_id = int(parts[0])
|
|
653
|
+
parent_module = parts[1]
|
|
654
|
+
return parent_id, parent_module
|
|
655
|
+
except (ValueError, IndexError):
|
|
656
|
+
# If parsing fails, return None values
|
|
657
|
+
pass
|
|
658
|
+
|
|
659
|
+
return None, None
|
|
660
|
+
|
|
661
|
+
|
|
600
662
|
# pylint: disable=E1136,R0914
|
|
601
663
|
def upload_existing_data(app: Application, api: Api, path: Path, obj_type: str, workbook_filename: str) -> None:
|
|
602
664
|
"""
|
|
603
|
-
This method reads in the spreadsheet filled with existing records to update in RegScale
|
|
665
|
+
This method reads in the spreadsheet filled with existing records to update in RegScale
|
|
666
|
+
using the RegScaleModel save() and bulk_save() methods.
|
|
604
667
|
|
|
605
668
|
:param Application app: The Application instance
|
|
606
669
|
:param Api api: The instance api handler
|
|
@@ -630,48 +693,100 @@ def upload_existing_data(app: Application, api: Api, path: Path, obj_type: str,
|
|
|
630
693
|
logger.debug(changes)
|
|
631
694
|
id_df = pd.DataFrame(ids, index=None, columns=["Id"])
|
|
632
695
|
id_df2 = id_df.drop_duplicates()
|
|
696
|
+
logger.info(f"Found {len(id_df2)} unique {obj_type} ID(s) with changes: {id_df2['Id'].tolist()}")
|
|
697
|
+
|
|
633
698
|
updated_files = os.path.join(path, workbook_filename)
|
|
634
699
|
df3 = pd.read_excel(updated_files, sheet_name=0, index_col=None)
|
|
700
|
+
logger.debug(f"Read {len(df3)} total rows from Excel file")
|
|
701
|
+
|
|
635
702
|
updated = df3[df3["Id"].isin(id_df2["Id"])]
|
|
703
|
+
logger.info(f"Filtered to {len(updated)} {obj_type}(s) matching changed IDs")
|
|
704
|
+
|
|
705
|
+
if len(updated) == 0:
|
|
706
|
+
logger.error(
|
|
707
|
+
f"No {obj_type}s found in Excel file matching the IDs in differences.txt. "
|
|
708
|
+
f"Expected IDs: {id_df2['Id'].tolist()}. "
|
|
709
|
+
f"This usually means the Excel file doesn't contain these records."
|
|
710
|
+
)
|
|
711
|
+
return
|
|
712
|
+
|
|
636
713
|
updated = map_workbook_to_dict(updated_files, updated)
|
|
714
|
+
logger.debug(f"Converted to dictionary with {len(updated)} entries")
|
|
637
715
|
config = app.config
|
|
638
|
-
|
|
639
|
-
|
|
716
|
+
|
|
717
|
+
# Load existing model instances from API
|
|
718
|
+
load_objs = load_model_for_id(api, updated, config["domain"] + obj.get_endpoint("get"), obj_type)
|
|
719
|
+
|
|
720
|
+
# Apply changes to model instances and queue for bulk update
|
|
721
|
+
modified_objects = []
|
|
640
722
|
for cur_obj in load_objs:
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
723
|
+
# Apply Excel changes to the model instance
|
|
724
|
+
modified_obj = find_and_apply_changes(cur_obj, changes, updated)
|
|
725
|
+
|
|
726
|
+
# Ignore change tracking to ensure all updates are saved
|
|
727
|
+
modified_obj._ignore_has_changed = True
|
|
728
|
+
# Queue the instance for bulk update
|
|
729
|
+
modified_obj.save(bulk=True)
|
|
730
|
+
modified_objects.append(modified_obj)
|
|
731
|
+
|
|
732
|
+
# Execute bulk update using the model class
|
|
733
|
+
if modified_objects:
|
|
734
|
+
app.logger.info("Executing bulk update for %i %s(s)...", len(modified_objects), obj_type)
|
|
735
|
+
model_class = type(modified_objects[0])
|
|
736
|
+
results = model_class.bulk_save()
|
|
737
|
+
|
|
738
|
+
updated_count = len(results.get("updated", []))
|
|
739
|
+
created_count = len(results.get("created", []))
|
|
740
|
+
|
|
741
|
+
app.logger.info(
|
|
742
|
+
"Bulk operation completed: Updated %i %s(s), Created %i %s(s)",
|
|
743
|
+
updated_count,
|
|
744
|
+
obj_type,
|
|
745
|
+
created_count,
|
|
746
|
+
obj_type,
|
|
747
|
+
)
|
|
650
748
|
|
|
651
749
|
|
|
652
750
|
# pylint: enable=E1136,R0914
|
|
653
751
|
|
|
654
752
|
|
|
655
|
-
def find_and_apply_changes(cur_object:
|
|
753
|
+
def find_and_apply_changes(cur_object: object, changes: list, updates: dict) -> object:
|
|
656
754
|
"""
|
|
657
755
|
This method looks through the changes and applies those that should be applied to
|
|
658
|
-
the current
|
|
756
|
+
the current model instance.
|
|
659
757
|
|
|
660
|
-
:param
|
|
758
|
+
:param object cur_object: the current model instance being updated
|
|
661
759
|
:param list changes: a list of the specific changes to apply
|
|
662
760
|
:param dict updates: a dictionary of updated models to be applied to the current object(s)
|
|
663
|
-
:return:
|
|
664
|
-
:rtype:
|
|
761
|
+
:return: object the updated model instance
|
|
762
|
+
:rtype: object
|
|
665
763
|
"""
|
|
666
764
|
for cur_change in changes:
|
|
667
|
-
if cur_change["id"] == cur_object
|
|
765
|
+
if cur_change["id"] == cur_object.id:
|
|
668
766
|
field_def = get_field_def_for_column(cur_change["column"])
|
|
669
|
-
if
|
|
670
|
-
|
|
671
|
-
|
|
767
|
+
if field_def is None:
|
|
768
|
+
logger.warning(
|
|
769
|
+
f"Column '{cur_change['column']}' not found in model fields for {type(cur_object).__name__} "
|
|
770
|
+
f"ID {cur_object.id}. Change will be skipped."
|
|
672
771
|
)
|
|
772
|
+
continue
|
|
773
|
+
if len(field_def.lookup_field) > 0:
|
|
774
|
+
value = check_empty_nan(extract_update_for_column(field_def.field_name, cur_change["id"], updates))
|
|
775
|
+
setattr(cur_object, field_def.field_name, value)
|
|
673
776
|
else:
|
|
674
|
-
|
|
777
|
+
field_name = get_field_name_for_column(cur_change["column"])
|
|
778
|
+
if not field_name:
|
|
779
|
+
logger.warning(
|
|
780
|
+
f"Could not find field name for column '{cur_change['column']}' in {type(cur_object).__name__} "
|
|
781
|
+
f"ID {cur_object.id}. Change will be skipped."
|
|
782
|
+
)
|
|
783
|
+
continue
|
|
784
|
+
value = check_empty_nan(cur_change["value"])
|
|
785
|
+
logger.debug(
|
|
786
|
+
f"Applying change to {type(cur_object).__name__} ID {cur_object.id}: "
|
|
787
|
+
f"{field_name} = {value} (was: {getattr(cur_object, field_name, 'N/A')})"
|
|
788
|
+
)
|
|
789
|
+
setattr(cur_object, field_name, value)
|
|
675
790
|
return cur_object
|
|
676
791
|
|
|
677
792
|
|
|
@@ -732,7 +847,8 @@ def post_and_save_models(
|
|
|
732
847
|
load_file_name: str,
|
|
733
848
|
) -> None:
|
|
734
849
|
"""
|
|
735
|
-
Function to post new records to RegScale and save record ids to excel workbook
|
|
850
|
+
Function to post new records to RegScale and save record ids to excel workbook.
|
|
851
|
+
Uses the RegScaleModel .create() method for new objects.
|
|
736
852
|
|
|
737
853
|
:param Application app: RegScale CLI Application object
|
|
738
854
|
:param list new_models: List of new records to post to RegScale
|
|
@@ -745,28 +861,42 @@ def post_and_save_models(
|
|
|
745
861
|
import pandas as pd # Optimize import performance
|
|
746
862
|
|
|
747
863
|
try:
|
|
864
|
+
# Create new objects using .create() method
|
|
748
865
|
new_objs = []
|
|
749
866
|
for cur_obj in new_models:
|
|
867
|
+
# Use .create() for new objects (id=0 or None)
|
|
868
|
+
cur_obj._ignore_has_changed = True
|
|
750
869
|
new_obj = cur_obj.create()
|
|
751
870
|
cur_obj.create_new_connecting_model(new_obj)
|
|
752
871
|
new_objs.append(cur_obj)
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
)
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
872
|
+
|
|
873
|
+
# Save IDs and all other fields to Excel
|
|
874
|
+
if new_objs:
|
|
875
|
+
# Create a list of dicts with all field values from created objects
|
|
876
|
+
obj_data = []
|
|
877
|
+
for obj in new_objs:
|
|
878
|
+
obj_dict = {"id_number": obj.id}
|
|
879
|
+
# Add all fields from obj_fields to ensure we capture API-populated fields
|
|
880
|
+
for field in obj_fields:
|
|
881
|
+
field_value = getattr(obj, field.field_name, None)
|
|
882
|
+
if field_value is not None:
|
|
883
|
+
obj_dict[field.field_name] = field_value
|
|
884
|
+
obj_data.append(obj_dict)
|
|
885
|
+
|
|
886
|
+
new_objs_df = pd.DataFrame(obj_data)
|
|
887
|
+
for file_name in [load_file_name]:
|
|
888
|
+
with pd.ExcelWriter(
|
|
889
|
+
os.path.join(workbook_path, file_name),
|
|
890
|
+
mode="a",
|
|
891
|
+
engine="openpyxl",
|
|
892
|
+
if_sheet_exists="overlay",
|
|
893
|
+
) as writer:
|
|
894
|
+
new_objs_df.to_excel(
|
|
895
|
+
writer,
|
|
896
|
+
sheet_name=obj_type + "_Ids",
|
|
897
|
+
index=False,
|
|
898
|
+
)
|
|
899
|
+
app.logger.info("%i total %s(s) were added to RegScale.", len(new_objs), obj_type)
|
|
770
900
|
except Exception as e:
|
|
771
901
|
app.logger.error(e)
|
|
772
902
|
|
|
@@ -793,26 +923,49 @@ def map_pandas_timestamp(date_time: "pd.Timestamp") -> Optional[str]:
|
|
|
793
923
|
return date_time or None
|
|
794
924
|
|
|
795
925
|
|
|
796
|
-
def load_model_for_id(api: Api, wb_data: dict, url: str) -> list:
|
|
926
|
+
def load_model_for_id(api: Api, wb_data: dict, url: str, obj_type: str) -> list:
|
|
797
927
|
"""
|
|
798
|
-
This method loads the current record for the updated objects.
|
|
928
|
+
This method loads the current record for the updated objects and returns model instances.
|
|
799
929
|
|
|
800
930
|
:param Api api: the API object instance to use
|
|
801
931
|
:param dict wb_data: The submitted workbook data in a dict
|
|
802
932
|
:param str url: the base url to use to retrieve the model data
|
|
803
|
-
:
|
|
933
|
+
:param str obj_type: The model type to cast the data to
|
|
934
|
+
:return: list of model instances of the specified type
|
|
804
935
|
:rtype: list
|
|
805
936
|
"""
|
|
806
937
|
load_data = []
|
|
938
|
+
failed_loads = []
|
|
939
|
+
|
|
940
|
+
logger.info(f"Loading {len(wb_data)} {obj_type}(s) from API for update...")
|
|
941
|
+
|
|
807
942
|
for cur_obj in wb_data:
|
|
808
943
|
obj = wb_data[cur_obj]
|
|
809
944
|
cur_id = int(obj["Id"])
|
|
810
945
|
if cur_id > 0:
|
|
811
946
|
url_to_use = url.replace("{id}", str(cur_id))
|
|
812
947
|
url_to_use = check_url_for_double_slash(url_to_use)
|
|
948
|
+
logger.debug(f"Fetching {obj_type} ID {cur_id} from {url_to_use}")
|
|
813
949
|
result = api.get(url_to_use)
|
|
814
950
|
if result.status_code == 200:
|
|
815
|
-
|
|
951
|
+
dict_data = result.json()
|
|
952
|
+
model_instance = cast_dict_as_model(dict_data, obj_type)
|
|
953
|
+
load_data.append(model_instance)
|
|
954
|
+
logger.debug(f"Successfully loaded {obj_type} ID {cur_id}")
|
|
955
|
+
else:
|
|
956
|
+
failed_loads.append((cur_id, result.status_code))
|
|
957
|
+
logger.warning(
|
|
958
|
+
f"Failed to load {obj_type} ID {cur_id} from API. Status code: {result.status_code}. "
|
|
959
|
+
f"This record will not be updated."
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
if failed_loads:
|
|
963
|
+
logger.warning(
|
|
964
|
+
f"Failed to load {len(failed_loads)} {obj_type}(s) from API: "
|
|
965
|
+
f"{', '.join([f'ID {id} (HTTP {code})' for id, code in failed_loads])}"
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
logger.info(f"Successfully loaded {len(load_data)} {obj_type}(s) from API for update.")
|
|
816
969
|
return load_data
|
|
817
970
|
|
|
818
971
|
|
|
@@ -945,7 +1098,9 @@ def map_workbook_to_lookups(file_path: str, workbook_data: Optional["pd.DataFram
|
|
|
945
1098
|
else:
|
|
946
1099
|
wb_data = pd.read_excel(file_path)
|
|
947
1100
|
|
|
948
|
-
|
|
1101
|
+
# Only drop rows where ALL values are NaN (completely empty rows)
|
|
1102
|
+
# Don't drop rows with some NaN values - those are legitimate records with optional empty fields
|
|
1103
|
+
wb_data = wb_data.dropna(how="all")
|
|
949
1104
|
for cur_row in obj_fields:
|
|
950
1105
|
if len(cur_row.lookup_field) > 0 and cur_row.lookup_field != "module":
|
|
951
1106
|
if cur_row.column_name in wb_data.columns:
|
|
@@ -347,13 +347,22 @@ def create_progress_object(indeterminate: bool = False) -> Progress:
|
|
|
347
347
|
:return: Progress object for live progress in console
|
|
348
348
|
:rtype: Progress
|
|
349
349
|
"""
|
|
350
|
+
task_description = "{task.description}"
|
|
351
|
+
# Disable Rich progress bar on Windows to avoid Unicode encoding errors
|
|
352
|
+
if platform.system() == "Windows":
|
|
353
|
+
# Return a minimal progress object without visual elements that cause encoding issues
|
|
354
|
+
return Progress(
|
|
355
|
+
TextColumn(task_description),
|
|
356
|
+
disable=True, # Disable progress bar rendering on Windows
|
|
357
|
+
)
|
|
358
|
+
|
|
350
359
|
if indeterminate:
|
|
351
360
|
return Progress(
|
|
352
|
-
|
|
361
|
+
task_description,
|
|
353
362
|
SpinnerColumn(),
|
|
354
363
|
)
|
|
355
364
|
return Progress(
|
|
356
|
-
|
|
365
|
+
task_description,
|
|
357
366
|
SpinnerColumn(),
|
|
358
367
|
BarColumn(),
|
|
359
368
|
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
@@ -584,6 +593,42 @@ def check_supported_file_type(file: Path) -> None:
|
|
|
584
593
|
raise RuntimeError(f"Unsupported file type: {file.suffix}")
|
|
585
594
|
|
|
586
595
|
|
|
596
|
+
def _remove_nested_dicts_before_saving(data: Any) -> "pd.DataFrame":
|
|
597
|
+
"""
|
|
598
|
+
Remove nested dictionaries before saving the data to a file.
|
|
599
|
+
|
|
600
|
+
:param Any data: The data to remove nested dictionaries from.
|
|
601
|
+
:return: A pandas DataFrame with the nested dictionaries removed.
|
|
602
|
+
:rtype: "pd.DataFrame"
|
|
603
|
+
"""
|
|
604
|
+
import pandas as pd # Optimize import performance
|
|
605
|
+
|
|
606
|
+
# Handle case where data is a single dict (not a list)
|
|
607
|
+
# This occurs with endpoints that return a single object with nested structures
|
|
608
|
+
if isinstance(data, dict) and not isinstance(data, list):
|
|
609
|
+
# Check if the dict contains nested dicts or lists of dicts (not simple lists)
|
|
610
|
+
has_nested_dicts = any(
|
|
611
|
+
isinstance(v, dict) or (isinstance(v, list) and v and isinstance(v[0], dict)) for v in data.values()
|
|
612
|
+
)
|
|
613
|
+
if has_nested_dicts:
|
|
614
|
+
# Use json_normalize to flatten nested dict structures
|
|
615
|
+
d_frame = pd.json_normalize(data)
|
|
616
|
+
else:
|
|
617
|
+
# Simple dict or dict with simple lists
|
|
618
|
+
# Check if all values are scalars (not lists) - if so, wrap in list for DataFrame
|
|
619
|
+
has_any_lists = any(isinstance(v, list) for v in data.values())
|
|
620
|
+
if has_any_lists:
|
|
621
|
+
# Dict with simple lists - can use DataFrame directly
|
|
622
|
+
d_frame = pd.DataFrame(data)
|
|
623
|
+
else:
|
|
624
|
+
# All scalar values - must wrap in list for DataFrame
|
|
625
|
+
d_frame = pd.DataFrame([data])
|
|
626
|
+
else:
|
|
627
|
+
# Handle list of dicts or other data structures
|
|
628
|
+
d_frame = pd.DataFrame(data)
|
|
629
|
+
return d_frame
|
|
630
|
+
|
|
631
|
+
|
|
587
632
|
def save_to_csv(file: Path, data: Any, output_log: bool, transpose: bool = True) -> None:
|
|
588
633
|
"""
|
|
589
634
|
Save data to a CSV file.
|
|
@@ -594,15 +639,14 @@ def save_to_csv(file: Path, data: Any, output_log: bool, transpose: bool = True)
|
|
|
594
639
|
:param bool transpose: Whether to transpose the data, defaults to True
|
|
595
640
|
:rtype: None
|
|
596
641
|
"""
|
|
597
|
-
|
|
642
|
+
d_frame = _remove_nested_dicts_before_saving(data)
|
|
598
643
|
|
|
599
644
|
if transpose:
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
data.to_csv(file)
|
|
645
|
+
d_frame = d_frame.transpose()
|
|
646
|
+
|
|
647
|
+
d_frame.to_csv(file)
|
|
604
648
|
if output_log:
|
|
605
|
-
logger.info("Data successfully saved to: %s", file.
|
|
649
|
+
logger.info("Data successfully saved to: %s", file.absolute())
|
|
606
650
|
|
|
607
651
|
|
|
608
652
|
def save_to_excel(file: Path, data: Any, output_log: bool, transpose: bool = True) -> None:
|
|
@@ -615,15 +659,14 @@ def save_to_excel(file: Path, data: Any, output_log: bool, transpose: bool = Tru
|
|
|
615
659
|
:param bool transpose: Whether to transpose the data, defaults to True
|
|
616
660
|
:rtype: None
|
|
617
661
|
"""
|
|
618
|
-
|
|
662
|
+
d_frame = _remove_nested_dicts_before_saving(data)
|
|
619
663
|
|
|
620
|
-
d_frame = pd.DataFrame(data)
|
|
621
664
|
if transpose:
|
|
622
665
|
d_frame = d_frame.transpose()
|
|
623
666
|
|
|
624
667
|
d_frame.to_excel(file)
|
|
625
668
|
if output_log:
|
|
626
|
-
logger.info("Data successfully saved to: %s", file.
|
|
669
|
+
logger.info("Data successfully saved to: %s", file.absolute())
|
|
627
670
|
|
|
628
671
|
|
|
629
672
|
def save_to_json(file: Path, data: Any, output_log: bool) -> None:
|
|
@@ -642,7 +685,7 @@ def save_to_json(file: Path, data: Any, output_log: bool) -> None:
|
|
|
642
685
|
with open(file, "w", encoding="utf-8") as outfile:
|
|
643
686
|
outfile.write(str(data))
|
|
644
687
|
if output_log:
|
|
645
|
-
logger.info("Data successfully saved to %s", file.
|
|
688
|
+
logger.info("Data successfully saved to %s", file.absolute())
|
|
646
689
|
|
|
647
690
|
|
|
648
691
|
def save_data_to(file: Path, data: Any, output_log: bool = True, transpose_data: bool = True) -> None:
|
|
@@ -1108,3 +1151,34 @@ def extract_vuln_id_from_strings(text: str) -> Union[list, str]:
|
|
|
1108
1151
|
if res:
|
|
1109
1152
|
return res # no need to save spaces
|
|
1110
1153
|
return text
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def filter_list(input_list: list, input_filter: Optional[dict]) -> list:
|
|
1157
|
+
"""
|
|
1158
|
+
Filter an input list based on the filter
|
|
1159
|
+
Implicit "and" between all keys
|
|
1160
|
+
Implicit "or" between values within a key
|
|
1161
|
+
|
|
1162
|
+
:param list filter_list: List of data to be filtered
|
|
1163
|
+
:param dict input_filter: Filter criteria
|
|
1164
|
+
:return: Filtered list
|
|
1165
|
+
:return_type: list
|
|
1166
|
+
"""
|
|
1167
|
+
if not input_filter:
|
|
1168
|
+
return input_list
|
|
1169
|
+
|
|
1170
|
+
filtered_results = []
|
|
1171
|
+
for item in input_list:
|
|
1172
|
+
match = True
|
|
1173
|
+
for key, value in input_filter.items():
|
|
1174
|
+
if isinstance(value, list):
|
|
1175
|
+
if item.get(key) not in value:
|
|
1176
|
+
match = False
|
|
1177
|
+
break
|
|
1178
|
+
elif item.get(key) != value:
|
|
1179
|
+
match = False
|
|
1180
|
+
break
|
|
1181
|
+
if match:
|
|
1182
|
+
filtered_results.append(item)
|
|
1183
|
+
|
|
1184
|
+
return filtered_results
|
|
@@ -87,5 +87,5 @@ def objective_to_control_dot(input_string: str) -> str:
|
|
|
87
87
|
if match:
|
|
88
88
|
return match.group(1)
|
|
89
89
|
else:
|
|
90
|
-
logger.
|
|
90
|
+
logger.debug(f"Failed to convert objective to control: {input_string}")
|
|
91
91
|
return input_string
|