regscale-cli 6.21.2.0__py3-none-any.whl → 6.28.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/api.py +5 -2
- regscale/core/app/application.py +36 -6
- regscale/core/app/internal/control_editor.py +73 -21
- regscale/core/app/internal/evidence.py +727 -204
- regscale/core/app/internal/login.py +4 -2
- regscale/core/app/internal/model_editor.py +219 -64
- regscale/core/app/utils/app_utils.py +86 -12
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/core/login.py +21 -4
- regscale/core/utils/async_graphql_client.py +363 -0
- regscale/core/utils/date.py +77 -1
- regscale/dev/cli.py +26 -0
- regscale/dev/code_gen.py +109 -24
- regscale/dev/version.py +72 -0
- regscale/integrations/commercial/__init__.py +30 -2
- regscale/integrations/commercial/aws/audit_manager_compliance.py +3908 -0
- regscale/integrations/commercial/aws/cli.py +3107 -54
- regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
- regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
- regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
- regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
- regscale/integrations/commercial/{amazon → aws}/common.py +71 -19
- regscale/integrations/commercial/aws/config_compliance.py +914 -0
- regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
- regscale/integrations/commercial/aws/control_compliance_analyzer.py +439 -0
- regscale/integrations/commercial/aws/evidence_generator.py +283 -0
- regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
- regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
- regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
- regscale/integrations/commercial/aws/iam_evidence.py +574 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +338 -22
- regscale/integrations/commercial/aws/inventory/base.py +107 -5
- regscale/integrations/commercial/aws/inventory/resources/analytics.py +390 -0
- regscale/integrations/commercial/aws/inventory/resources/applications.py +234 -0
- regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +328 -9
- regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
- regscale/integrations/commercial/aws/inventory/resources/database.py +481 -31
- regscale/integrations/commercial/aws/inventory/resources/developer_tools.py +253 -0
- regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
- regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
- regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
- regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
- regscale/integrations/commercial/aws/inventory/resources/machine_learning.py +358 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +390 -67
- regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
- regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +288 -29
- regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
- regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
- regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
- regscale/integrations/commercial/aws/kms_evidence.py +879 -0
- regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
- regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
- regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
- regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
- regscale/integrations/commercial/aws/org_evidence.py +666 -0
- regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
- regscale/integrations/commercial/aws/s3_evidence.py +632 -0
- regscale/integrations/commercial/aws/scanner.py +1072 -205
- regscale/integrations/commercial/aws/security_hub.py +319 -0
- regscale/integrations/commercial/aws/session_manager.py +282 -0
- regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
- regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
- regscale/integrations/commercial/jira.py +489 -153
- regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
- regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
- regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
- regscale/integrations/commercial/qualys/__init__.py +167 -68
- regscale/integrations/commercial/qualys/scanner.py +305 -39
- regscale/integrations/commercial/sarif/sairf_importer.py +432 -0
- regscale/integrations/commercial/sarif/sarif_converter.py +67 -0
- regscale/integrations/commercial/sicura/api.py +79 -42
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +83 -44
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/synqly/assets.py +133 -16
- regscale/integrations/commercial/synqly/edr.py +2 -8
- regscale/integrations/commercial/synqly/query_builder.py +536 -0
- regscale/integrations/commercial/synqly/ticketing.py +27 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +165 -28
- regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
- regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
- regscale/integrations/commercial/tenablev2/commands.py +146 -5
- regscale/integrations/commercial/tenablev2/scanner.py +1 -3
- regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
- regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +191 -76
- regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
- regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
- regscale/integrations/commercial/wizv2/compliance_report.py +1592 -0
- regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
- regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +7 -3
- regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +92 -89
- regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
- regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +66 -9
- regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
- regscale/integrations/commercial/wizv2/issue.py +776 -28
- regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
- regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
- regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
- regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
- regscale/integrations/commercial/wizv2/reports.py +243 -0
- regscale/integrations/commercial/wizv2/sbom.py +1 -1
- regscale/integrations/commercial/wizv2/scanner.py +1031 -441
- regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
- regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
- regscale/integrations/commercial/wizv2/variables.py +89 -3
- regscale/integrations/compliance_integration.py +1036 -151
- regscale/integrations/control_matcher.py +432 -0
- regscale/integrations/due_date_handler.py +333 -0
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +14 -0
- regscale/integrations/public/cci_importer.py +834 -0
- regscale/integrations/public/csam/__init__.py +0 -0
- regscale/integrations/public/csam/csam.py +938 -0
- regscale/integrations/public/csam/csam_agency_defined.py +179 -0
- regscale/integrations/public/csam/csam_common.py +154 -0
- regscale/integrations/public/csam/csam_controls.py +432 -0
- regscale/integrations/public/csam/csam_poam.py +124 -0
- regscale/integrations/public/fedramp/click.py +77 -6
- regscale/integrations/public/fedramp/docx_parser.py +10 -1
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +675 -289
- regscale/integrations/public/fedramp/fedramp_five.py +1 -1
- regscale/integrations/public/fedramp/poam/scanner.py +75 -7
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +1961 -430
- regscale/models/integration_models/CCI_List.xml +1 -0
- regscale/models/integration_models/aqua.py +2 -2
- regscale/models/integration_models/cisa_kev_data.json +805 -11
- regscale/models/integration_models/flat_file_importer/__init__.py +5 -8
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/qualys.py +3 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +87 -18
- regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +124 -25
- regscale/models/integration_models/synqly_models/synqly_model.py +89 -16
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +4 -2
- regscale/models/regscale_models/__init__.py +7 -0
- regscale/models/regscale_models/assessment.py +2 -1
- regscale/models/regscale_models/catalog.py +1 -1
- regscale/models/regscale_models/compliance_settings.py +251 -1
- regscale/models/regscale_models/component.py +1 -0
- regscale/models/regscale_models/control_implementation.py +236 -41
- regscale/models/regscale_models/control_objective.py +74 -5
- regscale/models/regscale_models/file.py +2 -0
- regscale/models/regscale_models/form_field_value.py +5 -3
- regscale/models/regscale_models/inheritance.py +44 -0
- regscale/models/regscale_models/issue.py +301 -102
- regscale/models/regscale_models/milestone.py +33 -14
- regscale/models/regscale_models/organization.py +3 -0
- regscale/models/regscale_models/regscale_model.py +310 -73
- regscale/models/regscale_models/security_plan.py +4 -2
- regscale/models/regscale_models/vulnerability.py +3 -3
- regscale/regscale.py +25 -4
- regscale/templates/__init__.py +0 -0
- regscale/utils/threading/threadhandler.py +20 -15
- regscale/validation/record.py +23 -1
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/METADATA +17 -33
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/RECORD +310 -111
- tests/core/__init__.py +0 -0
- tests/core/utils/__init__.py +0 -0
- tests/core/utils/test_async_graphql_client.py +472 -0
- tests/fixtures/test_fixture.py +13 -8
- tests/regscale/core/test_login.py +171 -4
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
- tests/regscale/integrations/commercial/aws/test_aws_analytics_collector.py +260 -0
- tests/regscale/integrations/commercial/aws/test_aws_applications_collector.py +242 -0
- tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
- tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
- tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
- tests/regscale/integrations/commercial/aws/test_aws_developer_tools_collector.py +203 -0
- tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
- tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
- tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
- tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
- tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
- tests/regscale/integrations/commercial/aws/test_aws_machine_learning_collector.py +237 -0
- tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
- tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
- tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
- tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
- tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
- tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
- tests/regscale/integrations/commercial/aws/test_control_compliance_analyzer.py +375 -0
- tests/regscale/integrations/commercial/aws/test_datetime_parsing.py +223 -0
- tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
- tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
- tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
- tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
- tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
- tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
- tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
- tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
- tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
- tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
- tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
- tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
- tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
- tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
- tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3742 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +2204 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +349 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1365 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
- tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
- tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
- tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
- tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
- tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
- tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
- tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
- tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
- tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
- tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
- tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1218 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
- tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
- tests/regscale/integrations/public/__init__.py +0 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_gen_asset_list.py +150 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/public/test_alienvault.py +220 -0
- tests/regscale/integrations/public/test_cci.py +1053 -0
- tests/regscale/integrations/public/test_cisa.py +1021 -0
- tests/regscale/integrations/public/test_emass.py +518 -0
- tests/regscale/integrations/public/test_fedramp.py +1152 -0
- tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
- tests/regscale/integrations/public/test_file_uploads.py +506 -0
- tests/regscale/integrations/public/test_oscal.py +453 -0
- tests/regscale/integrations/test_compliance_status_mapping.py +406 -0
- tests/regscale/integrations/test_control_matcher.py +1421 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_control_implementation.py +118 -3
- tests/regscale/models/test_form_field_value_integration.py +304 -0
- tests/regscale/models/test_issue.py +378 -1
- tests/regscale/models/test_module_integration.py +582 -0
- tests/regscale/models/test_tenable_integrations.py +811 -105
- regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3057
- regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +0 -7388
- regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +0 -9605
- regscale/integrations/public/fedramp/parts_mapper.py +0 -107
- /regscale/integrations/commercial/{amazon → sarif}/__init__.py +0 -0
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/LICENSE +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/WHEEL +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/top_level.txt +0 -0
|
@@ -8,11 +8,10 @@ import tempfile
|
|
|
8
8
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
9
|
from datetime import datetime, timedelta
|
|
10
10
|
from io import BytesIO
|
|
11
|
+
from pathlib import Path
|
|
11
12
|
from typing import TYPE_CHECKING, Any, Optional, Tuple, Union, Literal
|
|
12
13
|
from urllib.parse import urljoin
|
|
13
14
|
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
15
|
if TYPE_CHECKING:
|
|
17
16
|
from regscale.core.app.application import Application
|
|
18
17
|
|
|
@@ -40,7 +39,7 @@ from regscale.models import regscale_id, regscale_module
|
|
|
40
39
|
from regscale.models.regscale_models.file import File
|
|
41
40
|
from regscale.models.regscale_models.issue import Issue
|
|
42
41
|
from regscale.models.regscale_models.task import Task
|
|
43
|
-
from regscale.
|
|
42
|
+
from regscale.integrations.variables import ScannerVariables
|
|
44
43
|
|
|
45
44
|
job_progress = create_progress_object()
|
|
46
45
|
logger = create_logger()
|
|
@@ -98,6 +97,18 @@ def jira():
|
|
|
98
97
|
is_flag=True,
|
|
99
98
|
help="Use token authentication for Jira API instead of basic auth, defaults to False.",
|
|
100
99
|
)
|
|
100
|
+
@click.option(
|
|
101
|
+
"--jql",
|
|
102
|
+
type=click.STRING,
|
|
103
|
+
help="Custom JQL query for filtering Jira issues.",
|
|
104
|
+
required=False,
|
|
105
|
+
)
|
|
106
|
+
@click.option(
|
|
107
|
+
"--poams",
|
|
108
|
+
"-p",
|
|
109
|
+
is_flag=True,
|
|
110
|
+
help="Whether to create/update the incoming issues from Jira as POAMs in RegScale.",
|
|
111
|
+
)
|
|
101
112
|
def issues(
|
|
102
113
|
regscale_id: int,
|
|
103
114
|
regscale_module: str,
|
|
@@ -105,6 +116,8 @@ def issues(
|
|
|
105
116
|
jira_issue_type: str,
|
|
106
117
|
sync_attachments: bool = True,
|
|
107
118
|
token_auth: bool = False,
|
|
119
|
+
jql: Optional[str] = None,
|
|
120
|
+
poams: bool = False,
|
|
108
121
|
):
|
|
109
122
|
"""Sync issues from Jira into RegScale."""
|
|
110
123
|
sync_regscale_and_jira(
|
|
@@ -114,6 +127,8 @@ def issues(
|
|
|
114
127
|
jira_issue_type=jira_issue_type,
|
|
115
128
|
sync_attachments=sync_attachments,
|
|
116
129
|
token_auth=token_auth,
|
|
130
|
+
jql=jql,
|
|
131
|
+
use_poams=poams,
|
|
117
132
|
)
|
|
118
133
|
|
|
119
134
|
|
|
@@ -143,12 +158,19 @@ def issues(
|
|
|
143
158
|
is_flag=True,
|
|
144
159
|
help="Use token authentication for Jira API instead of basic auth, defaults to False.",
|
|
145
160
|
)
|
|
161
|
+
@click.option(
|
|
162
|
+
"--jql",
|
|
163
|
+
type=click.STRING,
|
|
164
|
+
help="Custom JQL query for filtering Jira tasks.",
|
|
165
|
+
required=False,
|
|
166
|
+
)
|
|
146
167
|
def tasks(
|
|
147
168
|
regscale_id: int,
|
|
148
169
|
regscale_module: str,
|
|
149
170
|
jira_project: str,
|
|
150
171
|
sync_attachments: bool = True,
|
|
151
172
|
token_auth: bool = False,
|
|
173
|
+
jql: Optional[str] = None,
|
|
152
174
|
):
|
|
153
175
|
"""Sync tasks from Jira into RegScale."""
|
|
154
176
|
sync_regscale_and_jira(
|
|
@@ -159,6 +181,7 @@ def tasks(
|
|
|
159
181
|
sync_attachments=sync_attachments,
|
|
160
182
|
sync_tasks_only=True,
|
|
161
183
|
token_auth=token_auth,
|
|
184
|
+
jql=jql,
|
|
162
185
|
)
|
|
163
186
|
|
|
164
187
|
|
|
@@ -202,6 +225,8 @@ def sync_regscale_and_jira(
|
|
|
202
225
|
sync_attachments: bool = True,
|
|
203
226
|
sync_tasks_only: bool = False,
|
|
204
227
|
token_auth: bool = False,
|
|
228
|
+
jql: Optional[str] = None,
|
|
229
|
+
use_poams: Optional[bool] = False,
|
|
205
230
|
) -> None:
|
|
206
231
|
"""
|
|
207
232
|
Sync issues, bidirectionally, from Jira into RegScale as issues
|
|
@@ -213,23 +238,35 @@ def sync_regscale_and_jira(
|
|
|
213
238
|
:param bool sync_attachments: Whether to sync attachments in RegScale & Jira, defaults to True
|
|
214
239
|
:param bool sync_tasks_only: Whether to sync only tasks from Jira, defaults to False
|
|
215
240
|
:param bool token_auth: Use token authentication for Jira API, defaults to False
|
|
241
|
+
:param Optional[str] jql: Custom JQL query for filtering Jira issues/tasks, defaults to None
|
|
242
|
+
:param Optional[bool] use_poams: Whether to mark the incoming issues as POAMs in RegScale, defaults to False
|
|
216
243
|
:rtype: None
|
|
217
244
|
"""
|
|
218
245
|
app = check_license()
|
|
219
246
|
api = Api()
|
|
220
247
|
config = app.config
|
|
221
248
|
|
|
249
|
+
# Load custom fields configuration from init.yaml
|
|
250
|
+
if custom_fields := config.get("jiraCustomFields", {}):
|
|
251
|
+
logger.info("Custom field mappings loaded from config: %s", custom_fields)
|
|
252
|
+
else:
|
|
253
|
+
logger.debug("No custom field mappings found in configuration")
|
|
254
|
+
|
|
222
255
|
# see if provided RegScale Module is an accepted option
|
|
223
256
|
verify_provided_module(parent_module)
|
|
224
257
|
|
|
225
258
|
# create Jira client
|
|
226
259
|
jira_client = create_jira_client(config, token_auth)
|
|
227
260
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
261
|
+
# Use custom JQL if provided, otherwise build default JQL
|
|
262
|
+
if jql:
|
|
263
|
+
jql_str = jql
|
|
264
|
+
else:
|
|
265
|
+
jql_str = (
|
|
266
|
+
f"project = '{jira_project}' AND issueType = '{jira_issue_type}'"
|
|
267
|
+
if sync_tasks_only
|
|
268
|
+
else f"project = '{jira_project}'"
|
|
269
|
+
)
|
|
233
270
|
regscale_objects, regscale_attachments = get_regscale_data_and_attachments(
|
|
234
271
|
parent_id=parent_id,
|
|
235
272
|
parent_module=parent_module,
|
|
@@ -274,35 +311,20 @@ def sync_regscale_and_jira(
|
|
|
274
311
|
api=api,
|
|
275
312
|
sync_attachments=sync_attachments,
|
|
276
313
|
attachments=regscale_attachments,
|
|
314
|
+
custom_fields=custom_fields,
|
|
277
315
|
):
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
)
|
|
284
|
-
# create threads to analyze Jira issues and RegScale issues
|
|
285
|
-
create_threads(
|
|
286
|
-
process=update_regscale_issues,
|
|
287
|
-
args=(
|
|
288
|
-
regscale_objects_to_update,
|
|
289
|
-
updating_issues,
|
|
290
|
-
),
|
|
291
|
-
thread_count=len(regscale_objects_to_update),
|
|
292
|
-
)
|
|
293
|
-
# output the final result
|
|
294
|
-
logger.info(
|
|
295
|
-
"%i/%i %s(s) updated in RegScale.",
|
|
296
|
-
len(regscale_objects_to_update),
|
|
297
|
-
len(update_counter),
|
|
298
|
-
output_str,
|
|
299
|
-
)
|
|
316
|
+
for regscale_object in regscale_objects_to_update:
|
|
317
|
+
regscale_object.save(bulk=True)
|
|
318
|
+
if isinstance(regscale_objects[0], Issue):
|
|
319
|
+
Issue.bulk_save()
|
|
320
|
+
elif isinstance(regscale_objects[0], Task):
|
|
321
|
+
Task.bulk_save()
|
|
300
322
|
else:
|
|
301
323
|
logger.info("No %s(s) need to be updated in RegScale.", output_str)
|
|
302
324
|
|
|
303
325
|
if jira_objects:
|
|
304
326
|
return sync_regscale_objects_to_jira(
|
|
305
|
-
jira_objects, regscale_objects, sync_attachments, app, parent_id, parent_module, sync_tasks_only
|
|
327
|
+
jira_objects, regscale_objects, sync_attachments, app, parent_id, parent_module, sync_tasks_only, use_poams
|
|
306
328
|
)
|
|
307
329
|
logger.info("No %s need to be analyzed from Jira.", output_str)
|
|
308
330
|
|
|
@@ -315,6 +337,7 @@ def sync_regscale_objects_to_jira(
|
|
|
315
337
|
parent_id: int,
|
|
316
338
|
parent_module: str,
|
|
317
339
|
sync_tasks_only: bool,
|
|
340
|
+
use_poams: Optional[bool] = False,
|
|
318
341
|
):
|
|
319
342
|
"""
|
|
320
343
|
Sync issues from Jira to RegScale
|
|
@@ -326,6 +349,7 @@ def sync_regscale_objects_to_jira(
|
|
|
326
349
|
:param int parent_id: Parent record ID in RegScale
|
|
327
350
|
:param str parent_module: Parent record module in RegScale
|
|
328
351
|
:param bool sync_tasks_only: Whether to sync only tasks from Jira
|
|
352
|
+
:param bool use_poams: Whether to create/update the incoming issues as POAMs in RegScale, defaults to False
|
|
329
353
|
"""
|
|
330
354
|
issues_closed = []
|
|
331
355
|
with job_progress:
|
|
@@ -347,21 +371,20 @@ def sync_regscale_objects_to_jira(
|
|
|
347
371
|
progress_task=creating_issues,
|
|
348
372
|
)
|
|
349
373
|
else:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
),
|
|
363
|
-
thread_count=len(jira_issues),
|
|
374
|
+
app.thread_manager.submit_tasks_from_list(
|
|
375
|
+
create_and_update_regscale_issues,
|
|
376
|
+
jira_issues,
|
|
377
|
+
regscale_objects,
|
|
378
|
+
use_poams,
|
|
379
|
+
sync_attachments,
|
|
380
|
+
jira_client,
|
|
381
|
+
app,
|
|
382
|
+
parent_id,
|
|
383
|
+
parent_module,
|
|
384
|
+
creating_issues,
|
|
385
|
+
job_progress,
|
|
364
386
|
)
|
|
387
|
+
app.thread_manager.execute_and_verify(timeout=ScannerVariables.timeout)
|
|
365
388
|
logger.info(
|
|
366
389
|
"Analyzed %i Jira %s(s), created %i %s(s), updated %i %s(s), and closed %i %s(s) in RegScale.",
|
|
367
390
|
len(jira_issues),
|
|
@@ -387,8 +410,6 @@ def create_jira_client(
|
|
|
387
410
|
:return: JIRA Client
|
|
388
411
|
:rtype: JIRA
|
|
389
412
|
"""
|
|
390
|
-
from regscale.integrations.variables import ScannerVariables
|
|
391
|
-
|
|
392
413
|
url = config["jiraUrl"]
|
|
393
414
|
token = config["jiraApiToken"]
|
|
394
415
|
jira_user = config["jiraUserName"]
|
|
@@ -399,36 +420,6 @@ def create_jira_client(
|
|
|
399
420
|
return JIRA(basic_auth=(jira_user, token), options={"server": url})
|
|
400
421
|
|
|
401
422
|
|
|
402
|
-
def update_regscale_issues(args: Tuple, thread: int) -> None:
|
|
403
|
-
"""
|
|
404
|
-
Function to compare Jira issues and RegScale issues
|
|
405
|
-
|
|
406
|
-
:param Tuple args: Tuple of args to use during the process
|
|
407
|
-
:param int thread: Thread number of current thread
|
|
408
|
-
:rtype: None
|
|
409
|
-
"""
|
|
410
|
-
# set up local variables from the passed args
|
|
411
|
-
(
|
|
412
|
-
regscale_issues,
|
|
413
|
-
task,
|
|
414
|
-
) = args
|
|
415
|
-
# find which records should be executed by the current thread
|
|
416
|
-
threads = thread_assignment(thread=thread, total_items=len(regscale_issues))
|
|
417
|
-
# iterate through the thread assignment items and process them
|
|
418
|
-
for i in range(len(threads)):
|
|
419
|
-
# set the issue for the thread for later use in the function
|
|
420
|
-
issue = regscale_issues[threads[i]]
|
|
421
|
-
# update the issue in RegScale
|
|
422
|
-
issue.save()
|
|
423
|
-
logger.debug(
|
|
424
|
-
"RegScale Issue %i was updated with the Jira link.",
|
|
425
|
-
issue.id,
|
|
426
|
-
)
|
|
427
|
-
update_counter.append(issue)
|
|
428
|
-
# update progress bar
|
|
429
|
-
job_progress.update(task, advance=1)
|
|
430
|
-
|
|
431
|
-
|
|
432
423
|
def convert_task_status(name: str) -> str:
|
|
433
424
|
"""
|
|
434
425
|
Convert the task status from Jira to RegScale
|
|
@@ -472,7 +463,7 @@ def create_regscale_task_from_jira(config: dict, jira_issue: jiraIssue, parent_i
|
|
|
472
463
|
date_closed = status_change_date
|
|
473
464
|
percent_complete = 100
|
|
474
465
|
|
|
475
|
-
|
|
466
|
+
task = Task(
|
|
476
467
|
title=title,
|
|
477
468
|
status=status,
|
|
478
469
|
description=description,
|
|
@@ -485,6 +476,13 @@ def create_regscale_task_from_jira(config: dict, jira_issue: jiraIssue, parent_i
|
|
|
485
476
|
extra_data={"jiraIssue": jira_issue}, # type: ignore
|
|
486
477
|
)
|
|
487
478
|
|
|
479
|
+
# Apply custom field mappings from Jira to RegScale
|
|
480
|
+
custom_fields = config.get("jiraCustomFields", {})
|
|
481
|
+
if custom_fields:
|
|
482
|
+
apply_custom_fields_to_regscale_object(task, custom_fields, jira_issue)
|
|
483
|
+
|
|
484
|
+
return task
|
|
485
|
+
|
|
488
486
|
|
|
489
487
|
def check_and_close_tasks(existing_tasks: list[Task], all_jira_titles: set[str]) -> list[Task]:
|
|
490
488
|
"""
|
|
@@ -545,9 +543,11 @@ def process_tasks_for_sync(
|
|
|
545
543
|
jira_task = create_regscale_task_from_jira(config, jira_issue, parent_id, parent_module)
|
|
546
544
|
|
|
547
545
|
# Check if we have a matching task in RegScale
|
|
548
|
-
existing_task
|
|
546
|
+
if existing_task := existing_task_map.get(jira_issue.key):
|
|
547
|
+
# Apply custom field mappings from Jira to RegScale for existing tasks
|
|
548
|
+
if custom_fields := config.get("jiraCustomFields", {}):
|
|
549
|
+
apply_custom_fields_to_regscale_object(existing_task, custom_fields, jira_issue)
|
|
549
550
|
|
|
550
|
-
if existing_task:
|
|
551
551
|
# Check if task is closed in Jira and needs to be closed in regscale
|
|
552
552
|
if jira_task.status in closed_statuses and existing_task.status not in closed_statuses:
|
|
553
553
|
existing_task.status = "Closed"
|
|
@@ -659,67 +659,93 @@ def task_and_attachments_sync(
|
|
|
659
659
|
)
|
|
660
660
|
|
|
661
661
|
|
|
662
|
-
def
|
|
662
|
+
def _create_new_regscale_issue(
|
|
663
|
+
jira_issue: jiraIssue, app: "Application", parent_id: int, parent_module: str, is_poam: Optional[bool] = False
|
|
664
|
+
) -> Optional[Issue]:
|
|
665
|
+
"""
|
|
666
|
+
Create a new RegScale issue from a Jira issue
|
|
667
|
+
|
|
668
|
+
:param jiraIssue jira_issue: The Jira issue to create from
|
|
669
|
+
:param Application app: RegScale application object
|
|
670
|
+
:param int parent_id: Parent record ID in RegScale
|
|
671
|
+
:param str parent_module: Parent record module in RegScale
|
|
672
|
+
:param bool is_poam: Whether to create the issue as a POAM in RegScale, defaults to False
|
|
673
|
+
:return: The created RegScale issue or None if creation failed
|
|
674
|
+
:rtype: Optional[Issue]
|
|
675
|
+
"""
|
|
676
|
+
issue = map_jira_to_regscale_issue(
|
|
677
|
+
jira_issue=jira_issue,
|
|
678
|
+
config=app.config,
|
|
679
|
+
parent_id=parent_id,
|
|
680
|
+
parent_module=parent_module,
|
|
681
|
+
is_poam=is_poam,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
if regscale_issue := issue.create():
|
|
685
|
+
logger.debug(
|
|
686
|
+
"Created issue #%i-%s in RegScale.",
|
|
687
|
+
regscale_issue.id,
|
|
688
|
+
regscale_issue.title,
|
|
689
|
+
)
|
|
690
|
+
return regscale_issue
|
|
691
|
+
else:
|
|
692
|
+
logger.warning("Unable to create issue in RegScale.\nIssue: %s", issue.dict())
|
|
693
|
+
return None
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _apply_custom_fields_and_update_issue(regscale_issue: Issue, app: "Application", jira_issue: jiraIssue) -> None:
|
|
697
|
+
"""
|
|
698
|
+
Apply custom field mappings and update a RegScale issue
|
|
699
|
+
|
|
700
|
+
:param Issue regscale_issue: The RegScale issue to update
|
|
701
|
+
:param Application app: RegScale application object
|
|
702
|
+
:param jiraIssue jira_issue: The Jira issue to get data from
|
|
703
|
+
:rtype: None
|
|
704
|
+
"""
|
|
705
|
+
if custom_fields := app.config.get("jiraCustomFields", {}):
|
|
706
|
+
apply_custom_fields_to_regscale_object(regscale_issue, custom_fields, jira_issue)
|
|
707
|
+
updated_regscale_issues.append(regscale_issue.save())
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def create_and_update_regscale_issues(jira_issue: jiraIssue, *args, **_) -> None:
|
|
663
711
|
"""
|
|
664
712
|
Function to create or update issues in RegScale from Jira
|
|
665
713
|
|
|
666
|
-
:param
|
|
667
|
-
:param
|
|
714
|
+
:param jiraIssue jira_issue: Jira issue to create or update in RegScale
|
|
715
|
+
:param args: Additional arguments
|
|
668
716
|
:rtype: None
|
|
669
717
|
"""
|
|
670
|
-
# set up local variables from the passed args
|
|
671
|
-
(
|
|
718
|
+
# set up local variables from the passed args Tuple
|
|
719
|
+
(regscale_issues, use_poams, add_attachments, jira_client, app, parent_id, parent_module, task, progress) = args
|
|
672
720
|
# find which records should be executed by the current thread
|
|
673
|
-
threads = thread_assignment(thread=thread, total_items=len(jira_issues))
|
|
674
721
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
722
|
+
regscale_issue: Optional[Issue] = next((issue for issue in regscale_issues if issue.jiraId == jira_issue.key), None)
|
|
723
|
+
if regscale_issue:
|
|
724
|
+
regscale_issue.isPoam = use_poams
|
|
725
|
+
|
|
726
|
+
# Process the Jira issue based on its status and existing RegScale issue
|
|
727
|
+
if jira_issue.fields.status.name.lower() == "done" and regscale_issue:
|
|
728
|
+
regscale_issue.status = "Closed"
|
|
729
|
+
regscale_issue.dateCompleted = get_current_datetime()
|
|
730
|
+
_apply_custom_fields_and_update_issue(regscale_issue, app, jira_issue)
|
|
731
|
+
elif regscale_issue:
|
|
732
|
+
_apply_custom_fields_and_update_issue(regscale_issue, app, jira_issue)
|
|
733
|
+
else:
|
|
734
|
+
regscale_issue = _create_new_regscale_issue(jira_issue, app, parent_id, parent_module, use_poams)
|
|
735
|
+
if regscale_issue:
|
|
736
|
+
new_regscale_issues.append(regscale_issue)
|
|
737
|
+
|
|
738
|
+
# Handle attachments if needed
|
|
739
|
+
if add_attachments and regscale_issue and jira_issue.fields.attachment:
|
|
740
|
+
compare_files_for_dupes_and_upload(
|
|
741
|
+
jira_issue=jira_issue,
|
|
742
|
+
regscale_object=regscale_issue,
|
|
743
|
+
jira_client=jira_client,
|
|
744
|
+
api=Api(),
|
|
680
745
|
)
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
regscale_issue.status = "Closed"
|
|
685
|
-
regscale_issue.dateCompleted = get_current_datetime()
|
|
686
|
-
# update the issue in RegScale
|
|
687
|
-
updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
|
|
688
|
-
elif regscale_issue:
|
|
689
|
-
# update the issue in RegScale
|
|
690
|
-
updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
|
|
691
|
-
else:
|
|
692
|
-
# map the jira issue to a RegScale issue object
|
|
693
|
-
issue = map_jira_to_regscale_issue(
|
|
694
|
-
jira_issue=jira_issue,
|
|
695
|
-
config=app.config,
|
|
696
|
-
parent_id=parent_id,
|
|
697
|
-
parent_module=parent_module,
|
|
698
|
-
)
|
|
699
|
-
# create the issue in RegScale
|
|
700
|
-
if regscale_issue := Issue.insert_issue(
|
|
701
|
-
app=app,
|
|
702
|
-
issue=issue,
|
|
703
|
-
):
|
|
704
|
-
logger.debug(
|
|
705
|
-
"Created issue #%i-%s in RegScale.",
|
|
706
|
-
regscale_issue.id,
|
|
707
|
-
regscale_issue.title,
|
|
708
|
-
)
|
|
709
|
-
new_regscale_issues.append(regscale_issue)
|
|
710
|
-
else:
|
|
711
|
-
logger.warning("Unable to create issue in RegScale.\nIssue: %s", issue.dict())
|
|
712
|
-
if add_attachments and regscale_issue and jira_issue.fields.attachment:
|
|
713
|
-
# determine which attachments need to be uploaded to prevent duplicates by
|
|
714
|
-
# getting the hashes of all Jira & RegScale attachments
|
|
715
|
-
compare_files_for_dupes_and_upload(
|
|
716
|
-
jira_issue=jira_issue,
|
|
717
|
-
regscale_object=regscale_issue,
|
|
718
|
-
jira_client=jira_client,
|
|
719
|
-
api=Api(),
|
|
720
|
-
)
|
|
721
|
-
# update progress bar
|
|
722
|
-
progress.update(task, advance=1)
|
|
746
|
+
|
|
747
|
+
# update progress bar
|
|
748
|
+
progress.update(task, advance=1)
|
|
723
749
|
|
|
724
750
|
|
|
725
751
|
def sync_regscale_to_jira(
|
|
@@ -730,17 +756,19 @@ def sync_regscale_to_jira(
|
|
|
730
756
|
sync_attachments: bool = True,
|
|
731
757
|
attachments: Optional[dict] = None,
|
|
732
758
|
api: Optional[Api] = None,
|
|
759
|
+
custom_fields: Optional[dict] = None,
|
|
733
760
|
) -> list[Union[Issue, Task]]:
|
|
734
761
|
"""
|
|
735
762
|
Sync issues or tasks from RegScale to Jira
|
|
736
763
|
|
|
737
|
-
:param list[Union[Issue, Task]]
|
|
764
|
+
:param list[Union[Issue, Task]] regscale_objects: list of RegScale issues or tasks to sync to Jira
|
|
738
765
|
:param JIRA jira_client: Jira client to use for issue creation in Jira
|
|
739
766
|
:param str jira_project: Jira Project to create the issues in
|
|
740
767
|
:param str jira_issue_type: Type of issue to create in Jira
|
|
741
768
|
:param bool sync_attachments: Sync attachments from RegScale to Jira, defaults to True
|
|
742
769
|
:param Optional[dict] attachments: Dict of attachments to sync from RegScale to Jira, defaults to None
|
|
743
770
|
:param Optional[Api] api: API object to download attachments, defaults to None
|
|
771
|
+
:param Optional[dict] custom_fields: Custom field mappings from Jira custom fields to RegScale issue fields, defaults to None
|
|
744
772
|
:return: list of RegScale issues or tasks that need to be updated
|
|
745
773
|
:rtype: list[Union[Issue, Task]]
|
|
746
774
|
"""
|
|
@@ -768,6 +796,7 @@ def sync_regscale_to_jira(
|
|
|
768
796
|
add_attachments=sync_attachments,
|
|
769
797
|
attachments=attachments,
|
|
770
798
|
api=api,
|
|
799
|
+
custom_fields=custom_fields,
|
|
771
800
|
)
|
|
772
801
|
# log progress
|
|
773
802
|
new_issue_counter += 1
|
|
@@ -790,7 +819,7 @@ def fetch_jira_objects(
|
|
|
790
819
|
jira_client: JIRA, jira_project: str, jira_issue_type: str, jql_str: str = None, sync_tasks_only: bool = False
|
|
791
820
|
) -> list[jiraIssue]:
|
|
792
821
|
"""
|
|
793
|
-
Fetch all issues from Jira for the provided project
|
|
822
|
+
Fetch all issues from Jira for the provided project using the enhanced search API.
|
|
794
823
|
|
|
795
824
|
:param JIRA jira_client: Jira client to use for the request
|
|
796
825
|
:param str jira_project: Name of the project in Jira
|
|
@@ -800,15 +829,77 @@ def fetch_jira_objects(
|
|
|
800
829
|
:return: List of Jira issues
|
|
801
830
|
:rtype: list[jiraIssue]
|
|
802
831
|
"""
|
|
803
|
-
start_pointer = 0
|
|
804
|
-
page_size = 100
|
|
805
|
-
jira_objects = []
|
|
806
832
|
if sync_tasks_only:
|
|
807
833
|
validate_issue_type(jira_client, jira_issue_type)
|
|
808
834
|
output_str = "task"
|
|
809
835
|
else:
|
|
810
836
|
output_str = "issue"
|
|
811
837
|
logger.info("Fetching %s(s) from Jira...", output_str.lower())
|
|
838
|
+
try:
|
|
839
|
+
max_results = 100 # 100 is the max allowed by Jira
|
|
840
|
+
jira_issues = []
|
|
841
|
+
issue_response = jira_client.enhanced_search_issues(
|
|
842
|
+
jql_str=jql_str or f"project = '{jira_project}'",
|
|
843
|
+
maxResults=max_results,
|
|
844
|
+
)
|
|
845
|
+
jira_issues.extend(issue_response)
|
|
846
|
+
logger.info(
|
|
847
|
+
"%i Jira %s(s) retrieved.",
|
|
848
|
+
len(jira_issues),
|
|
849
|
+
output_str.lower(),
|
|
850
|
+
)
|
|
851
|
+
# Handle pagination if there are more issues to fetch
|
|
852
|
+
while issue_response.nextPageToken:
|
|
853
|
+
issue_response = jira_client.enhanced_search_issues(
|
|
854
|
+
jql_str=jql_str, maxResults=max_results, nextPageToken=issue_response.nextPageToken
|
|
855
|
+
)
|
|
856
|
+
jira_issues.extend(issue_response)
|
|
857
|
+
logger.info(
|
|
858
|
+
"%i Jira %s(s) retrieved.",
|
|
859
|
+
len(jira_issues),
|
|
860
|
+
output_str.lower(),
|
|
861
|
+
)
|
|
862
|
+
# Save artifacts file and log final result if we have issues
|
|
863
|
+
if jira_issues:
|
|
864
|
+
save_jira_issues(jira_issues, jira_project, jira_issue_type)
|
|
865
|
+
logger.info("%i %s(s) retrieved from Jira.", len(jira_issues), output_str.lower())
|
|
866
|
+
return jira_issues
|
|
867
|
+
except Exception as e:
|
|
868
|
+
logger.warning(
|
|
869
|
+
"An error occurred while fetching Jira issues using the enhanced_search_issues method: %s", str(e)
|
|
870
|
+
)
|
|
871
|
+
logger.info("Falling back to the deprecated fetch method...")
|
|
872
|
+
|
|
873
|
+
try:
|
|
874
|
+
return deprecated_fetch_jira_objects(
|
|
875
|
+
jira_client=jira_client,
|
|
876
|
+
jira_project=jira_project,
|
|
877
|
+
jira_issue_type=jira_issue_type,
|
|
878
|
+
jql_str=jql_str,
|
|
879
|
+
output_str=output_str,
|
|
880
|
+
)
|
|
881
|
+
except JIRAError as e:
|
|
882
|
+
error_and_exit(f"Unable to fetch issues from Jira: {e}")
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def deprecated_fetch_jira_objects(
|
|
886
|
+
jira_client: JIRA, jira_project: str, jira_issue_type: str, jql_str: str = None, output_str: str = "issue"
|
|
887
|
+
) -> list[jiraIssue]:
|
|
888
|
+
"""
|
|
889
|
+
Fetch all issues from Jira for the provided project using the old API method, used as a fallback method.
|
|
890
|
+
|
|
891
|
+
:param JIRA jira_client: Jira client to use for the request
|
|
892
|
+
:param str jira_project: Name of the project in Jira
|
|
893
|
+
:param str jira_issue_type: Type of issue to fetch from Jira
|
|
894
|
+
:param str jql_str: JQL string to use for the request, default None
|
|
895
|
+
:param str output_str: String to use for logging, either "issue" or "task"
|
|
896
|
+
:return: List of Jira issues
|
|
897
|
+
:rtype: list[jiraIssue]
|
|
898
|
+
"""
|
|
899
|
+
start_pointer = 0
|
|
900
|
+
page_size = 100
|
|
901
|
+
jira_objects = []
|
|
902
|
+
logger.info("Fetching %s(s) from Jira...", output_str.lower())
|
|
812
903
|
# get all issues for the Jira project
|
|
813
904
|
while True:
|
|
814
905
|
start = start_pointer * page_size
|
|
@@ -829,25 +920,39 @@ def fetch_jira_objects(
|
|
|
829
920
|
output_str.lower(),
|
|
830
921
|
)
|
|
831
922
|
if jira_objects:
|
|
832
|
-
|
|
833
|
-
file_name = f"{jira_project.lower()}_existingJira{jira_issue_type}.json"
|
|
834
|
-
file_path = Path(f"./artifacts/{file_name}")
|
|
835
|
-
save_data_to(
|
|
836
|
-
file=file_path,
|
|
837
|
-
data=[issue.raw for issue in jira_objects],
|
|
838
|
-
output_log=False,
|
|
839
|
-
)
|
|
840
|
-
logger.info(
|
|
841
|
-
"Saved %i Jira %s(s), see %s",
|
|
842
|
-
len(jira_objects),
|
|
843
|
-
jira_issue_type.lower(),
|
|
844
|
-
str(file_path.absolute()),
|
|
845
|
-
)
|
|
923
|
+
save_jira_issues(jira_objects, jira_project, jira_issue_type)
|
|
846
924
|
logger.info("%i %s(s) retrieved from Jira.", len(jira_objects), output_str.lower())
|
|
847
925
|
return jira_objects
|
|
848
926
|
|
|
849
927
|
|
|
850
|
-
def
|
|
928
|
+
def save_jira_issues(jira_issues: list[jiraIssue], jira_project: str, jira_issue_type: str) -> None:
|
|
929
|
+
"""
|
|
930
|
+
Save Jira issues to a JSON file in the artifacts directory
|
|
931
|
+
|
|
932
|
+
:param list[jiraIssue] jira_issues: List of Jira issues to save
|
|
933
|
+
:param str jira_project: Name of the project in Jira
|
|
934
|
+
:param str jira_issue_type: Type of issue to fetch from Jira
|
|
935
|
+
:rtype: None
|
|
936
|
+
"""
|
|
937
|
+
check_file_path("artifacts")
|
|
938
|
+
file_name = f"{jira_project.lower()}_existingJira{jira_issue_type}.json"
|
|
939
|
+
file_path = Path(f"./artifacts/{file_name}")
|
|
940
|
+
save_data_to(
|
|
941
|
+
file=file_path,
|
|
942
|
+
data=[issue.raw for issue in jira_issues],
|
|
943
|
+
output_log=False,
|
|
944
|
+
)
|
|
945
|
+
logger.info(
|
|
946
|
+
"Saved %i Jira %s(s), see %s",
|
|
947
|
+
len(jira_issues),
|
|
948
|
+
jira_issue_type.lower(),
|
|
949
|
+
str(file_path.absolute()),
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def map_jira_to_regscale_issue(
|
|
954
|
+
jira_issue: jiraIssue, config: dict, parent_id: int, parent_module: str, is_poam: Optional[bool] = False
|
|
955
|
+
) -> Issue:
|
|
851
956
|
"""
|
|
852
957
|
Map Jira issues to RegScale issues
|
|
853
958
|
|
|
@@ -855,6 +960,7 @@ def map_jira_to_regscale_issue(jira_issue: jiraIssue, config: dict, parent_id: i
|
|
|
855
960
|
:param dict config: Application config
|
|
856
961
|
:param int parent_id: Parent record ID in RegScale
|
|
857
962
|
:param str parent_module: Parent record module in RegScale
|
|
963
|
+
:param bool is_poam: Whether to create the issue as a POAM in RegScale
|
|
858
964
|
:return: Issue object of the newly created issue in RegScale
|
|
859
965
|
:rtype: Issue
|
|
860
966
|
"""
|
|
@@ -871,11 +977,20 @@ def map_jira_to_regscale_issue(jira_issue: jiraIssue, config: dict, parent_id: i
|
|
|
871
977
|
),
|
|
872
978
|
status=("Closed" if jira_issue.fields.status.name.lower() == "done" else config["issues"]["jira"]["status"]),
|
|
873
979
|
jiraId=jira_issue.key,
|
|
980
|
+
identification="Jira Sync",
|
|
981
|
+
sourceReport="Jira",
|
|
874
982
|
parentId=parent_id,
|
|
875
983
|
parentModule=parent_module,
|
|
876
984
|
dateCreated=get_current_datetime(),
|
|
877
985
|
dateCompleted=(get_current_datetime() if jira_issue.fields.status.name.lower() == "done" else None),
|
|
986
|
+
isPoam=is_poam,
|
|
878
987
|
)
|
|
988
|
+
|
|
989
|
+
# Apply custom field mappings from Jira to RegScale
|
|
990
|
+
custom_fields = config.get("jiraCustomFields", {})
|
|
991
|
+
if custom_fields:
|
|
992
|
+
apply_custom_fields_to_regscale_object(issue, custom_fields, jira_issue)
|
|
993
|
+
|
|
879
994
|
return issue
|
|
880
995
|
|
|
881
996
|
|
|
@@ -929,6 +1044,7 @@ def create_issue_in_jira(
|
|
|
929
1044
|
add_attachments: Optional[bool] = True,
|
|
930
1045
|
attachments: Optional[dict] = None,
|
|
931
1046
|
api: Optional[Api] = None,
|
|
1047
|
+
custom_fields: Optional[dict] = None,
|
|
932
1048
|
) -> jiraIssue:
|
|
933
1049
|
"""
|
|
934
1050
|
Create a new issue in Jira
|
|
@@ -940,6 +1056,7 @@ def create_issue_in_jira(
|
|
|
940
1056
|
:param Optional[bool] add_attachments: Whether to add attachments to new issue, defaults to true
|
|
941
1057
|
:param Optional[dict] attachments: Dictionary containing attachments, defaults to None
|
|
942
1058
|
:param Optional[Api] api: API object to download attachments, defaults to None
|
|
1059
|
+
:param Optional[dict] custom_fields: Custom field mappings from Jira custom fields to RegScale issue fields, defaults to None
|
|
943
1060
|
:return: Newly created issue in Jira
|
|
944
1061
|
:rtype: jiraIssue
|
|
945
1062
|
"""
|
|
@@ -955,6 +1072,11 @@ def create_issue_in_jira(
|
|
|
955
1072
|
issuetype=issue_type,
|
|
956
1073
|
)
|
|
957
1074
|
logger.debug("Jira issue created: %s", new_issue.key)
|
|
1075
|
+
|
|
1076
|
+
# Apply custom field mappings if provided
|
|
1077
|
+
if custom_fields:
|
|
1078
|
+
apply_custom_fields_to_jira_issue(new_issue, custom_fields, regscale_object)
|
|
1079
|
+
|
|
958
1080
|
# add a comment to the new Jira issue
|
|
959
1081
|
logger.debug("Adding comment to Jira issue: %s", new_issue.key)
|
|
960
1082
|
_ = jira_client.add_comment(
|
|
@@ -1214,3 +1336,217 @@ def download_regscale_attachments_to_directory(
|
|
|
1214
1336
|
)
|
|
1215
1337
|
)
|
|
1216
1338
|
return jira_dir, regscale_dir
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def apply_custom_fields_to_jira_issue(
|
|
1342
|
+
jira_issue: jiraIssue, custom_fields: dict, regscale_object: Union[Issue, Task]
|
|
1343
|
+
) -> None:
|
|
1344
|
+
"""
|
|
1345
|
+
Apply custom field mappings to a Jira issue based on RegScale object attributes (RegScale -> Jira)
|
|
1346
|
+
|
|
1347
|
+
:param jiraIssue jira_issue: Jira issue to apply custom fields to
|
|
1348
|
+
:param dict custom_fields: Dictionary mapping Jira custom field names to RegScale attribute names
|
|
1349
|
+
:param Union[Issue, Task] regscale_object: RegScale object to get attribute values from
|
|
1350
|
+
:rtype: None
|
|
1351
|
+
"""
|
|
1352
|
+
if not custom_fields:
|
|
1353
|
+
return
|
|
1354
|
+
|
|
1355
|
+
try:
|
|
1356
|
+
# Convert RegScale object to dictionary for easier attribute access
|
|
1357
|
+
if hasattr(regscale_object, "model_dump"):
|
|
1358
|
+
regscale_dict = regscale_object.model_dump()
|
|
1359
|
+
elif hasattr(regscale_object, "dict"):
|
|
1360
|
+
regscale_dict = regscale_object.dict()
|
|
1361
|
+
else:
|
|
1362
|
+
regscale_dict = regscale_object.__dict__
|
|
1363
|
+
|
|
1364
|
+
# Build custom fields dictionary for Jira update
|
|
1365
|
+
jira_custom_fields = {}
|
|
1366
|
+
|
|
1367
|
+
for jira_field_name, regscale_field_name in custom_fields.items():
|
|
1368
|
+
try:
|
|
1369
|
+
# Get the value from RegScale object
|
|
1370
|
+
field_value = regscale_dict.get(regscale_field_name)
|
|
1371
|
+
|
|
1372
|
+
if field_value is not None:
|
|
1373
|
+
jira_custom_fields[jira_field_name] = field_value
|
|
1374
|
+
logger.debug(
|
|
1375
|
+
"Mapped custom field %s (RegScale: %s) = %s for Jira issue %s",
|
|
1376
|
+
jira_field_name,
|
|
1377
|
+
regscale_field_name,
|
|
1378
|
+
field_value,
|
|
1379
|
+
jira_issue.key,
|
|
1380
|
+
)
|
|
1381
|
+
else:
|
|
1382
|
+
logger.debug(
|
|
1383
|
+
"Custom field %s (RegScale: %s) has no value, skipping for Jira issue %s",
|
|
1384
|
+
jira_field_name,
|
|
1385
|
+
regscale_field_name,
|
|
1386
|
+
jira_issue.key,
|
|
1387
|
+
)
|
|
1388
|
+
except Exception as e:
|
|
1389
|
+
logger.warning(
|
|
1390
|
+
"Unable to set custom field %s (RegScale: %s) for Jira issue %s: %s",
|
|
1391
|
+
jira_field_name,
|
|
1392
|
+
regscale_field_name,
|
|
1393
|
+
jira_issue.key,
|
|
1394
|
+
str(e),
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
# Update the Jira issue with custom fields if any were found
|
|
1398
|
+
if jira_custom_fields:
|
|
1399
|
+
jira_issue.update(fields=jira_custom_fields)
|
|
1400
|
+
logger.info(
|
|
1401
|
+
"Applied %d custom fields to Jira issue %s: %s",
|
|
1402
|
+
len(jira_custom_fields),
|
|
1403
|
+
jira_issue.key,
|
|
1404
|
+
list(jira_custom_fields.keys()),
|
|
1405
|
+
)
|
|
1406
|
+
else:
|
|
1407
|
+
logger.debug("No custom field values found for Jira issue %s", jira_issue.key)
|
|
1408
|
+
|
|
1409
|
+
except Exception as e:
|
|
1410
|
+
logger.warning(
|
|
1411
|
+
"Error applying custom fields to Jira issue %s: %s",
|
|
1412
|
+
jira_issue.key,
|
|
1413
|
+
str(e),
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
def _get_jira_field_value(jira_issue: jiraIssue, jira_field_name: str) -> Optional[Any]:
|
|
1418
|
+
"""
|
|
1419
|
+
Get a custom field value from a Jira issue
|
|
1420
|
+
|
|
1421
|
+
:param jiraIssue jira_issue: The Jira issue to get the field value from
|
|
1422
|
+
:param str jira_field_name: The name of the field to retrieve
|
|
1423
|
+
:return: The field value or None if not found
|
|
1424
|
+
:rtype: Optional[Any]
|
|
1425
|
+
"""
|
|
1426
|
+
# Try to access the custom field from the Jira issue
|
|
1427
|
+
if hasattr(jira_issue.fields, jira_field_name):
|
|
1428
|
+
return getattr(jira_issue.fields, jira_field_name)
|
|
1429
|
+
|
|
1430
|
+
# Try accessing through raw fields (for custom fields)
|
|
1431
|
+
if hasattr(jira_issue.fields, "raw") and jira_field_name in jira_issue.fields.raw:
|
|
1432
|
+
return jira_issue.fields.raw[jira_field_name]
|
|
1433
|
+
|
|
1434
|
+
return None
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
def _set_regscale_field_value(
|
|
1438
|
+
regscale_object: Union[Issue, Task],
|
|
1439
|
+
regscale_field_name: str,
|
|
1440
|
+
jira_field_value: Any,
|
|
1441
|
+
jira_field_name: str,
|
|
1442
|
+
jira_issue: jiraIssue,
|
|
1443
|
+
) -> bool:
|
|
1444
|
+
"""
|
|
1445
|
+
Set a field value on a RegScale object
|
|
1446
|
+
|
|
1447
|
+
:param Union[Issue, Task] regscale_object: The RegScale object to set the field on
|
|
1448
|
+
:param str regscale_field_name: The name of the field to set
|
|
1449
|
+
:param Any jira_field_value: The value to set
|
|
1450
|
+
:param str jira_field_name: The Jira field name for logging
|
|
1451
|
+
:param jiraIssue jira_issue: The Jira issue for logging
|
|
1452
|
+
:return: True if the field was set successfully, False otherwise
|
|
1453
|
+
:rtype: bool
|
|
1454
|
+
"""
|
|
1455
|
+
if hasattr(regscale_object, regscale_field_name):
|
|
1456
|
+
setattr(regscale_object, regscale_field_name, jira_field_value)
|
|
1457
|
+
logger.debug(
|
|
1458
|
+
"Mapped custom field %s (Jira: %s) = %s for RegScale %s #%s from Jira issue %s",
|
|
1459
|
+
regscale_field_name,
|
|
1460
|
+
jira_field_name,
|
|
1461
|
+
jira_field_value,
|
|
1462
|
+
regscale_object.get_module_string().title(),
|
|
1463
|
+
regscale_object.id,
|
|
1464
|
+
jira_issue.key,
|
|
1465
|
+
)
|
|
1466
|
+
return True
|
|
1467
|
+
else:
|
|
1468
|
+
logger.debug(
|
|
1469
|
+
"RegScale object does not have field %s, skipping custom field %s from Jira issue %s",
|
|
1470
|
+
regscale_field_name,
|
|
1471
|
+
jira_field_name,
|
|
1472
|
+
jira_issue.key,
|
|
1473
|
+
)
|
|
1474
|
+
return False
|
|
1475
|
+
|
|
1476
|
+
|
|
1477
|
+
def _process_single_custom_field(
|
|
1478
|
+
jira_field_name: str, regscale_field_name: str, regscale_object: Union[Issue, Task], jira_issue: jiraIssue
|
|
1479
|
+
) -> bool:
|
|
1480
|
+
"""
|
|
1481
|
+
Process a single custom field mapping from Jira to RegScale
|
|
1482
|
+
|
|
1483
|
+
:param str jira_field_name: The Jira field name
|
|
1484
|
+
:param str regscale_field_name: The RegScale field name
|
|
1485
|
+
:param Union[Issue, Task] regscale_object: The RegScale object to update
|
|
1486
|
+
:param jiraIssue jira_issue: The Jira issue to get data from
|
|
1487
|
+
:return: True if the field was processed successfully, False otherwise
|
|
1488
|
+
:rtype: bool
|
|
1489
|
+
"""
|
|
1490
|
+
try:
|
|
1491
|
+
jira_field_value = _get_jira_field_value(jira_issue, jira_field_name)
|
|
1492
|
+
|
|
1493
|
+
if jira_field_value is not None:
|
|
1494
|
+
return _set_regscale_field_value(
|
|
1495
|
+
regscale_object, regscale_field_name, jira_field_value, jira_field_name, jira_issue
|
|
1496
|
+
)
|
|
1497
|
+
else:
|
|
1498
|
+
logger.debug(
|
|
1499
|
+
"Custom field %s has no value in Jira issue %s, skipping",
|
|
1500
|
+
jira_field_name,
|
|
1501
|
+
jira_issue.key,
|
|
1502
|
+
)
|
|
1503
|
+
return False
|
|
1504
|
+
except Exception as e:
|
|
1505
|
+
logger.warning(
|
|
1506
|
+
"Unable to set custom field %s (Jira: %s) for RegScale %s #%s: %s",
|
|
1507
|
+
regscale_field_name,
|
|
1508
|
+
jira_field_name,
|
|
1509
|
+
regscale_object.get_module_string().title(),
|
|
1510
|
+
regscale_object.id,
|
|
1511
|
+
str(e),
|
|
1512
|
+
)
|
|
1513
|
+
return False
|
|
1514
|
+
|
|
1515
|
+
|
|
1516
|
+
def apply_custom_fields_to_regscale_object(
|
|
1517
|
+
regscale_object: Union[Issue, Task], custom_fields: dict, jira_issue: jiraIssue
|
|
1518
|
+
) -> None:
|
|
1519
|
+
"""
|
|
1520
|
+
Apply custom field mappings to a RegScale object based on Jira issue custom fields (Jira -> RegScale)
|
|
1521
|
+
|
|
1522
|
+
:param Union[Issue, Task] regscale_object: RegScale object to apply custom fields to
|
|
1523
|
+
:param dict custom_fields: Dictionary mapping Jira custom field names to RegScale attribute names
|
|
1524
|
+
:param jiraIssue jira_issue: Jira issue to get custom field values from
|
|
1525
|
+
:rtype: None
|
|
1526
|
+
"""
|
|
1527
|
+
if not custom_fields:
|
|
1528
|
+
return
|
|
1529
|
+
|
|
1530
|
+
try:
|
|
1531
|
+
fields_updated = False
|
|
1532
|
+
|
|
1533
|
+
for jira_field_name, regscale_field_name in custom_fields.items():
|
|
1534
|
+
if _process_single_custom_field(jira_field_name, regscale_field_name, regscale_object, jira_issue):
|
|
1535
|
+
fields_updated = True
|
|
1536
|
+
|
|
1537
|
+
if fields_updated:
|
|
1538
|
+
logger.info(
|
|
1539
|
+
"Applied custom fields from Jira issue %s to RegScale %s #%s",
|
|
1540
|
+
jira_issue.key,
|
|
1541
|
+
regscale_object.get_module_string().title(),
|
|
1542
|
+
regscale_object.id,
|
|
1543
|
+
)
|
|
1544
|
+
|
|
1545
|
+
except Exception as e:
|
|
1546
|
+
logger.warning(
|
|
1547
|
+
"Error applying custom fields from Jira issue %s to RegScale %s #%s: %s",
|
|
1548
|
+
jira_issue.key,
|
|
1549
|
+
regscale_object.get_module_string().title(),
|
|
1550
|
+
regscale_object.id,
|
|
1551
|
+
str(e),
|
|
1552
|
+
)
|