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,980 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Salesforce integration for RegScale CLI to sync Salesforce Cases with RegScale Issues"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import mimetypes
|
|
7
|
+
import os
|
|
8
|
+
import tempfile
|
|
9
|
+
from collections import OrderedDict
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, Tuple
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
import simple_salesforce.exceptions
|
|
16
|
+
from requests.exceptions import HTTPError
|
|
17
|
+
from simple_salesforce import Salesforce
|
|
18
|
+
|
|
19
|
+
from regscale.core.app.api import Api
|
|
20
|
+
from regscale.core.app.application import Application
|
|
21
|
+
from regscale.core.app.utils.app_utils import (
|
|
22
|
+
check_file_path,
|
|
23
|
+
check_license,
|
|
24
|
+
compute_hashes_in_directory,
|
|
25
|
+
convert_datetime_to_regscale_string,
|
|
26
|
+
create_logger,
|
|
27
|
+
create_progress_object,
|
|
28
|
+
error_and_exit,
|
|
29
|
+
get_current_datetime,
|
|
30
|
+
get_file_name,
|
|
31
|
+
get_file_type,
|
|
32
|
+
save_data_to,
|
|
33
|
+
)
|
|
34
|
+
from regscale.utils.threading.threadhandler import create_threads, thread_assignment
|
|
35
|
+
from regscale.models import File, Issue, regscale_id, regscale_module
|
|
36
|
+
|
|
37
|
+
####################################################################################################
|
|
38
|
+
#
|
|
39
|
+
# simple-salesforce Documentation:
|
|
40
|
+
# https://github.com/simple-salesforce/simple-salesforce
|
|
41
|
+
# Salesforce API Docs:
|
|
42
|
+
# https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_rest.htm
|
|
43
|
+
# How to get Security Token:
|
|
44
|
+
# https://help.salesforce.com/s/articleView?id=sf.user_security_token.htm&type=5
|
|
45
|
+
#
|
|
46
|
+
####################################################################################################
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
job_progress = create_progress_object()
|
|
50
|
+
logger = create_logger()
|
|
51
|
+
updated_regscale_issues = []
|
|
52
|
+
new_regscale_issues = []
|
|
53
|
+
update_counter = []
|
|
54
|
+
new_sf_cases = []
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@click.group()
|
|
58
|
+
def salesforce():
|
|
59
|
+
"""Sync data between Salesforce Cases & RegScale Issues"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@salesforce.command()
|
|
63
|
+
@regscale_id()
|
|
64
|
+
@regscale_module()
|
|
65
|
+
@click.option(
|
|
66
|
+
"--sf_status",
|
|
67
|
+
type=click.STRING,
|
|
68
|
+
help="Salesforce status to filter Cases. (CASE SENSITIVE)",
|
|
69
|
+
required=False,
|
|
70
|
+
default=None,
|
|
71
|
+
)
|
|
72
|
+
@click.option(
|
|
73
|
+
"--not_equal",
|
|
74
|
+
is_flag=True,
|
|
75
|
+
help="Exclude the provided status from the filter.",
|
|
76
|
+
)
|
|
77
|
+
def sync(
|
|
78
|
+
regscale_id: int,
|
|
79
|
+
regscale_module: str,
|
|
80
|
+
sync_attachments: bool = True,
|
|
81
|
+
sf_status: Optional[str] = None,
|
|
82
|
+
not_equal: Optional[bool] = False,
|
|
83
|
+
):
|
|
84
|
+
"""Sync Salesforce cases and RegScale issues."""
|
|
85
|
+
sync_sf_and_regscale(
|
|
86
|
+
regscale_id=regscale_id,
|
|
87
|
+
regscale_module=regscale_module,
|
|
88
|
+
sync_attachments=sync_attachments,
|
|
89
|
+
sales_force_status=sf_status,
|
|
90
|
+
not_equal=not_equal,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def create_salesforce_client(config: dict) -> Salesforce:
|
|
95
|
+
"""
|
|
96
|
+
Create a Salesforce client with the provided config, if values aren't
|
|
97
|
+
in the provided config, it will use ENV vars
|
|
98
|
+
|
|
99
|
+
:param dict config: config to use to create the Salesforce client
|
|
100
|
+
:return: Salesforce client
|
|
101
|
+
:rtype: Salesforce
|
|
102
|
+
"""
|
|
103
|
+
# try to get the needed credentials before creating the Salesforce client
|
|
104
|
+
username = os.getenv("SF_USERNAME") or config.get("salesforceUserName")
|
|
105
|
+
password = os.getenv("SF_PASSWORD") or config.get("salesforcePassword")
|
|
106
|
+
security_token = os.getenv("SF_TOKEN") or config.get("salesforceToken")
|
|
107
|
+
if not username or not password or not security_token:
|
|
108
|
+
error_and_exit(
|
|
109
|
+
"Unable to retrieve Salesforce credentials, please provide them in"
|
|
110
|
+
" the init.yaml or as environment variables."
|
|
111
|
+
)
|
|
112
|
+
# make sure the credentials aren't the ones from Application.template
|
|
113
|
+
if any(credential in Application().template.values() for credential in [username, password, security_token]):
|
|
114
|
+
error_and_exit("Please update the Salesforce credentials in init.yaml or set up environment variables.")
|
|
115
|
+
return Salesforce(
|
|
116
|
+
username=username,
|
|
117
|
+
password=password,
|
|
118
|
+
security_token=security_token,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def sync_sf_and_regscale(
|
|
123
|
+
regscale_id: int,
|
|
124
|
+
regscale_module: str,
|
|
125
|
+
sync_attachments: Optional[bool] = True,
|
|
126
|
+
sales_force_status: Optional[str] = None,
|
|
127
|
+
not_equal: Optional[bool] = False,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Sync Salesforce cases and RegScale issues
|
|
131
|
+
|
|
132
|
+
:param int regscale_id: ID # from RegScale to associate cases with
|
|
133
|
+
:param str regscale_module: RegScale module to associate cases with
|
|
134
|
+
:param Optional[bool] sync_attachments: Sync attachments between Salesforce & RegScale, defaults to True
|
|
135
|
+
:param Optional[str] sales_force_status: Status to filter Salesforce cases by, defaults to None
|
|
136
|
+
:param Optional[bool] not_equal: Exclude the provided status, defaults to False
|
|
137
|
+
:rtype: None
|
|
138
|
+
"""
|
|
139
|
+
if not_equal and not sales_force_status:
|
|
140
|
+
error_and_exit("Cannot use '--not-equal' without providing '--sf-status'.")
|
|
141
|
+
|
|
142
|
+
app = check_license()
|
|
143
|
+
api = Api()
|
|
144
|
+
api.timeout = 60
|
|
145
|
+
|
|
146
|
+
sf_client = create_salesforce_client(app.config)
|
|
147
|
+
|
|
148
|
+
(
|
|
149
|
+
regscale_issues,
|
|
150
|
+
regscale_attachments,
|
|
151
|
+
) = Issue.fetch_issues_and_attachments_by_parent(
|
|
152
|
+
parent_id=regscale_id,
|
|
153
|
+
parent_module=regscale_module,
|
|
154
|
+
fetch_attachments=sync_attachments,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
sf_cases = fetch_sf_cases(
|
|
158
|
+
sf_client=sf_client,
|
|
159
|
+
sf_status=sales_force_status,
|
|
160
|
+
not_equal=not_equal,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if regscale_issues:
|
|
164
|
+
# sync RegScale issues to Salesforce
|
|
165
|
+
if issues_to_update := sync_regscale_to_sf(
|
|
166
|
+
regscale_issues=regscale_issues,
|
|
167
|
+
sf_client=sf_client,
|
|
168
|
+
sf_status=sales_force_status,
|
|
169
|
+
sync_attachments=sync_attachments,
|
|
170
|
+
attachments=regscale_attachments,
|
|
171
|
+
api=api,
|
|
172
|
+
):
|
|
173
|
+
with job_progress:
|
|
174
|
+
# create task to update RegScale issues
|
|
175
|
+
updating_issues = job_progress.add_task(
|
|
176
|
+
f"[#f8b737]Updating {len(issues_to_update)} RegScale issue(s)" "from Salesforce...",
|
|
177
|
+
total=len(issues_to_update),
|
|
178
|
+
)
|
|
179
|
+
# create threads to analyze Salesforce cases and RegScale issues
|
|
180
|
+
create_threads(
|
|
181
|
+
process=update_regscale_issues,
|
|
182
|
+
args=(
|
|
183
|
+
issues_to_update,
|
|
184
|
+
api,
|
|
185
|
+
updating_issues,
|
|
186
|
+
),
|
|
187
|
+
thread_count=len(issues_to_update),
|
|
188
|
+
)
|
|
189
|
+
# output the final result
|
|
190
|
+
logger.info(
|
|
191
|
+
"%i/%i issue(s) updated in RegScale.",
|
|
192
|
+
len(issues_to_update),
|
|
193
|
+
len(update_counter),
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
logger.info("No issues need to be updated in RegScale.")
|
|
197
|
+
|
|
198
|
+
if sf_cases:
|
|
199
|
+
# sync Salesforce cases to RegScale
|
|
200
|
+
with job_progress:
|
|
201
|
+
# create task to create RegScale issues
|
|
202
|
+
creating_issues = job_progress.add_task(
|
|
203
|
+
(
|
|
204
|
+
f"[#f8b737]Analyzing {len(sf_cases)} Salesforce case(s)"
|
|
205
|
+
f" and {len(regscale_issues)} RegScale issue(s)..."
|
|
206
|
+
),
|
|
207
|
+
total=len(sf_cases),
|
|
208
|
+
)
|
|
209
|
+
# create threads to analyze Salesforce cases and RegScale issues
|
|
210
|
+
create_threads(
|
|
211
|
+
process=create_and_update_regscale_issues,
|
|
212
|
+
args=(
|
|
213
|
+
sf_cases,
|
|
214
|
+
sf_client,
|
|
215
|
+
regscale_issues,
|
|
216
|
+
sync_attachments,
|
|
217
|
+
app,
|
|
218
|
+
regscale_id,
|
|
219
|
+
regscale_module,
|
|
220
|
+
creating_issues,
|
|
221
|
+
),
|
|
222
|
+
thread_count=len(sf_cases),
|
|
223
|
+
)
|
|
224
|
+
# output the final result
|
|
225
|
+
logger.info(
|
|
226
|
+
"Created %i Salesforce case(s), created %i issue(s) and updated %i issue(s) in RegScale.",
|
|
227
|
+
len(new_sf_cases),
|
|
228
|
+
len(new_regscale_issues),
|
|
229
|
+
len(updated_regscale_issues),
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
logger.info("No cases need to be analyzed from Salesforce.")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def fetch_sf_cases(
|
|
236
|
+
sf_client: Salesforce,
|
|
237
|
+
sf_status: Optional[str] = None,
|
|
238
|
+
not_equal: Optional[bool] = False,
|
|
239
|
+
) -> list:
|
|
240
|
+
"""
|
|
241
|
+
Fetch all cases from Salesforce by generating a SOQL with the optional params
|
|
242
|
+
|
|
243
|
+
:param Salesforce sf_client: Salesforce client to use for the request
|
|
244
|
+
:param Optional[str] sf_status: Status to filter Salesforce cases by, defaults to None
|
|
245
|
+
:param Optional[bool] not_equal: Exclude the provided status, defaults to False
|
|
246
|
+
:return: List of Salesforce cases
|
|
247
|
+
:rtype: list
|
|
248
|
+
"""
|
|
249
|
+
sf_cases = []
|
|
250
|
+
case_fields = sf_client.Case.describe()["fields"]
|
|
251
|
+
operator = "!=" if not_equal else "="
|
|
252
|
+
status = f"WHERE Status {operator} '{sf_status}'" if sf_status else ""
|
|
253
|
+
cases_query = f"""
|
|
254
|
+
SELECT {', '.join([field['name'] for field in case_fields])}
|
|
255
|
+
FROM Case
|
|
256
|
+
{status}
|
|
257
|
+
"""
|
|
258
|
+
cases = sf_client.query_all(cases_query)
|
|
259
|
+
sf_cases.extend(cases["records"])
|
|
260
|
+
while len(sf_cases) < cases["totalSize"]:
|
|
261
|
+
# extend the list of cases with the next page of results
|
|
262
|
+
sf_cases.extend(sf_client.query_more(cases["nextRecordsUrl"], identifier_is_url=True)["records"])
|
|
263
|
+
if sf_cases:
|
|
264
|
+
# write issue data to a json file
|
|
265
|
+
check_file_path("artifacts")
|
|
266
|
+
save_data_to(
|
|
267
|
+
file=Path("./artifacts/existingSalesforceCases.json"),
|
|
268
|
+
data=sf_cases,
|
|
269
|
+
output_log=False,
|
|
270
|
+
)
|
|
271
|
+
logger.info(
|
|
272
|
+
"Saved %i Salesforce case(s) with the provided criteria, see /artifacts/existingSalesforceCases.json",
|
|
273
|
+
len(sf_cases),
|
|
274
|
+
)
|
|
275
|
+
return sf_cases
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def fetch_sf_attachments(
|
|
279
|
+
sf_client: Salesforce,
|
|
280
|
+
case: OrderedDict,
|
|
281
|
+
) -> None:
|
|
282
|
+
"""
|
|
283
|
+
Fetch all attachments from Salesforce for the provided case
|
|
284
|
+
|
|
285
|
+
:param Salesforce sf_client: Salesforce client to use for the request
|
|
286
|
+
:param OrderedDict case: Case to fetch attachments for
|
|
287
|
+
:rtype: None
|
|
288
|
+
"""
|
|
289
|
+
attachments = None
|
|
290
|
+
attempt_one = sf_client.query_all(
|
|
291
|
+
f"""
|
|
292
|
+
SELECT
|
|
293
|
+
ContentDocumentId,
|
|
294
|
+
ContentDocument.Title,
|
|
295
|
+
ContentDocument.FileType,
|
|
296
|
+
ContentDocument.FileExtension
|
|
297
|
+
FROM ContentDocumentLink
|
|
298
|
+
WHERE LinkedEntityId = '{case.get("Id") or case.get("id")}'
|
|
299
|
+
"""
|
|
300
|
+
)
|
|
301
|
+
attempt_two = sf_client.query_all(
|
|
302
|
+
f"""
|
|
303
|
+
SELECT Id, Name, ContentType
|
|
304
|
+
FROM Attachment
|
|
305
|
+
WHERE ParentId = '{case.get("Id") or case.get("id")}'
|
|
306
|
+
"""
|
|
307
|
+
)
|
|
308
|
+
if attempt_one["totalSize"] > 0 and attempt_two["totalSize"] > 0:
|
|
309
|
+
attachments = {
|
|
310
|
+
key: attempt_one.get(key, []) + attempt_two.get(key, []) for key in set(attempt_one).union(set(attempt_two))
|
|
311
|
+
}
|
|
312
|
+
elif attempt_one["totalSize"] > 0:
|
|
313
|
+
attachments = attempt_one
|
|
314
|
+
elif attempt_two["totalSize"] > 0:
|
|
315
|
+
attachments = attempt_two
|
|
316
|
+
case["Attachments"] = attachments["records"] if attachments else []
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def map_case_to_regscale_issue(case: dict, config: dict, parent_id: int, parent_module: str) -> Issue:
|
|
320
|
+
"""
|
|
321
|
+
Map Salesforce case to a RegScale issue
|
|
322
|
+
|
|
323
|
+
:param dict case: Salesforce case
|
|
324
|
+
:param dict config: Application config
|
|
325
|
+
:param int parent_id: Parent record ID in RegScale
|
|
326
|
+
:param str parent_module: Parent record module in RegScale
|
|
327
|
+
:return: Issue object of the newly created issue in RegScale
|
|
328
|
+
:rtype: Issue
|
|
329
|
+
"""
|
|
330
|
+
import pandas as pd # Optimize import performance
|
|
331
|
+
|
|
332
|
+
due_date = map_case_due_date(case.get("Priority"), config)
|
|
333
|
+
issue = Issue(
|
|
334
|
+
title=case.get("Subject"),
|
|
335
|
+
severityLevel=Issue.assign_severity(case.get("Priority")),
|
|
336
|
+
issueOwnerId=config["userId"],
|
|
337
|
+
dueDate=due_date,
|
|
338
|
+
# pd.DataFrame.T transposes the columns and headers
|
|
339
|
+
description=f"{pd.DataFrame([case]).T.to_html(justify='left', border=1)}",
|
|
340
|
+
status=("Closed" if case["IsClosed"] else config["issues"]["salesforce"]["status"]),
|
|
341
|
+
salesforceId=case["CaseNumber"],
|
|
342
|
+
parentId=parent_id,
|
|
343
|
+
parentModule=parent_module,
|
|
344
|
+
dateCreated=get_current_datetime(),
|
|
345
|
+
dateCompleted=get_current_datetime() if case["IsClosed"] else None,
|
|
346
|
+
)
|
|
347
|
+
return issue
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def map_case_due_date(priority: Optional[str], config: dict) -> str:
|
|
351
|
+
"""
|
|
352
|
+
Use the provided priority to determine the due date of the issue in RegScale
|
|
353
|
+
|
|
354
|
+
:param Optional[str] priority: The priority of the case in Salesforce
|
|
355
|
+
:param dict config: Application config
|
|
356
|
+
:return: due date as a string
|
|
357
|
+
:rtype: str
|
|
358
|
+
"""
|
|
359
|
+
if not priority or priority.lower() not in config["issues"]["salesforce"].values():
|
|
360
|
+
due_date = datetime.now() + timedelta(days=config["issues"]["salesforce"]["low"])
|
|
361
|
+
else:
|
|
362
|
+
due_date = datetime.now() + timedelta(days=config["issues"]["salesforce"][priority.lower()])
|
|
363
|
+
return convert_datetime_to_regscale_string(due_date)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def create_and_update_regscale_issues(args: Tuple, thread: int) -> None:
|
|
367
|
+
"""
|
|
368
|
+
Function to create or update issues in RegScale from Salesforce
|
|
369
|
+
|
|
370
|
+
:param Tuple args: Tuple of args to use during the process
|
|
371
|
+
:param int thread: Thread number of current thread
|
|
372
|
+
:rtype: None
|
|
373
|
+
"""
|
|
374
|
+
# set up local variables from the passed args
|
|
375
|
+
(
|
|
376
|
+
sf_cases,
|
|
377
|
+
sf_client,
|
|
378
|
+
regscale_issues,
|
|
379
|
+
add_attachments,
|
|
380
|
+
app,
|
|
381
|
+
parent_id,
|
|
382
|
+
parent_module,
|
|
383
|
+
task,
|
|
384
|
+
) = args
|
|
385
|
+
# find which records should be executed by the current thread
|
|
386
|
+
threads = thread_assignment(thread=thread, total_items=len(sf_cases))
|
|
387
|
+
|
|
388
|
+
# iterate through the thread assignment items and process them
|
|
389
|
+
for i in range(len(threads)):
|
|
390
|
+
case: OrderedDict = sf_cases[threads[i]]
|
|
391
|
+
regscale_issue: Optional[Issue] = next(
|
|
392
|
+
(issue for issue in regscale_issues if issue.salesforceId == case.get("CaseNumber")),
|
|
393
|
+
None,
|
|
394
|
+
)
|
|
395
|
+
# see if the Salesforce case needs to be created as closed in RegScale
|
|
396
|
+
if case["IsClosed"] and regscale_issue:
|
|
397
|
+
# update the status and date completed of the RegScale issue
|
|
398
|
+
regscale_issue.status = "Closed"
|
|
399
|
+
regscale_issue.dateCompleted = get_current_datetime()
|
|
400
|
+
# update the issue in RegScale
|
|
401
|
+
updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
|
|
402
|
+
elif regscale_issue:
|
|
403
|
+
# update the issue in RegScale
|
|
404
|
+
updated_regscale_issues.append(Issue.update_issue(app=app, issue=regscale_issue))
|
|
405
|
+
else:
|
|
406
|
+
# map the Salesforce case to a RegScale issue object
|
|
407
|
+
issue = map_case_to_regscale_issue(
|
|
408
|
+
case=case,
|
|
409
|
+
config=app.config,
|
|
410
|
+
parent_id=parent_id,
|
|
411
|
+
parent_module=parent_module,
|
|
412
|
+
)
|
|
413
|
+
# create the issue in RegScale
|
|
414
|
+
if regscale_issue := Issue.insert_issue(
|
|
415
|
+
app=app,
|
|
416
|
+
issue=issue,
|
|
417
|
+
):
|
|
418
|
+
logger.debug(
|
|
419
|
+
"Created issue #%i-%s in RegScale.",
|
|
420
|
+
regscale_issue.id,
|
|
421
|
+
regscale_issue.title,
|
|
422
|
+
)
|
|
423
|
+
new_regscale_issues.append(regscale_issue)
|
|
424
|
+
else:
|
|
425
|
+
logger.warning("Unable to create issue in RegScale.\nIssue: %s", issue.dict())
|
|
426
|
+
if add_attachments and regscale_issue:
|
|
427
|
+
# determine which attachments need to be uploaded to prevent duplicates by
|
|
428
|
+
# getting the hashes of all Salesforce & RegScale attachments
|
|
429
|
+
compare_files_for_dupes_and_upload(
|
|
430
|
+
case=case,
|
|
431
|
+
regscale_issue=regscale_issue,
|
|
432
|
+
sf_client=sf_client,
|
|
433
|
+
api=Api(),
|
|
434
|
+
)
|
|
435
|
+
# update progress bar
|
|
436
|
+
job_progress.update(task, advance=1)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def sync_regscale_to_sf(
|
|
440
|
+
regscale_issues: list[Issue],
|
|
441
|
+
sf_client: Salesforce,
|
|
442
|
+
api: Api,
|
|
443
|
+
sync_attachments: bool = True,
|
|
444
|
+
sf_status: Optional[str] = None,
|
|
445
|
+
attachments: Optional[list[File]] = None,
|
|
446
|
+
) -> list[Issue]:
|
|
447
|
+
"""
|
|
448
|
+
Sync issues from RegScale to Salesforce cases
|
|
449
|
+
:param list[Issue] regscale_issues: list of RegScale issues to sync to Salesforce
|
|
450
|
+
:param Salesforce sf_client: Salesforce client to use for case creation in Salesforce
|
|
451
|
+
:param Api api: RegScale API client to move files, if sync_attachments is True
|
|
452
|
+
:param bool sync_attachments: Sync attachments from RegScale to Sales, defaults to True
|
|
453
|
+
:param Optional[str] sf_status: Status to set the Salesforce case to, defaults to None
|
|
454
|
+
:param Optional[list[File]] attachments: List of files to sync from RegScale to Salesforce, defaults to None
|
|
455
|
+
:return: list of RegScale issues that need to be updated
|
|
456
|
+
:rtype: list[Issue]
|
|
457
|
+
"""
|
|
458
|
+
issues_to_update = []
|
|
459
|
+
for issue in regscale_issues:
|
|
460
|
+
new_case_details = None
|
|
461
|
+
# see if Salesforce case already exists
|
|
462
|
+
if not issue.salesforceId:
|
|
463
|
+
new_case = create_case_in_sf(
|
|
464
|
+
issue=issue,
|
|
465
|
+
sf_client=sf_client,
|
|
466
|
+
sf_status=sf_status,
|
|
467
|
+
add_attachments=sync_attachments,
|
|
468
|
+
attachments=attachments,
|
|
469
|
+
)
|
|
470
|
+
# get the case number, creating a case only returns the ID
|
|
471
|
+
new_case_details = sf_client.Case.get(new_case["id"])
|
|
472
|
+
# update the RegScale issue for the Salesforce case number
|
|
473
|
+
issue.salesforceId = new_case_details.get("CaseNumber")
|
|
474
|
+
# add the issue to the update_issues global list
|
|
475
|
+
issues_to_update.append(issue)
|
|
476
|
+
elif issue.salesforceId:
|
|
477
|
+
try:
|
|
478
|
+
new_case_details = sf_client.Case.get_by_custom_id("CaseNumber", issue.salesforceId)
|
|
479
|
+
except simple_salesforce.exceptions.SalesforceResourceNotFound:
|
|
480
|
+
logger.warning("Unable to get details for Salesforce case #%s.", issue.salesforceId)
|
|
481
|
+
continue
|
|
482
|
+
if sync_attachments and attachments and (new_case_details and issue.salesforceId):
|
|
483
|
+
compare_files_for_dupes_and_upload(
|
|
484
|
+
case=new_case_details,
|
|
485
|
+
regscale_issue=issue,
|
|
486
|
+
sf_client=sf_client,
|
|
487
|
+
api=api,
|
|
488
|
+
)
|
|
489
|
+
return issues_to_update
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def create_case_in_sf(
|
|
493
|
+
issue: Issue,
|
|
494
|
+
sf_client: Salesforce,
|
|
495
|
+
sf_status: Optional[str] = None,
|
|
496
|
+
add_attachments: Optional[bool] = True,
|
|
497
|
+
attachments: Optional[list[Issue]] = None,
|
|
498
|
+
api: Optional[Api] = None,
|
|
499
|
+
) -> dict:
|
|
500
|
+
"""
|
|
501
|
+
Create a new case in Salesforce
|
|
502
|
+
|
|
503
|
+
:param Issue issue: RegScale issue object
|
|
504
|
+
:param Salesforce sf_client: Salesforce client to use for case creation in Salesforce
|
|
505
|
+
:param Optional[str] sf_status: Status to set the Salesforce case to, defaults to None
|
|
506
|
+
:param Optional[bool] add_attachments: Whether to add attachments to new case, defaults to true
|
|
507
|
+
:param Optional[list[Issue]] attachments: Dictionary containing attachments, defaults to None
|
|
508
|
+
:param Optional[Api] api: API object to download attachments from RegScale, defaults to None
|
|
509
|
+
:return: Newly created case in Salesforce
|
|
510
|
+
:rtype: dict
|
|
511
|
+
"""
|
|
512
|
+
new_case = sf_client.Case.create(
|
|
513
|
+
{
|
|
514
|
+
"Subject": f"RegScale Issue {issue.id}: {issue.title}",
|
|
515
|
+
"Description": f"RegScale Issue #{issue.id} description: {issue.description}",
|
|
516
|
+
"Priority": map_regscale_severity_to_sf(issue.severityLevel),
|
|
517
|
+
"Status": sf_status if sf_status else "New",
|
|
518
|
+
}
|
|
519
|
+
)
|
|
520
|
+
if new_case["errors"]:
|
|
521
|
+
logger.error(
|
|
522
|
+
"Unable to create case in Salesforce for RegScale issue #%i.\n%s",
|
|
523
|
+
issue.id,
|
|
524
|
+
new_case["errors"],
|
|
525
|
+
)
|
|
526
|
+
new_sf_cases.append(new_case)
|
|
527
|
+
# add the attachments to the new case in Salesforce
|
|
528
|
+
if add_attachments and attachments:
|
|
529
|
+
if not api:
|
|
530
|
+
api = Api()
|
|
531
|
+
api.timeout = 60
|
|
532
|
+
compare_files_for_dupes_and_upload(
|
|
533
|
+
case=sf_client.Case.get(new_case["id"]),
|
|
534
|
+
regscale_issue=issue,
|
|
535
|
+
sf_client=sf_client,
|
|
536
|
+
api=api,
|
|
537
|
+
)
|
|
538
|
+
return new_case
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def map_regscale_severity_to_sf(severity: str) -> str:
|
|
542
|
+
"""
|
|
543
|
+
Map RegScale severity to Salesforce priority
|
|
544
|
+
:param str severity: RegScale severity
|
|
545
|
+
:return: Salesforce priority
|
|
546
|
+
:rtype: str
|
|
547
|
+
"""
|
|
548
|
+
priority = "Low"
|
|
549
|
+
if severity.startswith("I -"):
|
|
550
|
+
priority = "High"
|
|
551
|
+
elif severity.startswith("II -"):
|
|
552
|
+
priority = "Medium"
|
|
553
|
+
return priority
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def compare_files_for_dupes_and_upload(
|
|
557
|
+
case: OrderedDict, regscale_issue: Issue, sf_client: Salesforce, api: Api
|
|
558
|
+
) -> None:
|
|
559
|
+
"""
|
|
560
|
+
Compare files for provided Salesforce Case and RegScale issue via hash to prevent duplicates
|
|
561
|
+
and then upload the attachments to the Salesforce case and/or RegScale issue
|
|
562
|
+
:param OrderedDict case: Salesforce case to compare attachments from
|
|
563
|
+
:param Issue regscale_issue: RegScale issue object to compare attachments from
|
|
564
|
+
:param Salesforce sf_client: Salesforce client to fetch attachments for the provided case
|
|
565
|
+
:param Api api:API client to move files, if sync_attachments is True
|
|
566
|
+
:rtype: None
|
|
567
|
+
"""
|
|
568
|
+
sf_uploaded_attachments = []
|
|
569
|
+
regscale_uploaded_attachments = []
|
|
570
|
+
# create a temporary directory to store the downloaded attachments from Salesforce and RegScale
|
|
571
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
572
|
+
# write attachments to the temporary directory
|
|
573
|
+
sf_dir, regscale_dir = download_attachments_to_directory(
|
|
574
|
+
directory=temp_dir,
|
|
575
|
+
case=case,
|
|
576
|
+
regscale_issue=regscale_issue,
|
|
577
|
+
api=api,
|
|
578
|
+
sf_client=sf_client,
|
|
579
|
+
)
|
|
580
|
+
# get the hashes for the attachments in the RegScale and Salesforce directories
|
|
581
|
+
# iterate all files in the Salesforce directory and compute their hashes
|
|
582
|
+
sf_attachment_hashes = compute_hashes_in_directory(sf_dir)
|
|
583
|
+
regscale_attachment_hashes = compute_hashes_in_directory(regscale_dir)
|
|
584
|
+
|
|
585
|
+
sf_uploaded_attachments = process_attachments_and_upload_to_sf(
|
|
586
|
+
regscale_attachment_hashes=regscale_attachment_hashes,
|
|
587
|
+
sf_attachment_hashes=sf_attachment_hashes,
|
|
588
|
+
case=case,
|
|
589
|
+
regscale_issue=regscale_issue,
|
|
590
|
+
sf_client=sf_client,
|
|
591
|
+
api=api,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
regscale_uploaded_attachments = process_attachments_and_upload_to_regscale(
|
|
595
|
+
regscale_attachment_hashes=regscale_attachment_hashes,
|
|
596
|
+
sf_attachment_hashes=sf_attachment_hashes,
|
|
597
|
+
regscale_issue=regscale_issue,
|
|
598
|
+
api=api,
|
|
599
|
+
)
|
|
600
|
+
output_console_summary(
|
|
601
|
+
regscale_issue=regscale_issue,
|
|
602
|
+
sf_case=case,
|
|
603
|
+
regscale_uploads=regscale_uploaded_attachments,
|
|
604
|
+
sf_uploads=sf_uploaded_attachments,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def process_attachments_and_upload_to_sf(
|
|
609
|
+
regscale_attachment_hashes: dict,
|
|
610
|
+
sf_attachment_hashes: dict,
|
|
611
|
+
case: OrderedDict,
|
|
612
|
+
regscale_issue: Issue,
|
|
613
|
+
sf_client: Salesforce,
|
|
614
|
+
api: Api,
|
|
615
|
+
) -> int:
|
|
616
|
+
"""
|
|
617
|
+
Process attachment hashes for RegScale and Salesforce and upload missing
|
|
618
|
+
files to Salesforce case
|
|
619
|
+
|
|
620
|
+
:param dict regscale_attachment_hashes: Dictionary containing RegScale attachment & hashes
|
|
621
|
+
:param dict sf_attachment_hashes: Dictionary containing Salesforce attachment & hashes
|
|
622
|
+
:param OrderedDict case: Salesforce case to compare attachments from
|
|
623
|
+
:param Issue regscale_issue: RegScale issue object to compare attachments from
|
|
624
|
+
:param Salesforce sf_client: Salesforce client to fetch attachments for the provided case
|
|
625
|
+
:param Api api: API client to move files
|
|
626
|
+
:return: # of attachments uploaded to Salesforce case
|
|
627
|
+
:rtype: int
|
|
628
|
+
"""
|
|
629
|
+
sf_uploaded_attachments = 0
|
|
630
|
+
# check where the files need to be uploaded to before uploading
|
|
631
|
+
for file_hash, file in regscale_attachment_hashes.items():
|
|
632
|
+
if file_hash not in sf_attachment_hashes:
|
|
633
|
+
if upload_file_to_sf(
|
|
634
|
+
# If it was a newly created case, it has id, if it already existed it
|
|
635
|
+
# we will use Id, with the or statement it will use whatever
|
|
636
|
+
# key is in the case dictionary
|
|
637
|
+
case_id=case.get("Id") or case.get("id"),
|
|
638
|
+
file_path=file,
|
|
639
|
+
file_name=f"RegScale_Issue_{regscale_issue.id}_{Path(file).name}",
|
|
640
|
+
sf_url=sf_client.base_url,
|
|
641
|
+
sf_session_id=sf_client.session_id,
|
|
642
|
+
api=api,
|
|
643
|
+
):
|
|
644
|
+
sf_uploaded_attachments += 1
|
|
645
|
+
logger.debug(
|
|
646
|
+
"Uploaded %s to Salesforce case #%s.",
|
|
647
|
+
Path(file).name,
|
|
648
|
+
case.get("Id") or case.get("id"),
|
|
649
|
+
)
|
|
650
|
+
else:
|
|
651
|
+
logger.warning(
|
|
652
|
+
"Unable to upload %s to Salesforce case #%s.",
|
|
653
|
+
Path(file).name,
|
|
654
|
+
case.get("Id") or case.get("id"),
|
|
655
|
+
)
|
|
656
|
+
return sf_uploaded_attachments
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def process_attachments_and_upload_to_regscale(
|
|
660
|
+
regscale_attachment_hashes: dict,
|
|
661
|
+
sf_attachment_hashes: dict,
|
|
662
|
+
regscale_issue: Issue,
|
|
663
|
+
api: Api,
|
|
664
|
+
) -> int:
|
|
665
|
+
"""
|
|
666
|
+
Process attachment hashes for RegScale and Salesforce and upload missing files to RegScale issue
|
|
667
|
+
|
|
668
|
+
:param dict regscale_attachment_hashes: Dictionary containing RegScale attachment & hashes
|
|
669
|
+
:param dict sf_attachment_hashes: Dictionary containing Salesforce attachment & hashes
|
|
670
|
+
:param Issue regscale_issue: RegScale issue to upload attachments to
|
|
671
|
+
:param Api api: API client to upload files to RegScale
|
|
672
|
+
:return: # of attachments uploaded to RegScale issue
|
|
673
|
+
:rtype: int
|
|
674
|
+
"""
|
|
675
|
+
regscale_uploaded_attachments = 0
|
|
676
|
+
for file_hash, file in sf_attachment_hashes.items():
|
|
677
|
+
if file_hash not in regscale_attachment_hashes:
|
|
678
|
+
with open(file, "rb") as in_file:
|
|
679
|
+
if File.upload_file_to_regscale(
|
|
680
|
+
file_name=f"Salesforce_attachment_{Path(file).name}",
|
|
681
|
+
parent_id=regscale_issue.id,
|
|
682
|
+
parent_module="issues",
|
|
683
|
+
api=api,
|
|
684
|
+
file_data=in_file.read(),
|
|
685
|
+
):
|
|
686
|
+
regscale_uploaded_attachments += 1
|
|
687
|
+
logger.debug(
|
|
688
|
+
"Uploaded %s to RegScale issue #%i.",
|
|
689
|
+
Path(file).name,
|
|
690
|
+
regscale_issue.id,
|
|
691
|
+
)
|
|
692
|
+
else:
|
|
693
|
+
logger.warning(
|
|
694
|
+
"Unable to upload %s to RegScale issue #%i.",
|
|
695
|
+
Path(file).name,
|
|
696
|
+
regscale_issue.id,
|
|
697
|
+
)
|
|
698
|
+
return regscale_uploaded_attachments
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def output_console_summary(
|
|
702
|
+
regscale_issue: Issue,
|
|
703
|
+
sf_case: dict,
|
|
704
|
+
regscale_uploads: Optional[int] = None,
|
|
705
|
+
sf_uploads: Optional[int] = None,
|
|
706
|
+
) -> None:
|
|
707
|
+
"""
|
|
708
|
+
Output a summary of the files uploaded to Salesforce and RegScale to the console
|
|
709
|
+
|
|
710
|
+
:param Issue regscale_issue: RegScale issue object
|
|
711
|
+
:param dict sf_case: Salesforce case dictionary
|
|
712
|
+
:param Optional[int] regscale_uploads: # of files uploaded to RegScale issue
|
|
713
|
+
:param Optional[int] sf_uploads: # of files uploaded to Salesforce case
|
|
714
|
+
:rtype: None
|
|
715
|
+
"""
|
|
716
|
+
if regscale_uploads and sf_uploads:
|
|
717
|
+
logger.info(
|
|
718
|
+
"%i file(s) uploaded to RegScale issue #%i and %i file(s) uploaded to Salesforce Case #%s.",
|
|
719
|
+
regscale_uploads,
|
|
720
|
+
regscale_issue.id,
|
|
721
|
+
sf_uploads,
|
|
722
|
+
sf_case["CaseNumber"],
|
|
723
|
+
)
|
|
724
|
+
elif sf_uploads:
|
|
725
|
+
logger.info(
|
|
726
|
+
"%i file(s) uploaded to Salesforce case #%s.",
|
|
727
|
+
sf_uploads,
|
|
728
|
+
sf_case["CaseNumber"],
|
|
729
|
+
)
|
|
730
|
+
elif regscale_uploads:
|
|
731
|
+
logger.info(
|
|
732
|
+
"%i file(s) uploaded to RegScale issue #%i.",
|
|
733
|
+
regscale_uploads,
|
|
734
|
+
regscale_issue.id,
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def download_attachments_to_directory(
|
|
739
|
+
directory: str,
|
|
740
|
+
case: OrderedDict,
|
|
741
|
+
regscale_issue: Issue,
|
|
742
|
+
api: Api,
|
|
743
|
+
sf_client: Salesforce,
|
|
744
|
+
) -> tuple[str, str]:
|
|
745
|
+
"""
|
|
746
|
+
Function to download attachments from Salesforce and RegScale to a directory
|
|
747
|
+
|
|
748
|
+
:param str directory: Directory to store the files in
|
|
749
|
+
:param OrderedDict case: Salesforce case to download the attachments for
|
|
750
|
+
:param Issue regscale_issue: RegScale issue to download the attachments for
|
|
751
|
+
:param Api api: Api object to use for interacting with RegScale
|
|
752
|
+
:param Salesforce sf_client: Salesforce client to use for interacting with Salesforce
|
|
753
|
+
:return: Tuple of strings containing the Salesforce and RegScale directories
|
|
754
|
+
:rtype: tuple[str, str]
|
|
755
|
+
"""
|
|
756
|
+
# determine which attachments need to be uploaded to prevent duplicates by checking hashes
|
|
757
|
+
sf_dir = os.path.join(directory, "salesforce")
|
|
758
|
+
check_file_path(sf_dir, False)
|
|
759
|
+
fetch_sf_attachments(
|
|
760
|
+
sf_client=sf_client,
|
|
761
|
+
case=case,
|
|
762
|
+
)
|
|
763
|
+
if case.get("Attachments"):
|
|
764
|
+
# download all attachments from Salesforce case to the Salesforce directory in temp_dir
|
|
765
|
+
for attachment in case["Attachments"]:
|
|
766
|
+
download_attachment_from_sf(
|
|
767
|
+
sf_url=sf_client.base_url,
|
|
768
|
+
sf_session_id=sf_client.session_id,
|
|
769
|
+
attachment=attachment,
|
|
770
|
+
api=api,
|
|
771
|
+
directory=sf_dir,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
# get the regscale issue attachments
|
|
775
|
+
regscale_issue_attachments = File.get_files_for_parent_from_regscale(
|
|
776
|
+
api=api,
|
|
777
|
+
parent_id=regscale_issue.id,
|
|
778
|
+
parent_module="issues",
|
|
779
|
+
)
|
|
780
|
+
# create a directory for the regscale attachments
|
|
781
|
+
regscale_dir = os.path.join(directory, "regscale")
|
|
782
|
+
check_file_path(regscale_dir, False)
|
|
783
|
+
# download regscale attachments to the directory
|
|
784
|
+
for attachment in regscale_issue_attachments:
|
|
785
|
+
with open(os.path.join(regscale_dir, attachment.trustedDisplayName), "wb") as file:
|
|
786
|
+
file.write(
|
|
787
|
+
File.download_file_from_regscale_to_memory(
|
|
788
|
+
api=api,
|
|
789
|
+
record_id=regscale_issue.id,
|
|
790
|
+
module="issues",
|
|
791
|
+
stored_name=attachment.trustedStorageName,
|
|
792
|
+
file_hash=(attachment.shaHash if attachment.shaHash else attachment.fileHash),
|
|
793
|
+
)
|
|
794
|
+
)
|
|
795
|
+
return sf_dir, regscale_dir
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def download_attachment_from_sf(
|
|
799
|
+
sf_url: str,
|
|
800
|
+
sf_session_id: str,
|
|
801
|
+
attachment: OrderedDict,
|
|
802
|
+
api: Api,
|
|
803
|
+
directory: str,
|
|
804
|
+
) -> None:
|
|
805
|
+
"""
|
|
806
|
+
Function to download an attachment from Salesforce and save it to the provided directory
|
|
807
|
+
|
|
808
|
+
:param str sf_url: Salesforce base url to use for downloading the file
|
|
809
|
+
:param str sf_session_id: Salesforce session id to use for authenticating the API call
|
|
810
|
+
:param OrderedDict attachment: Attachment to download from Salesforce
|
|
811
|
+
:param Api api: Api object to use for downloading the file
|
|
812
|
+
:param str directory: Directory to save the file to
|
|
813
|
+
:rtype: None
|
|
814
|
+
"""
|
|
815
|
+
attachment_id = None
|
|
816
|
+
endpoint = None
|
|
817
|
+
headers = {
|
|
818
|
+
"Authorization": f"Bearer {sf_session_id}",
|
|
819
|
+
}
|
|
820
|
+
if attachment.get("Id"):
|
|
821
|
+
attachment_id = attachment["Id"]
|
|
822
|
+
endpoint = "Attachment"
|
|
823
|
+
elif attachment.get("ContentDocumentId"):
|
|
824
|
+
attachment_id = attachment["ContentDocumentId"]
|
|
825
|
+
endpoint = "Document"
|
|
826
|
+
if not attachment_id or not endpoint:
|
|
827
|
+
logger.warning(
|
|
828
|
+
"Unable to download attachment from Salesforce case. No attachment ID found.\n%s",
|
|
829
|
+
attachment,
|
|
830
|
+
)
|
|
831
|
+
return
|
|
832
|
+
# Prepare Salesforce REST API endpoint for standard URL
|
|
833
|
+
url_standard = f"{sf_url}/sobjects/{endpoint}/{attachment_id}/body"
|
|
834
|
+
|
|
835
|
+
response = None
|
|
836
|
+
# Try to download from standard URL
|
|
837
|
+
try:
|
|
838
|
+
response = api.get(url_standard, headers=headers)
|
|
839
|
+
response.raise_for_status()
|
|
840
|
+
except HTTPError:
|
|
841
|
+
# Download from standard URL fails, try Lightning URL
|
|
842
|
+
# Prepare Salesforce REST API endpoint for Lightning URL
|
|
843
|
+
url_lightning = (
|
|
844
|
+
f'https://{sf_url.split("//")[1].split(".")[0]}.lightning.force.com'
|
|
845
|
+
f"/services/data/v58.0/sobjects/{endpoint}/{attachment_id}/body"
|
|
846
|
+
)
|
|
847
|
+
try:
|
|
848
|
+
response = api.get(url_lightning, headers=headers)
|
|
849
|
+
response.raise_for_status()
|
|
850
|
+
except HTTPError:
|
|
851
|
+
file_url = (
|
|
852
|
+
f'https://{sf_url.split("//")[1].split(".")[0]}.file.force.com'
|
|
853
|
+
f"/sfc/servlet.shepherd/document/download/{attachment_id}"
|
|
854
|
+
)
|
|
855
|
+
try:
|
|
856
|
+
response = api.get(file_url, headers=headers)
|
|
857
|
+
response.raise_for_status()
|
|
858
|
+
except HTTPError:
|
|
859
|
+
logger.error(
|
|
860
|
+
"Failed to download '%s.%s' from both standard and Lightning URLs in Salesforce.",
|
|
861
|
+
attachment["Title"],
|
|
862
|
+
attachment["FileType"].lower(),
|
|
863
|
+
)
|
|
864
|
+
# If a response was received, write the file data to a file
|
|
865
|
+
if response.ok:
|
|
866
|
+
try:
|
|
867
|
+
file_name = attachment["Name"]
|
|
868
|
+
except KeyError:
|
|
869
|
+
file_name = f'{attachment["ContentDocument"]["Title"]}.{attachment["ContentDocument"]["FileExtension"]}'
|
|
870
|
+
if not file_name:
|
|
871
|
+
logger.error(
|
|
872
|
+
"Unable to determine file name for attachment from Salesforce attachment.\n%s",
|
|
873
|
+
attachment,
|
|
874
|
+
)
|
|
875
|
+
return
|
|
876
|
+
with open(
|
|
877
|
+
os.path.join(
|
|
878
|
+
directory,
|
|
879
|
+
file_name.replace(" ", "_"),
|
|
880
|
+
),
|
|
881
|
+
"wb",
|
|
882
|
+
) as out_file:
|
|
883
|
+
out_file.write(response.content)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def upload_file_to_sf(
|
|
887
|
+
case_id: str,
|
|
888
|
+
file_path: str,
|
|
889
|
+
file_name: str,
|
|
890
|
+
sf_url: str,
|
|
891
|
+
sf_session_id: str,
|
|
892
|
+
api: Api,
|
|
893
|
+
) -> bool:
|
|
894
|
+
"""
|
|
895
|
+
Function to upload a file to Salesforce via API
|
|
896
|
+
|
|
897
|
+
:param str case_id: Salesforce Case ID to upload the file to
|
|
898
|
+
:param str file_path: File path to the file to upload
|
|
899
|
+
:param str file_name: Desired name of the file once uploaded
|
|
900
|
+
:param str sf_url: Salesforce URL to use for uploading the file
|
|
901
|
+
:param str sf_session_id: Salesforce session ID to use for authenticating the API call
|
|
902
|
+
:param Api api: Api object to use for uploading the file
|
|
903
|
+
:return: If upload was successful
|
|
904
|
+
:rtype: bool
|
|
905
|
+
"""
|
|
906
|
+
response = None
|
|
907
|
+
with open(file_path, "rb") as in_file:
|
|
908
|
+
data = in_file.read()
|
|
909
|
+
|
|
910
|
+
# Encode file data in base64
|
|
911
|
+
data = base64.b64encode(data).decode()
|
|
912
|
+
|
|
913
|
+
# Prepare Salesforce REST API headers
|
|
914
|
+
headers = {
|
|
915
|
+
"Authorization": "Bearer " + sf_session_id,
|
|
916
|
+
"Content-Type": "application/json",
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
# Prepare Salesforce REST API endpoint
|
|
920
|
+
url_standard = f"{sf_url}/sobjects/Attachment"
|
|
921
|
+
|
|
922
|
+
# Prepare Salesforce REST API request body
|
|
923
|
+
body = {
|
|
924
|
+
"Name": file_name,
|
|
925
|
+
"ParentId": case_id,
|
|
926
|
+
"Body": data,
|
|
927
|
+
"ContentType": mimetypes.types_map[get_file_type(file_path)],
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
# Make Salesforce REST API request
|
|
931
|
+
try:
|
|
932
|
+
response = api.post(url_standard, headers=headers, json=body)
|
|
933
|
+
response.raise_for_status()
|
|
934
|
+
except HTTPError:
|
|
935
|
+
# If upload with standard URL fails, try Lightning URL
|
|
936
|
+
# Prepare Salesforce REST API endpoint for Lightning URL
|
|
937
|
+
url_lightning = (
|
|
938
|
+
f'https://{sf_url.split("//")[1].split(".")[0]}.lightning.force.com'
|
|
939
|
+
f"/services/data/v52.0/sobjects/Attachment"
|
|
940
|
+
)
|
|
941
|
+
try:
|
|
942
|
+
response = api.post(url_lightning, headers=headers, json=body)
|
|
943
|
+
response.raise_for_status()
|
|
944
|
+
except HTTPError:
|
|
945
|
+
logger.error(
|
|
946
|
+
"Failed to upload '%s' using both standard and Lightning URLs in Salesforce.",
|
|
947
|
+
get_file_name(file_path),
|
|
948
|
+
)
|
|
949
|
+
return response.ok or False
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def update_regscale_issues(args: Tuple, thread: int) -> None:
|
|
953
|
+
"""
|
|
954
|
+
Function to compare Salesforce cases and RegScale issues with threads
|
|
955
|
+
|
|
956
|
+
:param Tuple args: Tuple of args to use during the process
|
|
957
|
+
:param int thread: Thread number of current thread
|
|
958
|
+
:rtype: None
|
|
959
|
+
"""
|
|
960
|
+
# set up local variables from the passed args
|
|
961
|
+
(
|
|
962
|
+
regscale_issues,
|
|
963
|
+
app,
|
|
964
|
+
task,
|
|
965
|
+
) = args
|
|
966
|
+
# find which records should be executed by the current thread
|
|
967
|
+
threads = thread_assignment(thread=thread, total_items=len(regscale_issues))
|
|
968
|
+
# iterate through the thread assignment items and process them
|
|
969
|
+
for i in range(len(threads)):
|
|
970
|
+
# set the issue for the thread for later use in the function
|
|
971
|
+
issue = regscale_issues[threads[i]]
|
|
972
|
+
# update the issue in RegScale
|
|
973
|
+
Issue.update_issue(app=app, issue=issue)
|
|
974
|
+
update_counter.append(Issue)
|
|
975
|
+
logger.info(
|
|
976
|
+
"RegScale Issue %i was updated with the Salesforce case number.",
|
|
977
|
+
issue.id,
|
|
978
|
+
)
|
|
979
|
+
# update progress bar
|
|
980
|
+
job_progress.update(task, advance=1)
|