regscale-cli 6.16.0.0__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.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/__init__.py +1 -0
- regscale/airflow/__init__.py +9 -0
- regscale/airflow/azure/__init__.py +9 -0
- regscale/airflow/azure/cli.py +89 -0
- regscale/airflow/azure/upload_dags.py +116 -0
- regscale/airflow/click_dags.py +127 -0
- regscale/airflow/click_mixins.py +82 -0
- regscale/airflow/config.py +25 -0
- regscale/airflow/factories/__init__.py +0 -0
- regscale/airflow/factories/connections.py +58 -0
- regscale/airflow/factories/workflows.py +78 -0
- regscale/airflow/hierarchy.py +88 -0
- regscale/airflow/operators/__init__.py +0 -0
- regscale/airflow/operators/click.py +36 -0
- regscale/airflow/sensors/__init__.py +0 -0
- regscale/airflow/sensors/sql.py +107 -0
- regscale/airflow/sessions/__init__.py +0 -0
- regscale/airflow/sessions/sql/__init__.py +3 -0
- regscale/airflow/sessions/sql/queries.py +64 -0
- regscale/airflow/sessions/sql/sql_server_queries.py +248 -0
- regscale/airflow/tasks/__init__.py +0 -0
- regscale/airflow/tasks/branches.py +22 -0
- regscale/airflow/tasks/cli.py +116 -0
- regscale/airflow/tasks/click.py +73 -0
- regscale/airflow/tasks/debugging.py +9 -0
- regscale/airflow/tasks/groups.py +116 -0
- regscale/airflow/tasks/init.py +60 -0
- regscale/airflow/tasks/states.py +47 -0
- regscale/airflow/tasks/workflows.py +36 -0
- regscale/ansible/__init__.py +9 -0
- regscale/core/__init__.py +0 -0
- regscale/core/app/__init__.py +3 -0
- regscale/core/app/api.py +571 -0
- regscale/core/app/application.py +665 -0
- regscale/core/app/internal/__init__.py +136 -0
- regscale/core/app/internal/admin_actions.py +230 -0
- regscale/core/app/internal/assessments_editor.py +873 -0
- regscale/core/app/internal/catalog.py +316 -0
- regscale/core/app/internal/comparison.py +459 -0
- regscale/core/app/internal/control_editor.py +571 -0
- regscale/core/app/internal/encrypt.py +79 -0
- regscale/core/app/internal/evidence.py +1240 -0
- regscale/core/app/internal/file_uploads.py +151 -0
- regscale/core/app/internal/healthcheck.py +66 -0
- regscale/core/app/internal/login.py +305 -0
- regscale/core/app/internal/migrations.py +240 -0
- regscale/core/app/internal/model_editor.py +1701 -0
- regscale/core/app/internal/poam_editor.py +632 -0
- regscale/core/app/internal/workflow.py +105 -0
- regscale/core/app/logz.py +74 -0
- regscale/core/app/utils/XMLIR.py +258 -0
- regscale/core/app/utils/__init__.py +0 -0
- regscale/core/app/utils/api_handler.py +358 -0
- regscale/core/app/utils/app_utils.py +1110 -0
- regscale/core/app/utils/catalog_utils/__init__.py +0 -0
- regscale/core/app/utils/catalog_utils/common.py +91 -0
- regscale/core/app/utils/catalog_utils/compare_catalog.py +193 -0
- regscale/core/app/utils/catalog_utils/diagnostic_catalog.py +97 -0
- regscale/core/app/utils/catalog_utils/download_catalog.py +103 -0
- regscale/core/app/utils/catalog_utils/update_catalog.py +718 -0
- regscale/core/app/utils/catalog_utils/update_catalog_v2.py +1378 -0
- regscale/core/app/utils/catalog_utils/update_catalog_v3.py +1272 -0
- regscale/core/app/utils/catalog_utils/update_plans.py +334 -0
- regscale/core/app/utils/file_utils.py +238 -0
- regscale/core/app/utils/parser_utils.py +81 -0
- regscale/core/app/utils/pickle_file_handler.py +57 -0
- regscale/core/app/utils/regscale_utils.py +319 -0
- regscale/core/app/utils/report_utils.py +119 -0
- regscale/core/app/utils/variables.py +226 -0
- regscale/core/decorators.py +31 -0
- regscale/core/lazy_group.py +65 -0
- regscale/core/login.py +63 -0
- regscale/core/server/__init__.py +0 -0
- regscale/core/server/flask_api.py +473 -0
- regscale/core/server/helpers.py +373 -0
- regscale/core/server/rest.py +64 -0
- regscale/core/server/static/css/bootstrap.css +6030 -0
- regscale/core/server/static/css/bootstrap.min.css +6 -0
- regscale/core/server/static/css/main.css +176 -0
- regscale/core/server/static/images/regscale-cli.svg +49 -0
- regscale/core/server/static/images/regscale.svg +38 -0
- regscale/core/server/templates/base.html +74 -0
- regscale/core/server/templates/index.html +43 -0
- regscale/core/server/templates/login.html +28 -0
- regscale/core/server/templates/make_base64.html +22 -0
- regscale/core/server/templates/upload_STIG.html +109 -0
- regscale/core/server/templates/upload_STIG_result.html +26 -0
- regscale/core/server/templates/upload_ssp.html +144 -0
- regscale/core/server/templates/upload_ssp_result.html +128 -0
- regscale/core/static/__init__.py +0 -0
- regscale/core/static/regex.py +14 -0
- regscale/core/utils/__init__.py +117 -0
- regscale/core/utils/click_utils.py +13 -0
- regscale/core/utils/date.py +238 -0
- regscale/core/utils/graphql.py +254 -0
- regscale/core/utils/urls.py +23 -0
- regscale/dev/__init__.py +6 -0
- regscale/dev/analysis.py +454 -0
- regscale/dev/cli.py +235 -0
- regscale/dev/code_gen.py +492 -0
- regscale/dev/dirs.py +69 -0
- regscale/dev/docs.py +384 -0
- regscale/dev/monitoring.py +26 -0
- regscale/dev/profiling.py +216 -0
- regscale/exceptions/__init__.py +4 -0
- regscale/exceptions/license_exception.py +7 -0
- regscale/exceptions/validation_exception.py +9 -0
- regscale/integrations/__init__.py +1 -0
- regscale/integrations/commercial/__init__.py +486 -0
- regscale/integrations/commercial/ad.py +433 -0
- regscale/integrations/commercial/amazon/__init__.py +0 -0
- regscale/integrations/commercial/amazon/common.py +106 -0
- regscale/integrations/commercial/aqua/__init__.py +0 -0
- regscale/integrations/commercial/aqua/aqua.py +91 -0
- regscale/integrations/commercial/aws/__init__.py +6 -0
- regscale/integrations/commercial/aws/cli.py +322 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +110 -0
- regscale/integrations/commercial/aws/inventory/base.py +64 -0
- regscale/integrations/commercial/aws/inventory/resources/__init__.py +19 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +234 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +113 -0
- regscale/integrations/commercial/aws/inventory/resources/database.py +101 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +237 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +253 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +240 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +91 -0
- regscale/integrations/commercial/aws/scanner.py +823 -0
- regscale/integrations/commercial/azure/__init__.py +0 -0
- regscale/integrations/commercial/azure/common.py +32 -0
- regscale/integrations/commercial/azure/intune.py +488 -0
- regscale/integrations/commercial/azure/scanner.py +49 -0
- regscale/integrations/commercial/burp.py +78 -0
- regscale/integrations/commercial/cpe.py +144 -0
- regscale/integrations/commercial/crowdstrike.py +1117 -0
- regscale/integrations/commercial/defender.py +1511 -0
- regscale/integrations/commercial/dependabot.py +210 -0
- regscale/integrations/commercial/durosuite/__init__.py +0 -0
- regscale/integrations/commercial/durosuite/api.py +1546 -0
- regscale/integrations/commercial/durosuite/process_devices.py +101 -0
- regscale/integrations/commercial/durosuite/scanner.py +637 -0
- regscale/integrations/commercial/durosuite/variables.py +21 -0
- regscale/integrations/commercial/ecr.py +90 -0
- regscale/integrations/commercial/gcp/__init__.py +237 -0
- regscale/integrations/commercial/gcp/auth.py +96 -0
- regscale/integrations/commercial/gcp/control_tests.py +238 -0
- regscale/integrations/commercial/gcp/variables.py +18 -0
- regscale/integrations/commercial/gitlab.py +332 -0
- regscale/integrations/commercial/grype.py +165 -0
- regscale/integrations/commercial/ibm.py +90 -0
- regscale/integrations/commercial/import_all/__init__.py +0 -0
- regscale/integrations/commercial/import_all/import_all_cmd.py +467 -0
- regscale/integrations/commercial/import_all/scan_file_fingerprints.json +27 -0
- regscale/integrations/commercial/jira.py +1046 -0
- regscale/integrations/commercial/mappings/__init__.py +0 -0
- regscale/integrations/commercial/mappings/csf_controls.json +713 -0
- regscale/integrations/commercial/mappings/nist_800_53_r5_controls.json +1516 -0
- regscale/integrations/commercial/nessus/__init__.py +0 -0
- regscale/integrations/commercial/nessus/nessus_utils.py +429 -0
- regscale/integrations/commercial/nessus/scanner.py +416 -0
- regscale/integrations/commercial/nexpose.py +90 -0
- regscale/integrations/commercial/okta.py +798 -0
- regscale/integrations/commercial/opentext/__init__.py +0 -0
- regscale/integrations/commercial/opentext/click.py +99 -0
- regscale/integrations/commercial/opentext/scanner.py +143 -0
- regscale/integrations/commercial/prisma.py +91 -0
- regscale/integrations/commercial/qualys.py +1462 -0
- regscale/integrations/commercial/salesforce.py +980 -0
- regscale/integrations/commercial/sap/__init__.py +0 -0
- regscale/integrations/commercial/sap/click.py +31 -0
- regscale/integrations/commercial/sap/sysdig/__init__.py +0 -0
- regscale/integrations/commercial/sap/sysdig/click.py +57 -0
- regscale/integrations/commercial/sap/sysdig/sysdig_scanner.py +190 -0
- regscale/integrations/commercial/sap/tenable/__init__.py +0 -0
- regscale/integrations/commercial/sap/tenable/click.py +49 -0
- regscale/integrations/commercial/sap/tenable/scanner.py +196 -0
- regscale/integrations/commercial/servicenow.py +1756 -0
- regscale/integrations/commercial/sicura/__init__.py +0 -0
- regscale/integrations/commercial/sicura/api.py +855 -0
- regscale/integrations/commercial/sicura/commands.py +73 -0
- regscale/integrations/commercial/sicura/scanner.py +481 -0
- regscale/integrations/commercial/sicura/variables.py +16 -0
- regscale/integrations/commercial/snyk.py +90 -0
- regscale/integrations/commercial/sonarcloud.py +260 -0
- regscale/integrations/commercial/sqlserver.py +369 -0
- regscale/integrations/commercial/stig_mapper_integration/__init__.py +0 -0
- regscale/integrations/commercial/stig_mapper_integration/click_commands.py +38 -0
- regscale/integrations/commercial/stig_mapper_integration/mapping_engine.py +353 -0
- regscale/integrations/commercial/stigv2/__init__.py +0 -0
- regscale/integrations/commercial/stigv2/ckl_parser.py +349 -0
- regscale/integrations/commercial/stigv2/click_commands.py +95 -0
- regscale/integrations/commercial/stigv2/stig_integration.py +202 -0
- regscale/integrations/commercial/synqly/__init__.py +0 -0
- regscale/integrations/commercial/synqly/assets.py +46 -0
- regscale/integrations/commercial/synqly/ticketing.py +132 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +223 -0
- regscale/integrations/commercial/synqly_jira.py +840 -0
- regscale/integrations/commercial/tenablev2/__init__.py +0 -0
- regscale/integrations/commercial/tenablev2/authenticate.py +31 -0
- regscale/integrations/commercial/tenablev2/click.py +1584 -0
- regscale/integrations/commercial/tenablev2/scanner.py +504 -0
- regscale/integrations/commercial/tenablev2/stig_parsers.py +140 -0
- regscale/integrations/commercial/tenablev2/utils.py +78 -0
- regscale/integrations/commercial/tenablev2/variables.py +17 -0
- regscale/integrations/commercial/trivy.py +162 -0
- regscale/integrations/commercial/veracode.py +96 -0
- regscale/integrations/commercial/wizv2/WizDataMixin.py +97 -0
- regscale/integrations/commercial/wizv2/__init__.py +0 -0
- regscale/integrations/commercial/wizv2/click.py +429 -0
- regscale/integrations/commercial/wizv2/constants.py +1001 -0
- regscale/integrations/commercial/wizv2/issue.py +361 -0
- regscale/integrations/commercial/wizv2/models.py +112 -0
- regscale/integrations/commercial/wizv2/parsers.py +339 -0
- regscale/integrations/commercial/wizv2/sbom.py +115 -0
- regscale/integrations/commercial/wizv2/scanner.py +416 -0
- regscale/integrations/commercial/wizv2/utils.py +796 -0
- regscale/integrations/commercial/wizv2/variables.py +39 -0
- regscale/integrations/commercial/wizv2/wiz_auth.py +159 -0
- regscale/integrations/commercial/xray.py +91 -0
- regscale/integrations/integration/__init__.py +2 -0
- regscale/integrations/integration/integration.py +26 -0
- regscale/integrations/integration/inventory.py +17 -0
- regscale/integrations/integration/issue.py +100 -0
- regscale/integrations/integration_override.py +149 -0
- regscale/integrations/public/__init__.py +103 -0
- regscale/integrations/public/cisa.py +641 -0
- regscale/integrations/public/criticality_updater.py +70 -0
- regscale/integrations/public/emass.py +411 -0
- regscale/integrations/public/emass_slcm_import.py +697 -0
- regscale/integrations/public/fedramp/__init__.py +0 -0
- regscale/integrations/public/fedramp/appendix_parser.py +548 -0
- regscale/integrations/public/fedramp/click.py +479 -0
- regscale/integrations/public/fedramp/components.py +714 -0
- regscale/integrations/public/fedramp/docx_parser.py +259 -0
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +1124 -0
- regscale/integrations/public/fedramp/fedramp_common.py +3181 -0
- regscale/integrations/public/fedramp/fedramp_docx.py +388 -0
- regscale/integrations/public/fedramp/fedramp_five.py +2343 -0
- regscale/integrations/public/fedramp/fedramp_traversal.py +138 -0
- regscale/integrations/public/fedramp/import_fedramp_r4_ssp.py +279 -0
- regscale/integrations/public/fedramp/import_workbook.py +495 -0
- regscale/integrations/public/fedramp/inventory_items.py +244 -0
- regscale/integrations/public/fedramp/mappings/__init__.py +0 -0
- regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +7388 -0
- regscale/integrations/public/fedramp/mappings/fedramp_r5_params.json +8636 -0
- regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +9605 -0
- regscale/integrations/public/fedramp/mappings/system_roles.py +34 -0
- regscale/integrations/public/fedramp/mappings/user.py +175 -0
- regscale/integrations/public/fedramp/mappings/values.py +141 -0
- regscale/integrations/public/fedramp/markdown_parser.py +150 -0
- regscale/integrations/public/fedramp/metadata.py +689 -0
- regscale/integrations/public/fedramp/models/__init__.py +59 -0
- regscale/integrations/public/fedramp/models/leveraged_auth_new.py +168 -0
- regscale/integrations/public/fedramp/models/poam_importer.py +522 -0
- regscale/integrations/public/fedramp/parts_mapper.py +107 -0
- regscale/integrations/public/fedramp/poam/__init__.py +0 -0
- regscale/integrations/public/fedramp/poam/scanner.py +851 -0
- regscale/integrations/public/fedramp/properties.py +201 -0
- regscale/integrations/public/fedramp/reporting.py +84 -0
- regscale/integrations/public/fedramp/resources.py +496 -0
- regscale/integrations/public/fedramp/rosetta.py +110 -0
- regscale/integrations/public/fedramp/ssp_logger.py +87 -0
- regscale/integrations/public/fedramp/system_characteristics.py +922 -0
- regscale/integrations/public/fedramp/system_control_implementations.py +582 -0
- regscale/integrations/public/fedramp/system_implementation.py +190 -0
- regscale/integrations/public/fedramp/xml_utils.py +87 -0
- regscale/integrations/public/nist_catalog.py +275 -0
- regscale/integrations/public/oscal.py +1946 -0
- regscale/integrations/public/otx.py +169 -0
- regscale/integrations/scanner_integration.py +2692 -0
- regscale/integrations/variables.py +25 -0
- regscale/models/__init__.py +7 -0
- regscale/models/app_models/__init__.py +5 -0
- regscale/models/app_models/catalog_compare.py +213 -0
- regscale/models/app_models/click.py +252 -0
- regscale/models/app_models/datetime_encoder.py +21 -0
- regscale/models/app_models/import_validater.py +321 -0
- regscale/models/app_models/mapping.py +260 -0
- regscale/models/app_models/pipeline.py +37 -0
- regscale/models/click_models.py +413 -0
- regscale/models/config.py +154 -0
- regscale/models/email_style.css +67 -0
- regscale/models/hierarchy.py +8 -0
- regscale/models/inspect_models.py +79 -0
- regscale/models/integration_models/__init__.py +0 -0
- regscale/models/integration_models/amazon_models/__init__.py +0 -0
- regscale/models/integration_models/amazon_models/inspector.py +262 -0
- regscale/models/integration_models/amazon_models/inspector_scan.py +206 -0
- regscale/models/integration_models/aqua.py +247 -0
- regscale/models/integration_models/azure_alerts.py +255 -0
- regscale/models/integration_models/base64.py +23 -0
- regscale/models/integration_models/burp.py +433 -0
- regscale/models/integration_models/burp_models.py +128 -0
- regscale/models/integration_models/cisa_kev_data.json +19333 -0
- regscale/models/integration_models/defender_data.py +93 -0
- regscale/models/integration_models/defenderimport.py +143 -0
- regscale/models/integration_models/drf.py +443 -0
- regscale/models/integration_models/ecr_models/__init__.py +0 -0
- regscale/models/integration_models/ecr_models/data.py +69 -0
- regscale/models/integration_models/ecr_models/ecr.py +239 -0
- regscale/models/integration_models/flat_file_importer.py +1079 -0
- regscale/models/integration_models/grype_import.py +247 -0
- regscale/models/integration_models/ibm.py +126 -0
- regscale/models/integration_models/implementation_results.py +85 -0
- regscale/models/integration_models/nexpose.py +140 -0
- regscale/models/integration_models/prisma.py +202 -0
- regscale/models/integration_models/qualys.py +720 -0
- regscale/models/integration_models/qualys_scanner.py +160 -0
- regscale/models/integration_models/sbom/__init__.py +0 -0
- regscale/models/integration_models/sbom/cyclone_dx.py +139 -0
- regscale/models/integration_models/send_reminders.py +620 -0
- regscale/models/integration_models/snyk.py +155 -0
- regscale/models/integration_models/synqly_models/__init__.py +0 -0
- regscale/models/integration_models/synqly_models/capabilities.json +1 -0
- regscale/models/integration_models/synqly_models/connector_types.py +22 -0
- regscale/models/integration_models/synqly_models/connectors/__init__.py +7 -0
- regscale/models/integration_models/synqly_models/connectors/assets.py +97 -0
- regscale/models/integration_models/synqly_models/connectors/ticketing.py +583 -0
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +169 -0
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +331 -0
- regscale/models/integration_models/synqly_models/param.py +72 -0
- regscale/models/integration_models/synqly_models/synqly_model.py +733 -0
- regscale/models/integration_models/synqly_models/tenants.py +39 -0
- regscale/models/integration_models/tenable_models/__init__.py +0 -0
- regscale/models/integration_models/tenable_models/integration.py +187 -0
- regscale/models/integration_models/tenable_models/models.py +513 -0
- regscale/models/integration_models/trivy_import.py +231 -0
- regscale/models/integration_models/veracode.py +217 -0
- regscale/models/integration_models/xray.py +135 -0
- regscale/models/locking.py +100 -0
- regscale/models/platform.py +110 -0
- regscale/models/regscale_models/__init__.py +67 -0
- regscale/models/regscale_models/assessment.py +570 -0
- regscale/models/regscale_models/assessment_plan.py +52 -0
- regscale/models/regscale_models/asset.py +567 -0
- regscale/models/regscale_models/asset_mapping.py +190 -0
- regscale/models/regscale_models/case.py +42 -0
- regscale/models/regscale_models/catalog.py +261 -0
- regscale/models/regscale_models/cci.py +46 -0
- regscale/models/regscale_models/change.py +167 -0
- regscale/models/regscale_models/checklist.py +372 -0
- regscale/models/regscale_models/comment.py +49 -0
- regscale/models/regscale_models/compliance_settings.py +112 -0
- regscale/models/regscale_models/component.py +412 -0
- regscale/models/regscale_models/component_mapping.py +65 -0
- regscale/models/regscale_models/control.py +38 -0
- regscale/models/regscale_models/control_implementation.py +1128 -0
- regscale/models/regscale_models/control_objective.py +261 -0
- regscale/models/regscale_models/control_parameter.py +100 -0
- regscale/models/regscale_models/control_test.py +34 -0
- regscale/models/regscale_models/control_test_plan.py +75 -0
- regscale/models/regscale_models/control_test_result.py +52 -0
- regscale/models/regscale_models/custom_field.py +245 -0
- regscale/models/regscale_models/data.py +109 -0
- regscale/models/regscale_models/data_center.py +40 -0
- regscale/models/regscale_models/deviation.py +203 -0
- regscale/models/regscale_models/email.py +97 -0
- regscale/models/regscale_models/evidence.py +47 -0
- regscale/models/regscale_models/evidence_mapping.py +40 -0
- regscale/models/regscale_models/facility.py +59 -0
- regscale/models/regscale_models/file.py +382 -0
- regscale/models/regscale_models/filetag.py +37 -0
- regscale/models/regscale_models/form_field_value.py +94 -0
- regscale/models/regscale_models/group.py +169 -0
- regscale/models/regscale_models/implementation_objective.py +335 -0
- regscale/models/regscale_models/implementation_option.py +275 -0
- regscale/models/regscale_models/implementation_role.py +33 -0
- regscale/models/regscale_models/incident.py +177 -0
- regscale/models/regscale_models/interconnection.py +43 -0
- regscale/models/regscale_models/issue.py +1176 -0
- regscale/models/regscale_models/leveraged_authorization.py +125 -0
- regscale/models/regscale_models/line_of_inquiry.py +52 -0
- regscale/models/regscale_models/link.py +205 -0
- regscale/models/regscale_models/meta_data.py +64 -0
- regscale/models/regscale_models/mixins/__init__.py +0 -0
- regscale/models/regscale_models/mixins/parent_cache.py +124 -0
- regscale/models/regscale_models/module.py +224 -0
- regscale/models/regscale_models/modules.py +191 -0
- regscale/models/regscale_models/objective.py +14 -0
- regscale/models/regscale_models/parameter.py +87 -0
- regscale/models/regscale_models/ports_protocol.py +81 -0
- regscale/models/regscale_models/privacy.py +89 -0
- regscale/models/regscale_models/profile.py +50 -0
- regscale/models/regscale_models/profile_link.py +68 -0
- regscale/models/regscale_models/profile_mapping.py +124 -0
- regscale/models/regscale_models/project.py +63 -0
- regscale/models/regscale_models/property.py +278 -0
- regscale/models/regscale_models/question.py +85 -0
- regscale/models/regscale_models/questionnaire.py +87 -0
- regscale/models/regscale_models/questionnaire_instance.py +177 -0
- regscale/models/regscale_models/rbac.py +132 -0
- regscale/models/regscale_models/reference.py +86 -0
- regscale/models/regscale_models/regscale_model.py +1643 -0
- regscale/models/regscale_models/requirement.py +29 -0
- regscale/models/regscale_models/risk.py +274 -0
- regscale/models/regscale_models/sbom.py +54 -0
- regscale/models/regscale_models/scan_history.py +436 -0
- regscale/models/regscale_models/search.py +53 -0
- regscale/models/regscale_models/security_control.py +132 -0
- regscale/models/regscale_models/security_plan.py +204 -0
- regscale/models/regscale_models/software_inventory.py +159 -0
- regscale/models/regscale_models/stake_holder.py +64 -0
- regscale/models/regscale_models/stig.py +647 -0
- regscale/models/regscale_models/supply_chain.py +152 -0
- regscale/models/regscale_models/system_role.py +188 -0
- regscale/models/regscale_models/system_role_external_assignment.py +40 -0
- regscale/models/regscale_models/tag.py +37 -0
- regscale/models/regscale_models/tag_mapping.py +19 -0
- regscale/models/regscale_models/task.py +133 -0
- regscale/models/regscale_models/threat.py +196 -0
- regscale/models/regscale_models/user.py +175 -0
- regscale/models/regscale_models/user_group.py +55 -0
- regscale/models/regscale_models/vulnerability.py +242 -0
- regscale/models/regscale_models/vulnerability_mapping.py +162 -0
- regscale/models/regscale_models/workflow.py +55 -0
- regscale/models/regscale_models/workflow_action.py +34 -0
- regscale/models/regscale_models/workflow_instance.py +269 -0
- regscale/models/regscale_models/workflow_instance_step.py +114 -0
- regscale/models/regscale_models/workflow_template.py +58 -0
- regscale/models/regscale_models/workflow_template_step.py +45 -0
- regscale/regscale.py +815 -0
- regscale/utils/__init__.py +7 -0
- regscale/utils/b64conversion.py +14 -0
- regscale/utils/click_utils.py +118 -0
- regscale/utils/decorators.py +48 -0
- regscale/utils/dict_utils.py +59 -0
- regscale/utils/files.py +79 -0
- regscale/utils/fxns.py +30 -0
- regscale/utils/graphql_client.py +113 -0
- regscale/utils/lists.py +16 -0
- regscale/utils/numbers.py +12 -0
- regscale/utils/shell.py +148 -0
- regscale/utils/string.py +121 -0
- regscale/utils/synqly_utils.py +165 -0
- regscale/utils/threading/__init__.py +8 -0
- regscale/utils/threading/threadhandler.py +131 -0
- regscale/utils/threading/threadsafe_counter.py +47 -0
- regscale/utils/threading/threadsafe_dict.py +242 -0
- regscale/utils/threading/threadsafe_list.py +83 -0
- regscale/utils/version.py +104 -0
- regscale/validation/__init__.py +0 -0
- regscale/validation/address.py +37 -0
- regscale/validation/record.py +48 -0
- regscale/visualization/__init__.py +5 -0
- regscale/visualization/click.py +34 -0
- regscale_cli-6.16.0.0.dist-info/LICENSE +21 -0
- regscale_cli-6.16.0.0.dist-info/METADATA +659 -0
- regscale_cli-6.16.0.0.dist-info/RECORD +481 -0
- regscale_cli-6.16.0.0.dist-info/WHEEL +5 -0
- regscale_cli-6.16.0.0.dist-info/entry_points.txt +6 -0
- regscale_cli-6.16.0.0.dist-info/top_level.txt +2 -0
- tests/fixtures/__init__.py +2 -0
- tests/fixtures/api.py +87 -0
- tests/fixtures/models.py +91 -0
- tests/fixtures/test_fixture.py +144 -0
- tests/mocks/__init__.py +0 -0
- tests/mocks/objects.py +3 -0
- tests/mocks/response.py +32 -0
- tests/mocks/xml.py +13 -0
- tests/regscale/__init__.py +0 -0
- tests/regscale/core/__init__.py +0 -0
- tests/regscale/core/test_api.py +232 -0
- tests/regscale/core/test_app.py +406 -0
- tests/regscale/core/test_login.py +37 -0
- tests/regscale/core/test_logz.py +66 -0
- tests/regscale/core/test_sbom_generator.py +87 -0
- tests/regscale/core/test_validation_utils.py +163 -0
- tests/regscale/core/test_version.py +78 -0
- tests/regscale/models/__init__.py +0 -0
- tests/regscale/models/test_asset.py +71 -0
- tests/regscale/models/test_config.py +26 -0
- tests/regscale/models/test_control_implementation.py +27 -0
- tests/regscale/models/test_import.py +97 -0
- tests/regscale/models/test_issue.py +36 -0
- tests/regscale/models/test_mapping.py +52 -0
- tests/regscale/models/test_platform.py +31 -0
- tests/regscale/models/test_regscale_model.py +346 -0
- tests/regscale/models/test_report.py +32 -0
- tests/regscale/models/test_tenable_integrations.py +118 -0
- tests/regscale/models/test_user_model.py +121 -0
- tests/regscale/test_about.py +19 -0
- tests/regscale/test_authorization.py +65 -0
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Jira integration for RegScale CLI"""
|
|
4
|
+
|
|
5
|
+
# Standard python imports
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from io import BytesIO
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Optional, Tuple, Union
|
|
13
|
+
from urllib.parse import urljoin
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from regscale.core.app.application import Application
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
from jira import JIRA
|
|
20
|
+
from jira import Issue as jiraIssue
|
|
21
|
+
from jira import JIRAError
|
|
22
|
+
from rich.progress import Progress
|
|
23
|
+
|
|
24
|
+
from regscale.core.app.api import Api
|
|
25
|
+
from regscale.core.app.logz import create_logger
|
|
26
|
+
from regscale.core.app.utils.app_utils import (
|
|
27
|
+
check_file_path,
|
|
28
|
+
check_license,
|
|
29
|
+
compute_hashes_in_directory,
|
|
30
|
+
convert_datetime_to_regscale_string,
|
|
31
|
+
create_progress_object,
|
|
32
|
+
error_and_exit,
|
|
33
|
+
get_current_datetime,
|
|
34
|
+
save_data_to,
|
|
35
|
+
)
|
|
36
|
+
from regscale.core.app.utils.regscale_utils import verify_provided_module
|
|
37
|
+
from regscale.models import regscale_id, regscale_module
|
|
38
|
+
from regscale.models.regscale_models.file import File
|
|
39
|
+
from regscale.models.regscale_models.issue import Issue
|
|
40
|
+
from regscale.models.regscale_models.task import Task
|
|
41
|
+
from regscale.utils.threading.threadhandler import create_threads, thread_assignment
|
|
42
|
+
|
|
43
|
+
job_progress = create_progress_object()
|
|
44
|
+
logger = create_logger()
|
|
45
|
+
update_issues = []
|
|
46
|
+
new_regscale_issues = []
|
|
47
|
+
updated_regscale_issues = []
|
|
48
|
+
update_counter = []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
####################################################################################################
|
|
52
|
+
#
|
|
53
|
+
# PROCESS ISSUES TO JIRA
|
|
54
|
+
# JIRA CLI Python Docs: https://jira.readthedocs.io/examples.html#issues
|
|
55
|
+
# JIRA API Docs: https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/
|
|
56
|
+
#
|
|
57
|
+
####################################################################################################
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Create group to handle Jira integration
|
|
61
|
+
@click.group()
|
|
62
|
+
def jira():
|
|
63
|
+
"""Sync issues between Jira and RegScale."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@jira.command()
|
|
67
|
+
@regscale_id()
|
|
68
|
+
@regscale_module()
|
|
69
|
+
@click.option(
|
|
70
|
+
"--jira_project",
|
|
71
|
+
type=click.STRING,
|
|
72
|
+
help="RegScale will sync the issues for the record to the Jira project.",
|
|
73
|
+
prompt="Enter the name of the project in Jira",
|
|
74
|
+
required=True,
|
|
75
|
+
)
|
|
76
|
+
@click.option(
|
|
77
|
+
"--jira_issue_type",
|
|
78
|
+
type=click.STRING,
|
|
79
|
+
help="Enter the Jira issue type to use when creating new issues from RegScale. (CASE SENSITIVE)",
|
|
80
|
+
prompt="Enter the Jira issue type",
|
|
81
|
+
required=True,
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--sync_attachments",
|
|
85
|
+
type=click.BOOL,
|
|
86
|
+
help=(
|
|
87
|
+
"Whether RegScale will sync the attachments for the issue "
|
|
88
|
+
"in the provided Jira project and vice versa. Defaults to True."
|
|
89
|
+
),
|
|
90
|
+
required=False,
|
|
91
|
+
default=True,
|
|
92
|
+
)
|
|
93
|
+
@click.option(
|
|
94
|
+
"--token_auth",
|
|
95
|
+
"-t",
|
|
96
|
+
is_flag=True,
|
|
97
|
+
help="Use token authentication for Jira API instead of basic auth, defaults to False.",
|
|
98
|
+
)
|
|
99
|
+
def issues(
|
|
100
|
+
regscale_id: int,
|
|
101
|
+
regscale_module: str,
|
|
102
|
+
jira_project: str,
|
|
103
|
+
jira_issue_type: str,
|
|
104
|
+
sync_attachments: bool = True,
|
|
105
|
+
token_auth: bool = False,
|
|
106
|
+
):
|
|
107
|
+
"""Sync issues from Jira into RegScale."""
|
|
108
|
+
sync_regscale_and_jira(
|
|
109
|
+
parent_id=regscale_id,
|
|
110
|
+
parent_module=regscale_module,
|
|
111
|
+
jira_project=jira_project,
|
|
112
|
+
jira_issue_type=jira_issue_type,
|
|
113
|
+
sync_attachments=sync_attachments,
|
|
114
|
+
token_auth=token_auth,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@jira.command()
|
|
119
|
+
@regscale_id()
|
|
120
|
+
@regscale_module()
|
|
121
|
+
@click.option(
|
|
122
|
+
"--jira_project",
|
|
123
|
+
type=click.STRING,
|
|
124
|
+
help="RegScale will sync the issues for the record to the Jira project.",
|
|
125
|
+
prompt="Enter the name of the project in Jira",
|
|
126
|
+
required=True,
|
|
127
|
+
)
|
|
128
|
+
@click.option(
|
|
129
|
+
"--token_auth",
|
|
130
|
+
"-t",
|
|
131
|
+
is_flag=True,
|
|
132
|
+
help="Use token authentication for Jira API instead of basic auth, defaults to False.",
|
|
133
|
+
)
|
|
134
|
+
def tasks(
|
|
135
|
+
regscale_id: int,
|
|
136
|
+
regscale_module: str,
|
|
137
|
+
jira_project: str,
|
|
138
|
+
token_auth: bool = False,
|
|
139
|
+
):
|
|
140
|
+
"""Sync tasks from Jira into RegScale."""
|
|
141
|
+
sync_regscale_and_jira(
|
|
142
|
+
parent_id=regscale_id,
|
|
143
|
+
parent_module=regscale_module,
|
|
144
|
+
jira_project=jira_project,
|
|
145
|
+
jira_issue_type="Task",
|
|
146
|
+
sync_attachments=False,
|
|
147
|
+
sync_tasks_only=True,
|
|
148
|
+
token_auth=token_auth,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def sync_regscale_and_jira(
|
|
153
|
+
parent_id: int,
|
|
154
|
+
parent_module: str,
|
|
155
|
+
jira_project: str,
|
|
156
|
+
jira_issue_type: str,
|
|
157
|
+
sync_attachments: bool = True,
|
|
158
|
+
sync_tasks_only: bool = False,
|
|
159
|
+
token_auth: bool = False,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Sync issues, bidirectionally, from Jira into RegScale as issues
|
|
163
|
+
|
|
164
|
+
:param int parent_id: ID # from RegScale to associate issues with
|
|
165
|
+
:param str parent_module: RegScale module to associate issues with
|
|
166
|
+
:param str jira_project: Name of the project in Jira
|
|
167
|
+
:param str jira_issue_type: Type of issues to sync from Jira
|
|
168
|
+
:param bool sync_attachments: Whether to sync attachments in RegScale & Jira, defaults to True
|
|
169
|
+
:param bool sync_tasks_only: Whether to sync only tasks from Jira, defaults to False
|
|
170
|
+
:param bool token_auth: Use token authentication for Jira API, defaults to False
|
|
171
|
+
:rtype: None
|
|
172
|
+
"""
|
|
173
|
+
app = check_license()
|
|
174
|
+
api = Api()
|
|
175
|
+
config = app.config
|
|
176
|
+
|
|
177
|
+
# see if provided RegScale Module is an accepted option
|
|
178
|
+
verify_provided_module(parent_module)
|
|
179
|
+
|
|
180
|
+
# create Jira client
|
|
181
|
+
jira_client = create_jira_client(config, token_auth)
|
|
182
|
+
|
|
183
|
+
if sync_tasks_only:
|
|
184
|
+
jql_str = f"project = {jira_project} AND issueType = {jira_issue_type}"
|
|
185
|
+
regscale_issues = Task.get_all_by_parent(parent_id, parent_module)
|
|
186
|
+
regscale_attachments = []
|
|
187
|
+
else:
|
|
188
|
+
jql_str = f"project = {jira_project}"
|
|
189
|
+
(
|
|
190
|
+
regscale_issues,
|
|
191
|
+
regscale_attachments,
|
|
192
|
+
) = Issue.fetch_issues_and_attachments_by_parent(
|
|
193
|
+
parent_id=parent_id,
|
|
194
|
+
parent_module=parent_module,
|
|
195
|
+
fetch_attachments=sync_attachments,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
jira_objects = fetch_jira_objects(
|
|
199
|
+
jira_client=jira_client,
|
|
200
|
+
jira_project=jira_project,
|
|
201
|
+
jql_str=jql_str,
|
|
202
|
+
jira_issue_type=jira_issue_type,
|
|
203
|
+
sync_tasks_only=sync_tasks_only,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if regscale_issues and not sync_tasks_only:
|
|
207
|
+
# sync RegScale issues to Jira
|
|
208
|
+
if issues_to_update := sync_regscale_to_jira(
|
|
209
|
+
regscale_issues=regscale_issues,
|
|
210
|
+
jira_client=jira_client,
|
|
211
|
+
jira_project=jira_project,
|
|
212
|
+
jira_issue_type=jira_issue_type,
|
|
213
|
+
api=api,
|
|
214
|
+
sync_attachments=sync_attachments,
|
|
215
|
+
attachments=regscale_attachments,
|
|
216
|
+
):
|
|
217
|
+
with job_progress:
|
|
218
|
+
# create task to update RegScale issues
|
|
219
|
+
updating_issues = job_progress.add_task(
|
|
220
|
+
f"[#f8b737]Updating {len(issues_to_update)} RegScale {jira_issue_type.lower()}(s) from Jira...",
|
|
221
|
+
total=len(issues_to_update),
|
|
222
|
+
)
|
|
223
|
+
# create threads to analyze Jira issues and RegScale issues
|
|
224
|
+
create_threads(
|
|
225
|
+
process=update_regscale_issues,
|
|
226
|
+
args=(
|
|
227
|
+
issues_to_update,
|
|
228
|
+
api,
|
|
229
|
+
updating_issues,
|
|
230
|
+
),
|
|
231
|
+
thread_count=len(issues_to_update),
|
|
232
|
+
)
|
|
233
|
+
# output the final result
|
|
234
|
+
logger.info(
|
|
235
|
+
"%i/%i issue(s) updated in RegScale.",
|
|
236
|
+
len(issues_to_update),
|
|
237
|
+
len(update_counter),
|
|
238
|
+
)
|
|
239
|
+
elif not sync_tasks_only:
|
|
240
|
+
logger.info("No issues need to be updated in RegScale.")
|
|
241
|
+
|
|
242
|
+
if jira_objects:
|
|
243
|
+
sync_regscale_objects_to_jira(
|
|
244
|
+
jira_objects, regscale_issues, sync_attachments, app, parent_id, parent_module, sync_tasks_only
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
logger.info(f"No {'tasks' if sync_tasks_only else 'issues'} need to be analyzed from Jira.")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def sync_regscale_objects_to_jira(
|
|
251
|
+
jira_issues: list[jiraIssue],
|
|
252
|
+
regscale_objects: list[Union[Issue, Task]],
|
|
253
|
+
sync_attachments: bool,
|
|
254
|
+
app: "Application",
|
|
255
|
+
parent_id: int,
|
|
256
|
+
parent_module: str,
|
|
257
|
+
sync_tasks_only: bool,
|
|
258
|
+
):
|
|
259
|
+
"""
|
|
260
|
+
Sync issues from Jira to RegScale
|
|
261
|
+
|
|
262
|
+
:param list[jiraIssue] jira_issues: List of Jira issues to sync to RegScale
|
|
263
|
+
:param list[Union[Issue, Task]] regscale_objects: List of RegScale issues or tasks to compare to Jira issues
|
|
264
|
+
:param bool sync_attachments: Sync attachments from Jira to RegScale, defaults to True
|
|
265
|
+
:param Application app: RegScale CLI application object
|
|
266
|
+
:param int parent_id: Parent record ID in RegScale
|
|
267
|
+
:param str parent_module: Parent record module in RegScale
|
|
268
|
+
:param bool sync_tasks_only: Whether to sync only tasks from Jira
|
|
269
|
+
"""
|
|
270
|
+
issues_closed = []
|
|
271
|
+
with job_progress:
|
|
272
|
+
type_str = "task" if sync_tasks_only else "issue"
|
|
273
|
+
creating_issues = job_progress.add_task(
|
|
274
|
+
f"[#f8b737]Comparing {len(jira_issues)} Jira {type_str}(s)"
|
|
275
|
+
f" and {len(regscale_objects)} RegScale {type_str}(s)...",
|
|
276
|
+
total=len(jira_issues),
|
|
277
|
+
)
|
|
278
|
+
jira_client = create_jira_client(app.config)
|
|
279
|
+
if sync_tasks_only:
|
|
280
|
+
tasks_inserted, tasks_updated, tasks_closed = create_and_update_regscale_tasks(
|
|
281
|
+
jira_issues=jira_issues,
|
|
282
|
+
existing_tasks=regscale_objects,
|
|
283
|
+
parent_id=parent_id,
|
|
284
|
+
parent_module=parent_module,
|
|
285
|
+
progress=job_progress,
|
|
286
|
+
progress_task=creating_issues,
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
create_threads(
|
|
290
|
+
process=create_and_update_regscale_issues,
|
|
291
|
+
args=(
|
|
292
|
+
jira_issues,
|
|
293
|
+
regscale_objects,
|
|
294
|
+
sync_attachments,
|
|
295
|
+
jira_client,
|
|
296
|
+
app,
|
|
297
|
+
parent_id,
|
|
298
|
+
parent_module,
|
|
299
|
+
creating_issues,
|
|
300
|
+
job_progress,
|
|
301
|
+
),
|
|
302
|
+
thread_count=len(jira_issues),
|
|
303
|
+
)
|
|
304
|
+
logger.info(
|
|
305
|
+
"Analyzed %i Jira %s(s), created %i %s(s), updated %i %s(s), and closed %i %s(s) in RegScale.",
|
|
306
|
+
len(jira_issues),
|
|
307
|
+
type_str,
|
|
308
|
+
len(new_regscale_issues) if not sync_tasks_only else tasks_inserted,
|
|
309
|
+
type_str,
|
|
310
|
+
len(updated_regscale_issues) if not sync_tasks_only else tasks_updated,
|
|
311
|
+
type_str,
|
|
312
|
+
len(issues_closed) if not sync_tasks_only else tasks_closed,
|
|
313
|
+
type_str,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def create_jira_client(
|
|
318
|
+
config: dict,
|
|
319
|
+
token_auth: bool = False,
|
|
320
|
+
) -> JIRA:
|
|
321
|
+
"""
|
|
322
|
+
Create a Jira client to use for interacting with Jira
|
|
323
|
+
|
|
324
|
+
:param dict config: RegScale CLI application config
|
|
325
|
+
:param bool token_auth: Use token authentication for Jira API, defaults to False
|
|
326
|
+
:return: JIRA Client
|
|
327
|
+
:rtype: JIRA
|
|
328
|
+
"""
|
|
329
|
+
from regscale.integrations.variables import ScannerVariables
|
|
330
|
+
|
|
331
|
+
url = config["jiraUrl"]
|
|
332
|
+
token = config["jiraApiToken"]
|
|
333
|
+
jira_user = config["jiraUserName"]
|
|
334
|
+
if token_auth:
|
|
335
|
+
return JIRA(token_auth=token, options={"server": url, "verify": ScannerVariables.sslVerify})
|
|
336
|
+
|
|
337
|
+
# set the JIRA Url
|
|
338
|
+
return JIRA(basic_auth=(jira_user, token), options={"server": url})
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def update_regscale_issues(args: Tuple, thread: int) -> None:
|
|
342
|
+
"""
|
|
343
|
+
Function to compare Jira issues and RegScale issues
|
|
344
|
+
|
|
345
|
+
:param Tuple args: Tuple of args to use during the process
|
|
346
|
+
:param int thread: Thread number of current thread
|
|
347
|
+
:rtype: None
|
|
348
|
+
"""
|
|
349
|
+
# set up local variables from the passed args
|
|
350
|
+
(
|
|
351
|
+
regscale_issues,
|
|
352
|
+
app,
|
|
353
|
+
task,
|
|
354
|
+
) = args
|
|
355
|
+
# find which records should be executed by the current thread
|
|
356
|
+
threads = thread_assignment(thread=thread, total_items=len(regscale_issues))
|
|
357
|
+
# iterate through the thread assignment items and process them
|
|
358
|
+
for i in range(len(threads)):
|
|
359
|
+
# set the issue for the thread for later use in the function
|
|
360
|
+
issue = regscale_issues[threads[i]]
|
|
361
|
+
# update the issue in RegScale
|
|
362
|
+
issue.save()
|
|
363
|
+
logger.debug(
|
|
364
|
+
"RegScale Issue %i was updated with the Jira link.",
|
|
365
|
+
issue.id,
|
|
366
|
+
)
|
|
367
|
+
update_counter.append(issue)
|
|
368
|
+
# update progress bar
|
|
369
|
+
job_progress.update(task, advance=1)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def convert_task_status(name: str) -> str:
|
|
373
|
+
"""
|
|
374
|
+
Convert the task status from Jira to RegScale
|
|
375
|
+
|
|
376
|
+
:param str name: Name of the task status in Jira
|
|
377
|
+
:return: Name of the task status in RegScale
|
|
378
|
+
:rtype: str
|
|
379
|
+
"""
|
|
380
|
+
jira_regscale_map = {
|
|
381
|
+
"to do": "Backlog",
|
|
382
|
+
"in progress": "Open",
|
|
383
|
+
"done": "Closed",
|
|
384
|
+
"closed": "Closed",
|
|
385
|
+
"cancelled": "Cancelled",
|
|
386
|
+
"canceled": "Cancelled",
|
|
387
|
+
}
|
|
388
|
+
return jira_regscale_map.get(name.lower(), "Open")
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def create_regscale_task_from_jira(config: dict, jira_issue: jiraIssue, parent_id: int, parent_module: str) -> Task:
|
|
392
|
+
"""
|
|
393
|
+
Function to create a Task object from a Jira issue
|
|
394
|
+
|
|
395
|
+
:param dict config: Application config
|
|
396
|
+
:param jiraIssue jira_issue: Jira issue to create a Task object from
|
|
397
|
+
:param int parent_id: Parent record ID in RegScale
|
|
398
|
+
:param str parent_module: Parent record module in RegScale
|
|
399
|
+
:return: RegScale Task object
|
|
400
|
+
:rtype: Task
|
|
401
|
+
"""
|
|
402
|
+
description = jira_issue.fields.description
|
|
403
|
+
due_date = jira_issue.fields.duedate
|
|
404
|
+
status = convert_task_status(jira_issue.fields.status.name)
|
|
405
|
+
status_change_date = convert_datetime_to_regscale_string(
|
|
406
|
+
datetime.strptime(jira_issue.fields.statuscategorychangedate, "%Y-%m-%dT%H:%M:%S.%f%z")
|
|
407
|
+
)
|
|
408
|
+
title = jira_issue.fields.summary
|
|
409
|
+
date_closed = None
|
|
410
|
+
percent_complete = None
|
|
411
|
+
if not due_date:
|
|
412
|
+
delta = config["issues"]["jira"]["medium"]
|
|
413
|
+
due_date = convert_datetime_to_regscale_string(datetime.now() + timedelta(days=delta))
|
|
414
|
+
if status == "Closed":
|
|
415
|
+
date_closed = status_change_date
|
|
416
|
+
percent_complete = 100
|
|
417
|
+
|
|
418
|
+
return Task(
|
|
419
|
+
title=title,
|
|
420
|
+
status=status,
|
|
421
|
+
description=description,
|
|
422
|
+
dueDate=due_date,
|
|
423
|
+
parentId=parent_id,
|
|
424
|
+
parentModule=parent_module,
|
|
425
|
+
dateClosed=date_closed,
|
|
426
|
+
percentComplete=percent_complete,
|
|
427
|
+
otherIdentifier=jira_issue.key,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def check_and_close_tasks(existing_tasks: list[Task], all_jira_titles: set[str]) -> list[Task]:
|
|
432
|
+
"""
|
|
433
|
+
Function to check and close tasks that are not in Jira
|
|
434
|
+
|
|
435
|
+
:param list[Task] existing_tasks: List of existing tasks in RegScale
|
|
436
|
+
:param set[str] all_jira_titles: Set of all Jira task titles
|
|
437
|
+
:return: List of tasks to close
|
|
438
|
+
:rtype: list[Task]
|
|
439
|
+
"""
|
|
440
|
+
close_tasks = []
|
|
441
|
+
for task in existing_tasks:
|
|
442
|
+
if task.title not in all_jira_titles and task.status != "Closed":
|
|
443
|
+
task.status = "Closed"
|
|
444
|
+
task.percentComplete = 100
|
|
445
|
+
task.dateClosed = get_current_datetime()
|
|
446
|
+
close_tasks.append(task)
|
|
447
|
+
return close_tasks
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def create_and_update_regscale_tasks(
|
|
451
|
+
jira_issues: list[jiraIssue],
|
|
452
|
+
existing_tasks: list[Task],
|
|
453
|
+
parent_id: int,
|
|
454
|
+
parent_module: str,
|
|
455
|
+
progress: Progress,
|
|
456
|
+
progress_task: Any,
|
|
457
|
+
) -> tuple[int, int, int]:
|
|
458
|
+
"""
|
|
459
|
+
Function to create or update Tasks in RegScale from Jira
|
|
460
|
+
|
|
461
|
+
:param list[jiraIssue] jira_issues: List of Jira issues to create or update in RegScale
|
|
462
|
+
:param list[Task] existing_tasks: List of existing tasks in RegScale
|
|
463
|
+
:param int parent_id: Parent record ID in RegScale
|
|
464
|
+
:param str parent_module: Parent record module in RegScale
|
|
465
|
+
:param Progress progress: Job progress object to use for updating the progress bar
|
|
466
|
+
:param Any progress_task: Task object to update the progress bar
|
|
467
|
+
:return: A tuple of counts
|
|
468
|
+
:rtype: tuple[int, int, int]
|
|
469
|
+
"""
|
|
470
|
+
from regscale.core.app.application import Application
|
|
471
|
+
|
|
472
|
+
app = Application()
|
|
473
|
+
config = app.config
|
|
474
|
+
insert_tasks = []
|
|
475
|
+
update_tasks = []
|
|
476
|
+
all_jira_titles = {jira_issue.fields.summary for jira_issue in jira_issues}
|
|
477
|
+
for jira_issue in jira_issues:
|
|
478
|
+
task = create_regscale_task_from_jira(config, jira_issue, parent_id, parent_module)
|
|
479
|
+
if task not in existing_tasks:
|
|
480
|
+
# set due date to today if not provided
|
|
481
|
+
insert_tasks.append(task)
|
|
482
|
+
else:
|
|
483
|
+
existing_task = next((t for t in existing_tasks if t == task), None)
|
|
484
|
+
task.id = existing_task.id
|
|
485
|
+
update_tasks.append(task)
|
|
486
|
+
progress.update(progress_task, advance=1)
|
|
487
|
+
close_tasks = check_and_close_tasks(existing_tasks, all_jira_titles)
|
|
488
|
+
|
|
489
|
+
with progress:
|
|
490
|
+
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
491
|
+
if insert_tasks:
|
|
492
|
+
creating_tasks = progress.add_task(
|
|
493
|
+
f"[#f8b737]Creating {len(insert_tasks)} task(s) in RegScale...",
|
|
494
|
+
total=len(insert_tasks),
|
|
495
|
+
)
|
|
496
|
+
create_futures = {executor.submit(task.create) for task in insert_tasks}
|
|
497
|
+
for _ in as_completed(create_futures):
|
|
498
|
+
progress.update(creating_tasks, advance=1)
|
|
499
|
+
if update_tasks:
|
|
500
|
+
update_task = progress.add_task(
|
|
501
|
+
f"[#f8b737]Updating {len(update_tasks)} task(s) in RegScale...",
|
|
502
|
+
total=len(update_tasks),
|
|
503
|
+
)
|
|
504
|
+
update_futures = {executor.submit(task.save) for task in update_tasks}
|
|
505
|
+
for _ in as_completed(update_futures):
|
|
506
|
+
progress.update(update_task, advance=1)
|
|
507
|
+
if close_tasks:
|
|
508
|
+
closing_tasks = progress.add_task(
|
|
509
|
+
f"[#f8b737]Closing {len(close_tasks)} task(s) in RegScale...",
|
|
510
|
+
total=len(close_tasks),
|
|
511
|
+
)
|
|
512
|
+
close_futures = {executor.submit(task.save) for task in close_tasks}
|
|
513
|
+
for _ in as_completed(close_futures):
|
|
514
|
+
progress.update(closing_tasks, advance=1)
|
|
515
|
+
return len(insert_tasks), len(update_tasks), len(close_tasks)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def create_and_update_regscale_issues(args: Tuple, thread: int) -> None:
|
|
519
|
+
"""
|
|
520
|
+
Function to create or update issues in RegScale from Jira
|
|
521
|
+
|
|
522
|
+
:param Tuple args: Tuple of args to use during the process
|
|
523
|
+
:param int thread: Thread number of current thread
|
|
524
|
+
:rtype: None
|
|
525
|
+
"""
|
|
526
|
+
# set up local variables from the passed args
|
|
527
|
+
(jira_issues, regscale_issues, add_attachments, jira_client, app, parent_id, parent_module, task, progress) = args
|
|
528
|
+
# find which records should be executed by the current thread
|
|
529
|
+
threads = thread_assignment(thread=thread, total_items=len(jira_issues))
|
|
530
|
+
|
|
531
|
+
# iterate through the thread assignment items and process them
|
|
532
|
+
for i in range(len(threads)):
|
|
533
|
+
jira_issue: jiraIssue = jira_issues[threads[i]]
|
|
534
|
+
regscale_issue: Optional[Issue] = next(
|
|
535
|
+
(issue for issue in regscale_issues if issue.jiraId == jira_issue.key), None
|
|
536
|
+
)
|
|
537
|
+
# see if the Jira issue needs to be created in RegScale
|
|
538
|
+
if jira_issue.fields.status.name.lower() == "done" and regscale_issue:
|
|
539
|
+
# update the status and date completed of the RegScale issue
|
|
540
|
+
regscale_issue.status = "Closed"
|
|
541
|
+
regscale_issue.dateCompleted = get_current_datetime()
|
|
542
|
+
# update the issue in RegScale
|
|
543
|
+
updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
|
|
544
|
+
elif regscale_issue:
|
|
545
|
+
# update the issue in RegScale
|
|
546
|
+
updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
|
|
547
|
+
else:
|
|
548
|
+
# map the jira issue to a RegScale issue object
|
|
549
|
+
issue = map_jira_to_regscale_issue(
|
|
550
|
+
jira_issue=jira_issue,
|
|
551
|
+
config=app.config,
|
|
552
|
+
parent_id=parent_id,
|
|
553
|
+
parent_module=parent_module,
|
|
554
|
+
)
|
|
555
|
+
# create the issue in RegScale
|
|
556
|
+
if regscale_issue := Issue.insert_issue(
|
|
557
|
+
app=app,
|
|
558
|
+
issue=issue,
|
|
559
|
+
):
|
|
560
|
+
logger.debug(
|
|
561
|
+
"Created issue #%i-%s in RegScale.",
|
|
562
|
+
regscale_issue.id,
|
|
563
|
+
regscale_issue.title,
|
|
564
|
+
)
|
|
565
|
+
new_regscale_issues.append(regscale_issue)
|
|
566
|
+
else:
|
|
567
|
+
logger.warning("Unable to create issue in RegScale.\nIssue: %s", issue.dict())
|
|
568
|
+
if add_attachments and regscale_issue and jira_issue.fields.attachment:
|
|
569
|
+
# determine which attachments need to be uploaded to prevent duplicates by
|
|
570
|
+
# getting the hashes of all Jira & RegScale attachments
|
|
571
|
+
compare_files_for_dupes_and_upload(
|
|
572
|
+
jira_issue=jira_issue,
|
|
573
|
+
regscale_issue=regscale_issue,
|
|
574
|
+
jira_client=jira_client,
|
|
575
|
+
api=Api(),
|
|
576
|
+
)
|
|
577
|
+
# update progress bar
|
|
578
|
+
progress.update(task, advance=1)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def sync_regscale_to_jira(
|
|
582
|
+
regscale_issues: list[Issue],
|
|
583
|
+
jira_client: JIRA,
|
|
584
|
+
jira_project: str,
|
|
585
|
+
jira_issue_type: str,
|
|
586
|
+
sync_attachments: bool = True,
|
|
587
|
+
attachments: Optional[dict] = None,
|
|
588
|
+
api: Optional[Api] = None,
|
|
589
|
+
) -> list[Issue]:
|
|
590
|
+
"""
|
|
591
|
+
Sync issues from RegScale to Jira
|
|
592
|
+
|
|
593
|
+
:param list[Issue] regscale_issues: list of RegScale issues to sync to Jira
|
|
594
|
+
:param JIRA jira_client: Jira client to use for issue creation in Jira
|
|
595
|
+
:param str jira_project: Jira Project to create the issues in
|
|
596
|
+
:param str jira_issue_type: Type of issue to create in Jira
|
|
597
|
+
:param bool sync_attachments: Sync attachments from RegScale to Jira, defaults to True
|
|
598
|
+
:param Optional[dict] attachments: Dict of attachments to sync from RegScale to Jira, defaults to None
|
|
599
|
+
:param Optional[Api] api: API object to download attachments, defaults to None
|
|
600
|
+
:return: list of RegScale issues that need to be updated
|
|
601
|
+
:rtype: list[Issue]
|
|
602
|
+
"""
|
|
603
|
+
new_issue_counter = 0
|
|
604
|
+
issuess_to_update = []
|
|
605
|
+
with job_progress:
|
|
606
|
+
# create task to create Jira issues
|
|
607
|
+
creating_issues = job_progress.add_task(
|
|
608
|
+
f"[#f8b737]Verifying {len(regscale_issues)} RegScale issue(s) exist in Jira...",
|
|
609
|
+
total=len(regscale_issues),
|
|
610
|
+
)
|
|
611
|
+
for issue in regscale_issues:
|
|
612
|
+
# see if Jira issue already exists
|
|
613
|
+
if not issue.jiraId or issue.jiraId == "":
|
|
614
|
+
new_issue = create_issue_in_jira(
|
|
615
|
+
issue=issue,
|
|
616
|
+
jira_client=jira_client,
|
|
617
|
+
jira_project=jira_project,
|
|
618
|
+
issue_type=jira_issue_type,
|
|
619
|
+
add_attachments=sync_attachments,
|
|
620
|
+
attachments=attachments,
|
|
621
|
+
api=api,
|
|
622
|
+
)
|
|
623
|
+
# log progress
|
|
624
|
+
new_issue_counter += 1
|
|
625
|
+
# get the Jira ID
|
|
626
|
+
jira_id = new_issue.key
|
|
627
|
+
# update the RegScale issue for the Jira link
|
|
628
|
+
issue.jiraId = jira_id
|
|
629
|
+
# add the issue to the update_issues global list
|
|
630
|
+
issuess_to_update.append(issue)
|
|
631
|
+
job_progress.update(creating_issues, advance=1)
|
|
632
|
+
# output the final result
|
|
633
|
+
logger.info("%i new issue(s) opened in Jira.", new_issue_counter)
|
|
634
|
+
return issuess_to_update
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def fetch_jira_objects(
|
|
638
|
+
jira_client: JIRA, jira_project: str, jira_issue_type: str, jql_str: str = None, sync_tasks_only: bool = False
|
|
639
|
+
) -> list[jiraIssue]:
|
|
640
|
+
"""
|
|
641
|
+
Fetch all issues from Jira for the provided project
|
|
642
|
+
|
|
643
|
+
:param JIRA jira_client: Jira client to use for the request
|
|
644
|
+
:param str jira_project: Name of the project in Jira
|
|
645
|
+
:param str jira_issue_type: Type of issue to fetch from Jira
|
|
646
|
+
:param str jql_str: JQL string to use for the request, default None
|
|
647
|
+
:param bool sync_tasks_only: Whether to sync only tasks from Jira, defaults to False
|
|
648
|
+
:return: List of Jira issues
|
|
649
|
+
:rtype: list[jiraIssue]
|
|
650
|
+
"""
|
|
651
|
+
start_pointer = 0
|
|
652
|
+
page_size = 100
|
|
653
|
+
jira_objects = []
|
|
654
|
+
if sync_tasks_only:
|
|
655
|
+
validate_issue_type(jira_client, jira_issue_type)
|
|
656
|
+
output_str = "task"
|
|
657
|
+
else:
|
|
658
|
+
output_str = "issue"
|
|
659
|
+
logger.info("Fetching %s(s) from Jira...", output_str.lower())
|
|
660
|
+
# get all issues for the Jira project
|
|
661
|
+
while True:
|
|
662
|
+
start = start_pointer * page_size
|
|
663
|
+
jira_issues_response = jira_client.search_issues(
|
|
664
|
+
jql_str=jql_str,
|
|
665
|
+
startAt=start,
|
|
666
|
+
maxResults=page_size,
|
|
667
|
+
)
|
|
668
|
+
if len(jira_objects) == jira_issues_response.total:
|
|
669
|
+
break
|
|
670
|
+
start_pointer += 1
|
|
671
|
+
# append new records to jira_issues
|
|
672
|
+
jira_objects.extend(jira_issues_response)
|
|
673
|
+
logger.info(
|
|
674
|
+
"%i/%i Jira %s(s) retrieved.",
|
|
675
|
+
len(jira_objects),
|
|
676
|
+
jira_issues_response.total,
|
|
677
|
+
output_str.lower(),
|
|
678
|
+
)
|
|
679
|
+
if jira_objects:
|
|
680
|
+
check_file_path("artifacts")
|
|
681
|
+
file_name = f"{jira_project.lower()}_existingJira{jira_issue_type}.json"
|
|
682
|
+
file_path = Path(f"./artifacts/{file_name}")
|
|
683
|
+
save_data_to(
|
|
684
|
+
file=file_path,
|
|
685
|
+
data=[issue.raw for issue in jira_objects],
|
|
686
|
+
output_log=False,
|
|
687
|
+
)
|
|
688
|
+
logger.info(
|
|
689
|
+
"Saved %i Jira %s(s), see %s",
|
|
690
|
+
len(jira_objects),
|
|
691
|
+
jira_issue_type.lower(),
|
|
692
|
+
str(file_path.absolute()),
|
|
693
|
+
)
|
|
694
|
+
logger.info("%i %s(s) retrieved from Jira.", len(jira_objects), output_str.lower())
|
|
695
|
+
return jira_objects
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def map_jira_to_regscale_issue(jira_issue: jiraIssue, config: dict, parent_id: int, parent_module: str) -> Issue:
|
|
699
|
+
"""
|
|
700
|
+
Map Jira issues to RegScale issues
|
|
701
|
+
|
|
702
|
+
:param jiraIssue jira_issue: Jira issue to map to issue in RegScale
|
|
703
|
+
:param dict config: Application config
|
|
704
|
+
:param int parent_id: Parent record ID in RegScale
|
|
705
|
+
:param str parent_module: Parent record module in RegScale
|
|
706
|
+
:return: Issue object of the newly created issue in RegScale
|
|
707
|
+
:rtype: Issue
|
|
708
|
+
"""
|
|
709
|
+
due_date = map_jira_due_date(jira_issue, config)
|
|
710
|
+
issue = Issue(
|
|
711
|
+
title=jira_issue.fields.summary,
|
|
712
|
+
severityLevel=Issue.assign_severity(jira_issue.fields.priority.name),
|
|
713
|
+
issueOwnerId=config["userId"],
|
|
714
|
+
dueDate=due_date,
|
|
715
|
+
description=(
|
|
716
|
+
f"Description {jira_issue.fields.description}"
|
|
717
|
+
f"\nStatus: {jira_issue.fields.status.name}"
|
|
718
|
+
f"\nDue Date: {due_date}"
|
|
719
|
+
),
|
|
720
|
+
status=("Closed" if jira_issue.fields.status.name.lower() == "done" else config["issues"]["jira"]["status"]),
|
|
721
|
+
jiraId=jira_issue.key,
|
|
722
|
+
parentId=parent_id,
|
|
723
|
+
parentModule=parent_module,
|
|
724
|
+
dateCreated=get_current_datetime(),
|
|
725
|
+
dateCompleted=(get_current_datetime() if jira_issue.fields.status.name.lower() == "done" else None),
|
|
726
|
+
)
|
|
727
|
+
return issue
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def map_jira_due_date(jira_issue: Optional[jiraIssue], config: dict) -> str:
|
|
731
|
+
"""
|
|
732
|
+
Parses the provided jira_issue for a due date and returns it as a string
|
|
733
|
+
|
|
734
|
+
:param Optional[jiraIssue] jira_issue: Jira issue to parse for a due date
|
|
735
|
+
:param dict config: Application config
|
|
736
|
+
:return: Due date as a string
|
|
737
|
+
:rtype: str
|
|
738
|
+
"""
|
|
739
|
+
if jira_issue.fields.duedate:
|
|
740
|
+
due_date = jira_issue.fields.duedate
|
|
741
|
+
elif jira_issue.fields.priority:
|
|
742
|
+
due_date = datetime.now() + timedelta(days=config["issues"]["jira"][jira_issue.fields.priority.name.lower()])
|
|
743
|
+
due_date = convert_datetime_to_regscale_string(due_date)
|
|
744
|
+
else:
|
|
745
|
+
due_date = datetime.now() + timedelta(days=config["issues"]["jira"]["medium"])
|
|
746
|
+
due_date = convert_datetime_to_regscale_string(due_date)
|
|
747
|
+
return due_date
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _generate_jira_comment(issue: Issue) -> str:
|
|
751
|
+
"""
|
|
752
|
+
Generate a Jira comment from a RegScale issue and it's populated fields
|
|
753
|
+
|
|
754
|
+
:param Issue issue: RegScale issue to generate a Jira comment from
|
|
755
|
+
:return: Jira comment
|
|
756
|
+
:rtype: str
|
|
757
|
+
"""
|
|
758
|
+
comment = ""
|
|
759
|
+
exclude_fields = ["createdById", "lastUpdatedById", "issueOwnerId", "uuid"] + issue._exclude_graphql_fields
|
|
760
|
+
for field_name, field_value in issue.__dict__.items():
|
|
761
|
+
if field_value and field_name not in exclude_fields:
|
|
762
|
+
comment += f"**{field_name}:** {field_value}\n"
|
|
763
|
+
return comment
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def create_issue_in_jira(
|
|
767
|
+
issue: Issue,
|
|
768
|
+
jira_client: JIRA,
|
|
769
|
+
jira_project: str,
|
|
770
|
+
issue_type: str,
|
|
771
|
+
add_attachments: Optional[bool] = True,
|
|
772
|
+
attachments: Optional[dict] = None,
|
|
773
|
+
api: Optional[Api] = None,
|
|
774
|
+
) -> jiraIssue:
|
|
775
|
+
"""
|
|
776
|
+
Create a new issue in Jira
|
|
777
|
+
|
|
778
|
+
:param Issue issue: RegScale issue object
|
|
779
|
+
:param JIRA jira_client: Jira client to use for issue creation in Jira
|
|
780
|
+
:param str jira_project: Project name in Jira to create the issue in
|
|
781
|
+
:param str issue_type: The type of issue to create in Jira
|
|
782
|
+
:param Optional[bool] add_attachments: Whether to add attachments to new issue, defaults to true
|
|
783
|
+
:param Optional[dict] attachments: Dictionary containing attachments, defaults to None
|
|
784
|
+
:param Optional[Api] api: API object to download attachments, defaults to None
|
|
785
|
+
:return: Newly created issue in Jira
|
|
786
|
+
:rtype: jiraIssue
|
|
787
|
+
"""
|
|
788
|
+
if not api:
|
|
789
|
+
api = Api()
|
|
790
|
+
try:
|
|
791
|
+
reg_issue_url = f"RegScale Issue #{issue.id}: {urljoin(api.config['domain'], f'/form/issues/{issue.id}')}\n\n"
|
|
792
|
+
logger.debug("Creating Jira issue: %s", issue.title)
|
|
793
|
+
new_issue = jira_client.create_issue(
|
|
794
|
+
project=jira_project,
|
|
795
|
+
summary=issue.title,
|
|
796
|
+
description=reg_issue_url + issue.description,
|
|
797
|
+
issuetype=issue_type,
|
|
798
|
+
)
|
|
799
|
+
logger.debug("Jira issue created: %s", new_issue.key)
|
|
800
|
+
# add a comment to the new Jira issue
|
|
801
|
+
logger.debug("Adding comment to Jira issue: %s", new_issue.key)
|
|
802
|
+
_ = jira_client.add_comment(
|
|
803
|
+
issue=new_issue,
|
|
804
|
+
body=reg_issue_url + _generate_jira_comment(issue),
|
|
805
|
+
)
|
|
806
|
+
logger.debug("Comment added to Jira issue: %s", new_issue.key)
|
|
807
|
+
except JIRAError as ex:
|
|
808
|
+
error_and_exit(f"Unable to create Jira issue.\nError: {ex}")
|
|
809
|
+
# add the attachments to the new Jira issue
|
|
810
|
+
if add_attachments and attachments:
|
|
811
|
+
compare_files_for_dupes_and_upload(
|
|
812
|
+
jira_issue=new_issue,
|
|
813
|
+
regscale_issue=issue,
|
|
814
|
+
jira_client=jira_client,
|
|
815
|
+
api=api,
|
|
816
|
+
)
|
|
817
|
+
return new_issue
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def compare_files_for_dupes_and_upload(
|
|
821
|
+
jira_issue: jiraIssue, regscale_issue: Issue, jira_client: JIRA, api: Api
|
|
822
|
+
) -> None:
|
|
823
|
+
"""
|
|
824
|
+
Compare files for duplicates and upload them to Jira and RegScale
|
|
825
|
+
|
|
826
|
+
:param jiraIssue jira_issue: Jira issue to upload the attachments to
|
|
827
|
+
:param Issue regscale_issue: RegScale issue to upload the attachments from
|
|
828
|
+
:param JIRA jira_client: Jira client to use for uploading the attachments
|
|
829
|
+
:param Api api: Api object to use for interacting with RegScale
|
|
830
|
+
:rtype: None
|
|
831
|
+
:return: None
|
|
832
|
+
"""
|
|
833
|
+
jira_uploaded_attachments = []
|
|
834
|
+
regscale_uploaded_attachments = []
|
|
835
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
836
|
+
jira_dir, regscale_dir = download_issue_attachments_to_directory(
|
|
837
|
+
directory=temp_dir,
|
|
838
|
+
jira_issue=jira_issue,
|
|
839
|
+
regscale_issue=regscale_issue,
|
|
840
|
+
api=api,
|
|
841
|
+
)
|
|
842
|
+
jira_attachment_hashes = compute_hashes_in_directory(jira_dir)
|
|
843
|
+
regscale_attachment_hashes = compute_hashes_in_directory(regscale_dir)
|
|
844
|
+
|
|
845
|
+
upload_files_to_jira(
|
|
846
|
+
jira_attachment_hashes,
|
|
847
|
+
regscale_attachment_hashes,
|
|
848
|
+
jira_issue,
|
|
849
|
+
regscale_issue,
|
|
850
|
+
jira_client,
|
|
851
|
+
jira_uploaded_attachments,
|
|
852
|
+
)
|
|
853
|
+
upload_files_to_regscale(
|
|
854
|
+
jira_attachment_hashes, regscale_attachment_hashes, regscale_issue, api, regscale_uploaded_attachments
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
log_upload_results(regscale_uploaded_attachments, jira_uploaded_attachments, regscale_issue, jira_issue)
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def upload_files_to_jira(
|
|
861
|
+
jira_attachment_hashes: dict,
|
|
862
|
+
regscale_attachment_hashes: dict,
|
|
863
|
+
jira_issue: jiraIssue,
|
|
864
|
+
regscale_issue: Issue,
|
|
865
|
+
jira_client: JIRA,
|
|
866
|
+
jira_uploaded_attachments: list,
|
|
867
|
+
) -> None:
|
|
868
|
+
"""
|
|
869
|
+
Upload files to Jira
|
|
870
|
+
|
|
871
|
+
:param dict jira_attachment_hashes: Dictionary of Jira attachment hashes
|
|
872
|
+
:param dict regscale_attachment_hashes: Dictionary of RegScale attachment hashes
|
|
873
|
+
:param jiraIssue jira_issue: Jira issue to upload the attachments to
|
|
874
|
+
:param Issue regscale_issue: RegScale issue to upload the attachments from
|
|
875
|
+
:param JIRA jira_client: Jira client to use for uploading the attachments
|
|
876
|
+
:param list jira_uploaded_attachments: List of Jira attachments that were uploaded
|
|
877
|
+
:rtype: None
|
|
878
|
+
:return: None
|
|
879
|
+
"""
|
|
880
|
+
for file_hash, file in regscale_attachment_hashes.items():
|
|
881
|
+
if file_hash not in jira_attachment_hashes:
|
|
882
|
+
try:
|
|
883
|
+
with open(file, "rb") as in_file:
|
|
884
|
+
jira_client.add_attachment(
|
|
885
|
+
issue=jira_issue.id,
|
|
886
|
+
attachment=BytesIO(in_file.read()), # type: ignore
|
|
887
|
+
filename=f"RegScale_Issue_{regscale_issue.id}_{Path(file).name}",
|
|
888
|
+
)
|
|
889
|
+
jira_uploaded_attachments.append(file)
|
|
890
|
+
except JIRAError as ex:
|
|
891
|
+
logger.error(
|
|
892
|
+
"Unable to upload %s to Jira issue %s.\nError: %s",
|
|
893
|
+
Path(file).name,
|
|
894
|
+
jira_issue.key,
|
|
895
|
+
ex,
|
|
896
|
+
)
|
|
897
|
+
except TypeError as ex:
|
|
898
|
+
logger.error(
|
|
899
|
+
"Unable to upload %s to Jira issue %s.\nError: %s",
|
|
900
|
+
Path(file).name,
|
|
901
|
+
jira_issue.key,
|
|
902
|
+
ex,
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def upload_files_to_regscale(
|
|
907
|
+
jira_attachment_hashes: dict,
|
|
908
|
+
regscale_attachment_hashes: dict,
|
|
909
|
+
regscale_issue: Issue,
|
|
910
|
+
api: Api,
|
|
911
|
+
regscale_uploaded_attachments: list,
|
|
912
|
+
) -> None:
|
|
913
|
+
"""
|
|
914
|
+
Upload files to RegScale
|
|
915
|
+
|
|
916
|
+
:param dict jira_attachment_hashes: Dictionary of Jira attachment hashes
|
|
917
|
+
:param dict regscale_attachment_hashes: Dictionary of RegScale attachment hashes
|
|
918
|
+
:param Issue regscale_issue: RegScale issue to upload the attachments to
|
|
919
|
+
:param Api api: Api object to use for interacting with RegScale
|
|
920
|
+
:param list regscale_uploaded_attachments: List of RegScale attachments that were uploaded
|
|
921
|
+
:rtype: None
|
|
922
|
+
:return: None
|
|
923
|
+
"""
|
|
924
|
+
for file_hash, file in jira_attachment_hashes.items():
|
|
925
|
+
if file_hash not in regscale_attachment_hashes:
|
|
926
|
+
with open(file, "rb") as in_file:
|
|
927
|
+
if File.upload_file_to_regscale(
|
|
928
|
+
file_name=f"Jira_attachment_{Path(file).name}",
|
|
929
|
+
parent_id=regscale_issue.id,
|
|
930
|
+
parent_module="issues",
|
|
931
|
+
api=api,
|
|
932
|
+
file_data=in_file.read(),
|
|
933
|
+
):
|
|
934
|
+
regscale_uploaded_attachments.append(file)
|
|
935
|
+
logger.debug(
|
|
936
|
+
"Uploaded %s to RegScale issue #%i.",
|
|
937
|
+
Path(file).name,
|
|
938
|
+
regscale_issue.id,
|
|
939
|
+
)
|
|
940
|
+
else:
|
|
941
|
+
logger.warning(
|
|
942
|
+
"Unable to upload %s to RegScale issue #%i.",
|
|
943
|
+
Path(file).name,
|
|
944
|
+
regscale_issue.id,
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def log_upload_results(
|
|
949
|
+
regscale_uploaded_attachments: list, jira_uploaded_attachments: list, regscale_issue: Issue, jira_issue: jiraIssue
|
|
950
|
+
) -> None:
|
|
951
|
+
"""
|
|
952
|
+
Log the results of the upload process
|
|
953
|
+
|
|
954
|
+
:param list regscale_uploaded_attachments: List of RegScale attachments that were uploaded
|
|
955
|
+
:param list jira_uploaded_attachments: List of Jira attachments that were uploaded
|
|
956
|
+
:param Issue regscale_issue: RegScale issue that the attachments were uploaded to
|
|
957
|
+
:param jiraIssue jira_issue: Jira issue that the attachments were uploaded to
|
|
958
|
+
:rtype: None
|
|
959
|
+
:return: None
|
|
960
|
+
"""
|
|
961
|
+
if regscale_uploaded_attachments and jira_uploaded_attachments:
|
|
962
|
+
logger.info(
|
|
963
|
+
"%i file(s) uploaded to RegScale issue #%i and %i file(s) uploaded to Jira issue %s.",
|
|
964
|
+
len(regscale_uploaded_attachments),
|
|
965
|
+
regscale_issue.id,
|
|
966
|
+
len(jira_uploaded_attachments),
|
|
967
|
+
jira_issue.key,
|
|
968
|
+
)
|
|
969
|
+
elif jira_uploaded_attachments:
|
|
970
|
+
logger.info(
|
|
971
|
+
"%i file(s) uploaded to Jira issue %s.",
|
|
972
|
+
len(jira_uploaded_attachments),
|
|
973
|
+
jira_issue.key,
|
|
974
|
+
)
|
|
975
|
+
elif regscale_uploaded_attachments:
|
|
976
|
+
logger.info(
|
|
977
|
+
"%i file(s) uploaded to RegScale issue #%i.",
|
|
978
|
+
len(regscale_uploaded_attachments),
|
|
979
|
+
regscale_issue.id,
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def validate_issue_type(jira_client: JIRA, issue_type: str) -> Any:
|
|
984
|
+
"""
|
|
985
|
+
Validate the provided issue type in Jira
|
|
986
|
+
|
|
987
|
+
:param JIRA jira_client: Jira client to use for the request
|
|
988
|
+
:param str issue_type: Issue type to validate
|
|
989
|
+
:rtype: Any
|
|
990
|
+
:return: True if the issue type is valid, otherwise exit with an error
|
|
991
|
+
"""
|
|
992
|
+
issue_types = jira_client.issue_types()
|
|
993
|
+
for issue in issue_types:
|
|
994
|
+
if issue.name == issue_type:
|
|
995
|
+
return True
|
|
996
|
+
message = f"Invalid Jira issue type provided: {issue_type}, the available types are: " + ", ".join(
|
|
997
|
+
{iss.name for iss in issue_types}
|
|
998
|
+
)
|
|
999
|
+
error_and_exit(error_desc=message)
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def download_issue_attachments_to_directory(
|
|
1003
|
+
directory: str,
|
|
1004
|
+
jira_issue: jiraIssue,
|
|
1005
|
+
regscale_issue: Issue,
|
|
1006
|
+
api: Api,
|
|
1007
|
+
) -> tuple[str, str]:
|
|
1008
|
+
"""
|
|
1009
|
+
Function to download attachments from Jira and RegScale issues to a directory
|
|
1010
|
+
|
|
1011
|
+
:param str directory: Directory to store the files in
|
|
1012
|
+
:param jiraIssue jira_issue: Jira issue to download the attachments for
|
|
1013
|
+
:param Issue regscale_issue: RegScale issue to download the attachments for
|
|
1014
|
+
:param Api api: Api object to use for interacting with RegScale
|
|
1015
|
+
:return: Tuple of strings containing the Jira and RegScale directories
|
|
1016
|
+
:rtype: tuple[str, str]
|
|
1017
|
+
"""
|
|
1018
|
+
# determine which attachments need to be uploaded to prevent duplicates by checking hashes
|
|
1019
|
+
jira_dir = os.path.join(directory, "jira")
|
|
1020
|
+
check_file_path(jira_dir, False)
|
|
1021
|
+
# download all attachments from Jira to the jira directory in temp_dir
|
|
1022
|
+
for attachment in jira_issue.fields.attachment:
|
|
1023
|
+
with open(os.path.join(jira_dir, attachment.filename), "wb") as file:
|
|
1024
|
+
file.write(attachment.get())
|
|
1025
|
+
# get the regscale issue attachments
|
|
1026
|
+
regscale_issue_attachments = File.get_files_for_parent_from_regscale(
|
|
1027
|
+
api=api,
|
|
1028
|
+
parent_id=regscale_issue.id,
|
|
1029
|
+
parent_module="issues",
|
|
1030
|
+
)
|
|
1031
|
+
# create a directory for the regscale attachments
|
|
1032
|
+
regscale_dir = os.path.join(directory, "regscale")
|
|
1033
|
+
check_file_path(regscale_dir, False)
|
|
1034
|
+
# download regscale attachments to the directory
|
|
1035
|
+
for attachment in regscale_issue_attachments:
|
|
1036
|
+
with open(os.path.join(regscale_dir, attachment.trustedDisplayName), "wb") as file:
|
|
1037
|
+
file.write(
|
|
1038
|
+
File.download_file_from_regscale_to_memory(
|
|
1039
|
+
api=api,
|
|
1040
|
+
record_id=regscale_issue.id,
|
|
1041
|
+
module="issues",
|
|
1042
|
+
stored_name=attachment.trustedStorageName,
|
|
1043
|
+
file_hash=(attachment.fileHash if attachment.fileHash else attachment.shaHash),
|
|
1044
|
+
)
|
|
1045
|
+
)
|
|
1046
|
+
return jira_dir, regscale_dir
|