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,1756 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Integration of ServiceNow into RegScale CLI tool"""
|
|
4
|
+
|
|
5
|
+
# standard python imports
|
|
6
|
+
import datetime
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from concurrent.futures import CancelledError, ThreadPoolExecutor, as_completed
|
|
11
|
+
from json import JSONDecodeError
|
|
12
|
+
from threading import Lock
|
|
13
|
+
from typing import List, Optional, Tuple, Union, Literal
|
|
14
|
+
from urllib.parse import urljoin
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
import requests
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from rich.progress import track
|
|
20
|
+
|
|
21
|
+
from regscale.core.app.api import Api
|
|
22
|
+
from regscale.core.app.application import Application
|
|
23
|
+
from regscale.core.app.logz import create_logger
|
|
24
|
+
from regscale.core.app.utils.api_handler import APIUpdateError
|
|
25
|
+
from regscale.core.app.utils.app_utils import (
|
|
26
|
+
check_file_path,
|
|
27
|
+
check_license,
|
|
28
|
+
create_progress_object,
|
|
29
|
+
compute_hashes_in_directory,
|
|
30
|
+
error_and_exit,
|
|
31
|
+
save_data_to,
|
|
32
|
+
get_current_datetime,
|
|
33
|
+
)
|
|
34
|
+
from regscale.core.app.utils.regscale_utils import verify_provided_module
|
|
35
|
+
from regscale.models import Change, Data, File, Issue, regscale_id, regscale_module
|
|
36
|
+
from regscale.utils.threading.threadhandler import create_threads, thread_assignment
|
|
37
|
+
|
|
38
|
+
job_progress = create_progress_object()
|
|
39
|
+
logger = create_logger()
|
|
40
|
+
APP_JSON = "application/json"
|
|
41
|
+
HEADERS = {"Content-Type": APP_JSON, "Accept": APP_JSON}
|
|
42
|
+
INCIDENT_TABLE = "api/now/table/incident"
|
|
43
|
+
update_counter = []
|
|
44
|
+
update_objects = []
|
|
45
|
+
new_regscale_objects = []
|
|
46
|
+
updated_regscale_objects = []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ServiceNowConfig:
|
|
50
|
+
"""
|
|
51
|
+
ServiceNow configuration class
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
reg_config: dict
|
|
55
|
+
url: str
|
|
56
|
+
user: str
|
|
57
|
+
pwd: str
|
|
58
|
+
reg_api: "Api" = Api()
|
|
59
|
+
api: "Api" = Api()
|
|
60
|
+
custom_fields: dict = {}
|
|
61
|
+
incident_type: str = "Low"
|
|
62
|
+
incident_group: str = "Service Desk"
|
|
63
|
+
urgency_map = {
|
|
64
|
+
"High": "1",
|
|
65
|
+
"Medium": "2",
|
|
66
|
+
"Low": "3",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def __init__(self, reg_config: dict, incident_type: str = "Low", incident_group: str = "Service Desk"):
|
|
70
|
+
self.reg_config = reg_config
|
|
71
|
+
self.url = reg_config.get("snowUrl")
|
|
72
|
+
self.user = reg_config.get("snowUserName")
|
|
73
|
+
self.pwd = reg_config.get("snowPassword")
|
|
74
|
+
self.api.auth = (self.user, self.pwd)
|
|
75
|
+
self.custom_fields = reg_config.get("serviceNow", {}).get("customFields", {})
|
|
76
|
+
self.incident_type = self.urgency_map.get(incident_type, "Low")
|
|
77
|
+
self.incident_group = incident_group
|
|
78
|
+
self.check_servicenow_config()
|
|
79
|
+
|
|
80
|
+
def check_servicenow_config(self) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Check if ServiceNow configuration is complete and not the defaults
|
|
83
|
+
|
|
84
|
+
:return: None
|
|
85
|
+
"""
|
|
86
|
+
fields = {"snowUrl": "url", "snowUserName": "user", "snowPassword": "pwd"}
|
|
87
|
+
missing_keys = []
|
|
88
|
+
for key, field in fields.items():
|
|
89
|
+
if value := getattr(self, field):
|
|
90
|
+
if value == self.api.app.template.get(key):
|
|
91
|
+
missing_keys.append(key)
|
|
92
|
+
if missing_keys:
|
|
93
|
+
error_and_exit(
|
|
94
|
+
f"ServiceNow configuration is incomplete. Missing values for the following key(s): {', '.join(missing_keys)}",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Create group to handle ServiceNow integration
|
|
99
|
+
@click.group()
|
|
100
|
+
def servicenow():
|
|
101
|
+
"""Auto-assigns incidents in ServiceNow for remediation."""
|
|
102
|
+
check_license()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
####################################################################################################
|
|
106
|
+
#
|
|
107
|
+
# PROCESS ISSUES TO ServiceNow
|
|
108
|
+
# ServiceNow REST API Docs:
|
|
109
|
+
# https://docs.servicenow.com/bundle/xanadu-application-development/page/build/custom-application/concept/build-applications.html
|
|
110
|
+
# Use the REST API Explorer in ServiceNow to select table, get URL, and select which fields to
|
|
111
|
+
# populate
|
|
112
|
+
#
|
|
113
|
+
####################################################################################################
|
|
114
|
+
@servicenow.command()
|
|
115
|
+
@regscale_id()
|
|
116
|
+
@regscale_module()
|
|
117
|
+
@click.option(
|
|
118
|
+
"--snow_assignment_group",
|
|
119
|
+
type=click.STRING,
|
|
120
|
+
help="RegScale will sync the issues for the record to this ServiceNow assignment group.",
|
|
121
|
+
prompt="Enter the name of the project in ServiceNow",
|
|
122
|
+
required=True,
|
|
123
|
+
)
|
|
124
|
+
@click.option(
|
|
125
|
+
"--snow_incident_type",
|
|
126
|
+
type=click.STRING,
|
|
127
|
+
help="Enter the ServiceNow incident type to use when creating new issues from RegScale.",
|
|
128
|
+
prompt="Enter the ServiceNow incident type",
|
|
129
|
+
required=True,
|
|
130
|
+
)
|
|
131
|
+
def issues(
|
|
132
|
+
regscale_id: int,
|
|
133
|
+
regscale_module: str,
|
|
134
|
+
snow_assignment_group: str,
|
|
135
|
+
snow_incident_type: str,
|
|
136
|
+
):
|
|
137
|
+
"""Process issues to ServiceNow."""
|
|
138
|
+
sync_snow_to_regscale(
|
|
139
|
+
regscale_id=regscale_id,
|
|
140
|
+
regscale_module=regscale_module,
|
|
141
|
+
snow_assignment_group=snow_assignment_group,
|
|
142
|
+
snow_incident_type=snow_incident_type,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@servicenow.command(name="issues_and_attachments")
|
|
147
|
+
@regscale_id()
|
|
148
|
+
@regscale_module()
|
|
149
|
+
@click.option(
|
|
150
|
+
"--snow_assignment_group",
|
|
151
|
+
"-g",
|
|
152
|
+
type=click.STRING,
|
|
153
|
+
help="RegScale will sync the issues for the record to this ServiceNow assignment group.",
|
|
154
|
+
prompt="ServiceNow assignment group",
|
|
155
|
+
required=True,
|
|
156
|
+
)
|
|
157
|
+
@click.option(
|
|
158
|
+
"--snow_incident_type",
|
|
159
|
+
"-t",
|
|
160
|
+
type=click.Choice(["High", "Medium", "Low"], case_sensitive=False),
|
|
161
|
+
help="Enter the ServiceNow incident type to use when creating new issues from RegScale.",
|
|
162
|
+
prompt="ServiceNow incident type",
|
|
163
|
+
required=True,
|
|
164
|
+
)
|
|
165
|
+
@click.option(
|
|
166
|
+
"--sync_attachments",
|
|
167
|
+
"-a",
|
|
168
|
+
type=click.BOOL,
|
|
169
|
+
help=(
|
|
170
|
+
"Whether RegScale will sync the attachments for the issue "
|
|
171
|
+
"in the provided ServiceNow assignment group and vice versa. Defaults to True."
|
|
172
|
+
),
|
|
173
|
+
required=False,
|
|
174
|
+
default=True,
|
|
175
|
+
)
|
|
176
|
+
@click.option(
|
|
177
|
+
"--sync_all_incidents",
|
|
178
|
+
"-all",
|
|
179
|
+
type=click.BOOL,
|
|
180
|
+
help=(
|
|
181
|
+
"Whether to Sync all incidents from ServiceNow and RegScale issues for the "
|
|
182
|
+
"provided regscale_id and regscale_module."
|
|
183
|
+
),
|
|
184
|
+
required=False,
|
|
185
|
+
default=True,
|
|
186
|
+
)
|
|
187
|
+
def issues_and_attachments(
|
|
188
|
+
regscale_id: int,
|
|
189
|
+
regscale_module: str,
|
|
190
|
+
snow_assignment_group: str,
|
|
191
|
+
snow_incident_type: str,
|
|
192
|
+
sync_attachments: bool = True,
|
|
193
|
+
sync_all_incidents: bool = True,
|
|
194
|
+
):
|
|
195
|
+
"""Process issues to ServiceNow."""
|
|
196
|
+
sync_snow_and_regscale(
|
|
197
|
+
parent_id=regscale_id,
|
|
198
|
+
parent_module=regscale_module,
|
|
199
|
+
snow_assignment_group=snow_assignment_group,
|
|
200
|
+
snow_incident_type=snow_incident_type.title(),
|
|
201
|
+
sync_attachments=sync_attachments,
|
|
202
|
+
sync_all_incidents=sync_all_incidents,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@servicenow.command(name="sync_work_notes")
|
|
207
|
+
@regscale_id(required=False)
|
|
208
|
+
@regscale_module(required=False)
|
|
209
|
+
def sync_work_notes(regscale_id: int, regscale_module: str):
|
|
210
|
+
"""Sync work notes from ServiceNow to existing issues in RegScale. Use regscale_id and regscale_module to sync work notes to specific issues."""
|
|
211
|
+
if not regscale_id and not regscale_module:
|
|
212
|
+
sync_notes_to_regscale()
|
|
213
|
+
elif regscale_id and regscale_module:
|
|
214
|
+
sync_notes_to_regscale(regscale_id=regscale_id, regscale_module=regscale_module)
|
|
215
|
+
else:
|
|
216
|
+
error_and_exit("Please provide both --regscale_id and --regscale_module to sync work notes.")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def get_issues_data(reg_api: Api, url_issues: str) -> List[dict]:
|
|
220
|
+
"""
|
|
221
|
+
Fetch the full issue list from RegScale
|
|
222
|
+
|
|
223
|
+
:param Api reg_api: RegScale API object
|
|
224
|
+
:param str url_issues: URL for RegScale issues
|
|
225
|
+
:return: List of issues
|
|
226
|
+
:rtype: List[dict]
|
|
227
|
+
"""
|
|
228
|
+
logger.info("Fetching full issue list from RegScale.")
|
|
229
|
+
issue_response = reg_api.get(url_issues)
|
|
230
|
+
result = []
|
|
231
|
+
if issue_response.status_code == 204:
|
|
232
|
+
logger.warning("No existing issues for this RegScale record.")
|
|
233
|
+
else:
|
|
234
|
+
try:
|
|
235
|
+
result = issue_response.json()
|
|
236
|
+
except JSONDecodeError as rex:
|
|
237
|
+
error_and_exit(f"Unable to fetch issues from RegScale.\\n{rex}")
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def create_snow_incident(
|
|
242
|
+
snow_config: ServiceNowConfig,
|
|
243
|
+
incident_url: str,
|
|
244
|
+
snow_incident: dict,
|
|
245
|
+
tag: dict,
|
|
246
|
+
custom_fields: Optional[dict] = None,
|
|
247
|
+
) -> dict:
|
|
248
|
+
"""
|
|
249
|
+
Create a new incident in ServiceNow
|
|
250
|
+
|
|
251
|
+
:param ServiceNowConfig snow_config: ServiceNow configuration as a dictionary
|
|
252
|
+
:param str incident_url: URL for ServiceNow incidents
|
|
253
|
+
:param dict snow_incident: Incident data
|
|
254
|
+
:param dict tag: ServiceNow tag to add to new incident
|
|
255
|
+
:param Optional[dict] custom_fields: Custom fields to add to the incident, defaults to None
|
|
256
|
+
:return: Incident response
|
|
257
|
+
:rtype: dict
|
|
258
|
+
"""
|
|
259
|
+
if custom_fields is None:
|
|
260
|
+
custom_fields = {}
|
|
261
|
+
result = {}
|
|
262
|
+
snow_api = snow_config.api
|
|
263
|
+
try:
|
|
264
|
+
response = snow_api.post(
|
|
265
|
+
url=incident_url,
|
|
266
|
+
headers=HEADERS,
|
|
267
|
+
json={**snow_incident, **custom_fields},
|
|
268
|
+
)
|
|
269
|
+
if not response.raise_for_status():
|
|
270
|
+
result = response.json()
|
|
271
|
+
if tag:
|
|
272
|
+
new_incident = result["result"]
|
|
273
|
+
payload = {
|
|
274
|
+
"label": tag["sys_id"],
|
|
275
|
+
"read": "yes",
|
|
276
|
+
"table": "incident",
|
|
277
|
+
"table_key": new_incident["sys_id"],
|
|
278
|
+
"title": f"Incident - {new_incident['number']}",
|
|
279
|
+
"id_type": "incident",
|
|
280
|
+
"id_display": new_incident["number"],
|
|
281
|
+
"viewable_by": "everyone",
|
|
282
|
+
}
|
|
283
|
+
tag_url = urljoin(snow_config.url, "/api/now/table/label_entry")
|
|
284
|
+
res = snow_api.post(tag_url, headers=HEADERS, json=payload)
|
|
285
|
+
if res.ok:
|
|
286
|
+
logger.debug("Tag %s added to incident %s", tag["name"], new_incident["sys_id"])
|
|
287
|
+
else:
|
|
288
|
+
logger.warning("Unable to add tag %s to incident %s", tag["name"], new_incident["sys_id"])
|
|
289
|
+
except requests.exceptions.RequestException as ex:
|
|
290
|
+
if custom_fields:
|
|
291
|
+
logger.error(
|
|
292
|
+
"Unable to create incident %s in ServiceNow. Retrying without custom fields %s...\n%s",
|
|
293
|
+
snow_incident,
|
|
294
|
+
custom_fields,
|
|
295
|
+
ex,
|
|
296
|
+
)
|
|
297
|
+
return create_snow_incident(snow_config, incident_url, snow_incident, tag)
|
|
298
|
+
logger.error("Unable to create incident %s in ServiceNow...\n%s", snow_incident, ex)
|
|
299
|
+
return result
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def sync_snow_to_regscale(
|
|
303
|
+
regscale_id: int,
|
|
304
|
+
regscale_module: str,
|
|
305
|
+
snow_assignment_group: str,
|
|
306
|
+
snow_incident_type: str,
|
|
307
|
+
) -> None:
|
|
308
|
+
"""
|
|
309
|
+
Sync issues from ServiceNow to RegScale via API
|
|
310
|
+
:param int regscale_id: ID # of record in RegScale to associate issues with
|
|
311
|
+
:param str regscale_module: RegScale module to associate issues with
|
|
312
|
+
:param str snow_assignment_group: Snow assignment group to filter for
|
|
313
|
+
:param str snow_incident_type: Snow incident type to filter for
|
|
314
|
+
:rtype: None
|
|
315
|
+
"""
|
|
316
|
+
# initialize variables
|
|
317
|
+
app = Application()
|
|
318
|
+
reg_api = Api()
|
|
319
|
+
verify_provided_module(regscale_module)
|
|
320
|
+
config = app.config
|
|
321
|
+
|
|
322
|
+
# Group related variables into a dictionary
|
|
323
|
+
snow_config = ServiceNowConfig(reg_config=config)
|
|
324
|
+
|
|
325
|
+
url_issues = urljoin(
|
|
326
|
+
config["domain"],
|
|
327
|
+
f"api/issues/getAllByParent/{str(regscale_id)}/{str(regscale_module).lower()}",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
if issues_data := get_issues_data(reg_api, url_issues):
|
|
331
|
+
check_file_path("artifacts")
|
|
332
|
+
save_data_to(
|
|
333
|
+
file=Path("./artifacts/existingRecordIssues.json"),
|
|
334
|
+
data=issues_data,
|
|
335
|
+
)
|
|
336
|
+
logger.info(
|
|
337
|
+
"Writing out RegScale issue list for Record # %s to the artifacts folder "
|
|
338
|
+
+ "(see existingRecordIssues.json).",
|
|
339
|
+
regscale_id,
|
|
340
|
+
)
|
|
341
|
+
logger.info(
|
|
342
|
+
"%s existing issues retrieved for processing from RegScale.",
|
|
343
|
+
len(issues_data),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
int_new, int_skipped = process_issues(
|
|
347
|
+
issues_data,
|
|
348
|
+
snow_config,
|
|
349
|
+
snow_assignment_group,
|
|
350
|
+
snow_incident_type,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
logger.info(
|
|
354
|
+
"%i new issue incidents opened in ServiceNow and %i issues already exist and were skipped.",
|
|
355
|
+
int_new,
|
|
356
|
+
int_skipped,
|
|
357
|
+
)
|
|
358
|
+
else:
|
|
359
|
+
logger.warning("No issues found for this record in RegScale. No issues were processed.")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def create_snow_assignment_group(snow_assignment_group: str, snow_config: ServiceNowConfig) -> None:
|
|
363
|
+
"""
|
|
364
|
+
Create a new assignment group in ServiceNow or if one already exists,
|
|
365
|
+
a 403 is returned from SNOW.
|
|
366
|
+
|
|
367
|
+
:param str snow_assignment_group: ServiceNow assignment group
|
|
368
|
+
:param ServiceNowConfig snow_config: ServiceNow configuration
|
|
369
|
+
:rtype: None
|
|
370
|
+
"""
|
|
371
|
+
# Create a service now assignment group. The api will not allow me create dups
|
|
372
|
+
snow_api = snow_config.api
|
|
373
|
+
payload = {
|
|
374
|
+
"name": snow_assignment_group,
|
|
375
|
+
"description": "An automatically generated Service Now assignment group from RegScale.",
|
|
376
|
+
"active": True,
|
|
377
|
+
}
|
|
378
|
+
url = urljoin(snow_config.url, "api/now/table/sys_user_group")
|
|
379
|
+
response = snow_api.post(
|
|
380
|
+
url=url,
|
|
381
|
+
headers=HEADERS,
|
|
382
|
+
json=payload,
|
|
383
|
+
)
|
|
384
|
+
if response.status_code == 201:
|
|
385
|
+
logger.info("ServiceNow Assignment Group %s created.", snow_assignment_group)
|
|
386
|
+
elif response.status_code == 403:
|
|
387
|
+
# I expect a 403 for a duplicate code already found
|
|
388
|
+
logger.debug("ServiceNow Assignment Group %s already exists.", snow_assignment_group)
|
|
389
|
+
elif response.status_code == 401:
|
|
390
|
+
error_and_exit("Unauthorized to create ServiceNow Assignment Group. Please check your ServiceNow credentials.")
|
|
391
|
+
else:
|
|
392
|
+
error_and_exit(
|
|
393
|
+
f"Unable to create ServiceNow Assignment Group {snow_assignment_group}. "
|
|
394
|
+
f"Status code: {response.status_code}"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def get_service_now_incidents(snow_config: ServiceNowConfig, query: str) -> List[dict]:
|
|
399
|
+
"""
|
|
400
|
+
Get all incidents from ServiceNow
|
|
401
|
+
|
|
402
|
+
:param dict snow_config: ServiceNow configuration
|
|
403
|
+
:param str query: Query string
|
|
404
|
+
:return: List of incidents
|
|
405
|
+
:rtype: List[dict]
|
|
406
|
+
"""
|
|
407
|
+
snow_api = snow_config.api
|
|
408
|
+
incident_url = urljoin(snow_config.url, INCIDENT_TABLE)
|
|
409
|
+
offset = 0
|
|
410
|
+
limit = 500
|
|
411
|
+
data = []
|
|
412
|
+
|
|
413
|
+
while True:
|
|
414
|
+
result, offset = query_service_now(
|
|
415
|
+
api=snow_api,
|
|
416
|
+
snow_url=incident_url,
|
|
417
|
+
offset=offset,
|
|
418
|
+
limit=limit,
|
|
419
|
+
query=query,
|
|
420
|
+
)
|
|
421
|
+
data += result
|
|
422
|
+
if not result:
|
|
423
|
+
break
|
|
424
|
+
|
|
425
|
+
return data
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def process_issues(
|
|
429
|
+
issues_data: List[dict],
|
|
430
|
+
snow_config: ServiceNowConfig,
|
|
431
|
+
snow_assignment_group: str,
|
|
432
|
+
snow_incident_type: str,
|
|
433
|
+
) -> Tuple[int, int]:
|
|
434
|
+
"""
|
|
435
|
+
Process issues and create new incidents in ServiceNow
|
|
436
|
+
|
|
437
|
+
:param List[dict] issues_data: List of issues
|
|
438
|
+
:param ServiceNowConfig snow_config: ServiceNow configuration
|
|
439
|
+
:param str snow_assignment_group: ServiceNow assignment group
|
|
440
|
+
:param str snow_incident_type: ServiceNow incident type
|
|
441
|
+
:return: Number of new incidents created, plus number of skipped incidents
|
|
442
|
+
:rtype: Tuple[int, int]
|
|
443
|
+
"""
|
|
444
|
+
config = snow_config.reg_config
|
|
445
|
+
int_new = 0
|
|
446
|
+
int_skipped = 0
|
|
447
|
+
# Need a lock for int_new
|
|
448
|
+
lock = Lock()
|
|
449
|
+
# Make sure the assignment group exists
|
|
450
|
+
create_snow_assignment_group(snow_assignment_group, snow_config)
|
|
451
|
+
|
|
452
|
+
with job_progress:
|
|
453
|
+
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
454
|
+
if issues_data:
|
|
455
|
+
task = job_progress.add_task(
|
|
456
|
+
f"[#f8b737]Syncing {len(issues_data)} RegScale issues to ServiceNow",
|
|
457
|
+
total=len(issues_data),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
futures = [
|
|
461
|
+
executor.submit(
|
|
462
|
+
create_incident,
|
|
463
|
+
iss,
|
|
464
|
+
snow_config,
|
|
465
|
+
snow_assignment_group,
|
|
466
|
+
snow_incident_type,
|
|
467
|
+
config,
|
|
468
|
+
{},
|
|
469
|
+
{},
|
|
470
|
+
False,
|
|
471
|
+
)
|
|
472
|
+
for iss in issues_data
|
|
473
|
+
]
|
|
474
|
+
for future in as_completed(futures):
|
|
475
|
+
try:
|
|
476
|
+
snow_response = future.result()
|
|
477
|
+
with lock:
|
|
478
|
+
if snow_response:
|
|
479
|
+
iss = snow_response["originalIssue"]
|
|
480
|
+
int_new += 1
|
|
481
|
+
logger.debug(snow_response)
|
|
482
|
+
logger.info(
|
|
483
|
+
"SNOW Incident ID %s created.",
|
|
484
|
+
snow_response["result"]["sys_id"],
|
|
485
|
+
)
|
|
486
|
+
iss["serviceNowId"] = snow_response["result"]["sys_id"]
|
|
487
|
+
try:
|
|
488
|
+
Issue(**iss).save()
|
|
489
|
+
except APIUpdateError as ex:
|
|
490
|
+
logger.error(
|
|
491
|
+
"Unable to update issue in RegScale: %s\n%s",
|
|
492
|
+
iss,
|
|
493
|
+
ex,
|
|
494
|
+
)
|
|
495
|
+
else:
|
|
496
|
+
int_skipped += 1
|
|
497
|
+
job_progress.update(task, advance=1)
|
|
498
|
+
except CancelledError as e:
|
|
499
|
+
logger.error("Future was cancelled: %s", e)
|
|
500
|
+
|
|
501
|
+
return int_new, int_skipped
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def create_incident(
|
|
505
|
+
iss: dict,
|
|
506
|
+
snow_config: ServiceNowConfig,
|
|
507
|
+
snow_assignment_group: str,
|
|
508
|
+
snow_incident_type: str,
|
|
509
|
+
config: dict,
|
|
510
|
+
tag: dict,
|
|
511
|
+
attachments: dict,
|
|
512
|
+
add_attachments: bool = False,
|
|
513
|
+
) -> Optional[dict]:
|
|
514
|
+
"""
|
|
515
|
+
Create a new incident in ServiceNow
|
|
516
|
+
|
|
517
|
+
:param dict iss: Issue data
|
|
518
|
+
:param ServiceNowConfig snow_config: ServiceNow configuration
|
|
519
|
+
:param str snow_assignment_group: ServiceNow assignment group
|
|
520
|
+
:param str snow_incident_type: ServiceNow incident type
|
|
521
|
+
:param dict config: Application config
|
|
522
|
+
:param dict tag: ServiceNow tag to add to new incidents
|
|
523
|
+
:param dict attachments: Dict of attachments from RegScale and ServiceNow
|
|
524
|
+
:param bool add_attachments: Sync attachments from RegScale to ServiceNow, defaults to False
|
|
525
|
+
:return: Response dataset from ServiceNow or None
|
|
526
|
+
:rtype: Optional[dict]
|
|
527
|
+
"""
|
|
528
|
+
response = None
|
|
529
|
+
if iss.get("serviceNowId", "") != "" and iss.get("serviceNowId") is not None:
|
|
530
|
+
return response
|
|
531
|
+
|
|
532
|
+
snow_incident = map_regscale_to_snow_incident(
|
|
533
|
+
regscale_issue=iss,
|
|
534
|
+
snow_assignment_group=snow_assignment_group,
|
|
535
|
+
snow_incident_type=snow_incident_type,
|
|
536
|
+
config=config,
|
|
537
|
+
)
|
|
538
|
+
incident_url = urljoin(snow_config.url, INCIDENT_TABLE)
|
|
539
|
+
if response := create_snow_incident(
|
|
540
|
+
snow_config=snow_config,
|
|
541
|
+
incident_url=incident_url,
|
|
542
|
+
snow_incident=snow_incident,
|
|
543
|
+
tag=tag,
|
|
544
|
+
custom_fields=snow_config.custom_fields, # type: ignore
|
|
545
|
+
):
|
|
546
|
+
response["originalIssue"] = iss
|
|
547
|
+
if add_attachments and attachments:
|
|
548
|
+
compare_files_for_dupes_and_upload(
|
|
549
|
+
snow_issue=response["result"],
|
|
550
|
+
regscale_issue=iss,
|
|
551
|
+
snow_config=snow_config,
|
|
552
|
+
attachments=attachments,
|
|
553
|
+
)
|
|
554
|
+
return response
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def map_regscale_to_snow_incident(
|
|
558
|
+
regscale_issue: Union[dict, Issue],
|
|
559
|
+
snow_assignment_group: str,
|
|
560
|
+
snow_incident_type: str,
|
|
561
|
+
config: dict,
|
|
562
|
+
) -> dict:
|
|
563
|
+
"""
|
|
564
|
+
Map RegScale issue to ServiceNow incident
|
|
565
|
+
|
|
566
|
+
:param Union[dict, Issue] regscale_issue: RegScale issue to map to ServiceNow incident
|
|
567
|
+
:param str snow_assignment_group: ServiceNow assignment group
|
|
568
|
+
:param str snow_incident_type: ServiceNow incident type
|
|
569
|
+
:param dict config: RegScale CLI Application configuration
|
|
570
|
+
:return: ServiceNow incident data
|
|
571
|
+
:rtype: dict
|
|
572
|
+
"""
|
|
573
|
+
if isinstance(regscale_issue, Issue):
|
|
574
|
+
regscale_issue = regscale_issue.model_dump()
|
|
575
|
+
snow_incident = {
|
|
576
|
+
"description": regscale_issue["description"],
|
|
577
|
+
"short_description": regscale_issue["title"],
|
|
578
|
+
"assignment_group": snow_assignment_group,
|
|
579
|
+
"due_date": regscale_issue["dueDate"],
|
|
580
|
+
"comments": f"RegScale Issue #{regscale_issue['id']} - {config['domain']}/form/issues/{regscale_issue['id']}",
|
|
581
|
+
"state": "New",
|
|
582
|
+
"urgency": snow_incident_type,
|
|
583
|
+
}
|
|
584
|
+
# update state and closed_at if the RegScale issue is closed
|
|
585
|
+
if regscale_issue["status"] == "Closed":
|
|
586
|
+
snow_incident["state"] = "Closed"
|
|
587
|
+
snow_incident["closed_at"] = regscale_issue["dateCompleted"]
|
|
588
|
+
return snow_incident
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def sync_snow_and_regscale(
|
|
592
|
+
parent_id: int,
|
|
593
|
+
parent_module: str,
|
|
594
|
+
snow_assignment_group: str,
|
|
595
|
+
snow_incident_type: Literal["High", "Medium", "Low"],
|
|
596
|
+
sync_attachments: bool = True,
|
|
597
|
+
sync_all_incidents: bool = True,
|
|
598
|
+
) -> None:
|
|
599
|
+
"""
|
|
600
|
+
Sync issues, bidirectionally, from ServiceNow into RegScale as issues
|
|
601
|
+
|
|
602
|
+
:param int parent_id: ID # from RegScale to associate issues with
|
|
603
|
+
:param str parent_module: RegScale module to associate issues with
|
|
604
|
+
:param str snow_assignment_group: Assignment Group Name of the project in ServiceNow
|
|
605
|
+
:param str snow_incident_type: Type of issues to sync from ServiceNow
|
|
606
|
+
:param bool sync_attachments: Whether to sync attachments in RegScale & ServiceNow, defaults to True
|
|
607
|
+
:param bool sync_all_incidents: Whether to sync all incidents from ServiceNow and RegScale issues
|
|
608
|
+
:rtype: None
|
|
609
|
+
"""
|
|
610
|
+
app = check_license()
|
|
611
|
+
api = Api()
|
|
612
|
+
config = app.config
|
|
613
|
+
snow_config = ServiceNowConfig(
|
|
614
|
+
reg_config=config, incident_type=snow_incident_type, incident_group=snow_assignment_group
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# see if provided RegScale Module is an accepted option
|
|
618
|
+
verify_provided_module(parent_module)
|
|
619
|
+
# Make sure the assignment group exists
|
|
620
|
+
create_snow_assignment_group(snow_assignment_group, snow_config)
|
|
621
|
+
query = "&sysparm_display_value=true"
|
|
622
|
+
tag = get_or_create_snow_tag(snow_config=snow_config, tag_name=f"regscale-{parent_module}-{parent_id}")
|
|
623
|
+
if sync_all_incidents:
|
|
624
|
+
incidents = get_service_now_incidents(snow_config=snow_config, query=query)
|
|
625
|
+
else:
|
|
626
|
+
incidents = get_snow_incidents(snow_config=snow_config, query=query, tag=tag)
|
|
627
|
+
|
|
628
|
+
(
|
|
629
|
+
regscale_issues,
|
|
630
|
+
regscale_attachments,
|
|
631
|
+
) = Issue.fetch_issues_and_attachments_by_parent(
|
|
632
|
+
parent_id=parent_id,
|
|
633
|
+
parent_module=parent_module,
|
|
634
|
+
fetch_attachments=sync_attachments,
|
|
635
|
+
)
|
|
636
|
+
snow_attachments = get_snow_attachment_metadata(snow_config)
|
|
637
|
+
attachments = {
|
|
638
|
+
"regscale": regscale_attachments or {},
|
|
639
|
+
"snow": snow_attachments or {},
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if regscale_issues:
|
|
643
|
+
# sync RegScale issues to SNOW
|
|
644
|
+
if issues_to_update := sync_regscale_to_snow(
|
|
645
|
+
regscale_issues=regscale_issues,
|
|
646
|
+
snow_config=snow_config,
|
|
647
|
+
config=config,
|
|
648
|
+
attachments=attachments,
|
|
649
|
+
tag=tag,
|
|
650
|
+
sync_attachments=sync_attachments,
|
|
651
|
+
):
|
|
652
|
+
with job_progress:
|
|
653
|
+
# create task to update RegScale issues
|
|
654
|
+
updating_issues = job_progress.add_task(
|
|
655
|
+
f"[#f8b737]Updating {len(issues_to_update)} RegScale issue(s) from ServiceNow...",
|
|
656
|
+
total=len(issues_to_update),
|
|
657
|
+
)
|
|
658
|
+
# create threads to analyze ServiceNow incidents and RegScale issues
|
|
659
|
+
create_threads(
|
|
660
|
+
process=update_regscale_issues,
|
|
661
|
+
args=(
|
|
662
|
+
issues_to_update,
|
|
663
|
+
api,
|
|
664
|
+
updating_issues,
|
|
665
|
+
),
|
|
666
|
+
thread_count=len(issues_to_update),
|
|
667
|
+
)
|
|
668
|
+
# output the final result
|
|
669
|
+
logger.info(
|
|
670
|
+
"%i/%i issue(s) updated in RegScale.",
|
|
671
|
+
len(issues_to_update),
|
|
672
|
+
len(update_counter),
|
|
673
|
+
)
|
|
674
|
+
else:
|
|
675
|
+
logger.info("No issues need to be updated in RegScale.")
|
|
676
|
+
|
|
677
|
+
if incidents:
|
|
678
|
+
sync_snow_incidents_to_regscale_issues(
|
|
679
|
+
incidents=incidents,
|
|
680
|
+
regscale_issues=regscale_issues,
|
|
681
|
+
sync_attachments=sync_attachments,
|
|
682
|
+
attachments=attachments,
|
|
683
|
+
app=app,
|
|
684
|
+
snow_config=snow_config,
|
|
685
|
+
parent_id=parent_id,
|
|
686
|
+
parent_module=parent_module,
|
|
687
|
+
)
|
|
688
|
+
else:
|
|
689
|
+
logger.info("No incidents need to be analyzed from ServiceNow.")
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def get_or_create_snow_tag(snow_config: ServiceNowConfig, tag_name: str) -> dict:
|
|
693
|
+
"""
|
|
694
|
+
Check if a tag exists in ServiceNow, if not, create it
|
|
695
|
+
|
|
696
|
+
:param ServiceNowConfig snow_config: ServiceNow configuration
|
|
697
|
+
:param str tag_name: Desired name of the tag
|
|
698
|
+
:return: List of tags
|
|
699
|
+
:rtype: List[str]
|
|
700
|
+
"""
|
|
701
|
+
snow_api = snow_config.api
|
|
702
|
+
tags_url = urljoin(snow_config.url, "api/now/table/label")
|
|
703
|
+
|
|
704
|
+
offset = 0
|
|
705
|
+
limit = 500
|
|
706
|
+
data = []
|
|
707
|
+
|
|
708
|
+
while True:
|
|
709
|
+
result, offset = query_service_now(
|
|
710
|
+
api=snow_api,
|
|
711
|
+
snow_url=tags_url,
|
|
712
|
+
offset=offset,
|
|
713
|
+
limit=limit,
|
|
714
|
+
query=f"&sysparm_query=name={tag_name}",
|
|
715
|
+
)
|
|
716
|
+
data += result
|
|
717
|
+
if not result:
|
|
718
|
+
break
|
|
719
|
+
|
|
720
|
+
if data:
|
|
721
|
+
return data[0]
|
|
722
|
+
return create_snow_tag(snow_config=snow_config, tag_name=tag_name)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def create_snow_tag(snow_config: ServiceNowConfig, tag_name: str) -> Optional[dict]:
|
|
726
|
+
"""
|
|
727
|
+
Create a new assignment group in ServiceNow or if one already exists,
|
|
728
|
+
a 403 is returned from SNOW.
|
|
729
|
+
|
|
730
|
+
:param ServiceNowConfig snow_config: ServiceNow configuration dictionary
|
|
731
|
+
:param str tag_name: ServiceNow tag name
|
|
732
|
+
:return: Created tag or None
|
|
733
|
+
:rtype: Optional[dict]
|
|
734
|
+
"""
|
|
735
|
+
# Create a service now tag. The api will not allow duplicates
|
|
736
|
+
snow_api = snow_config.api
|
|
737
|
+
payload = {
|
|
738
|
+
"name": tag_name,
|
|
739
|
+
"max_entries": 100000, # arbitrary number, just needs to be large to avoid limit issues
|
|
740
|
+
"global": False,
|
|
741
|
+
"active": True,
|
|
742
|
+
"sys_class_name": "tag",
|
|
743
|
+
"type": "Standard",
|
|
744
|
+
}
|
|
745
|
+
url = urljoin(snow_config.url, "api/now/table/label")
|
|
746
|
+
response = snow_api.post(
|
|
747
|
+
url=url,
|
|
748
|
+
headers=HEADERS,
|
|
749
|
+
json=payload,
|
|
750
|
+
)
|
|
751
|
+
if response.status_code == 201:
|
|
752
|
+
logger.info("ServiceNow Tag %s created.", tag_name)
|
|
753
|
+
return response.json()["result"]
|
|
754
|
+
elif response.status_code == 403:
|
|
755
|
+
# I expect a 403 for a duplicate code already found
|
|
756
|
+
logger.debug("ServiceNow Tag %s already exists.", tag_name)
|
|
757
|
+
elif response.status_code == 401:
|
|
758
|
+
error_and_exit("Unauthorized to create ServiceNow Tag. Please check your ServiceNow credentials.")
|
|
759
|
+
else:
|
|
760
|
+
error_and_exit(f"Unable to create ServiceNow Tag {tag_name}. Status code: {response.status_code}")
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def update_regscale_issues(args: Tuple, thread: int) -> None:
|
|
764
|
+
"""
|
|
765
|
+
Function to compare ServiceNow incidents and RegScale issues
|
|
766
|
+
|
|
767
|
+
:param Tuple args: Tuple of args to use during the process
|
|
768
|
+
:param int thread: Thread number of current thread
|
|
769
|
+
:rtype: None
|
|
770
|
+
"""
|
|
771
|
+
# set up local variables from the passed args
|
|
772
|
+
(
|
|
773
|
+
regscale_issues,
|
|
774
|
+
app,
|
|
775
|
+
task,
|
|
776
|
+
) = args
|
|
777
|
+
# find which records should be executed by the current thread
|
|
778
|
+
threads = thread_assignment(thread=thread, total_items=len(regscale_issues))
|
|
779
|
+
# iterate through the thread assignment items and process them
|
|
780
|
+
for i in range(len(threads)):
|
|
781
|
+
# set the issue for the thread for later use in the function
|
|
782
|
+
issue = regscale_issues[threads[i]]
|
|
783
|
+
# update the issue in RegScale
|
|
784
|
+
issue = issue.save()
|
|
785
|
+
logger.debug(
|
|
786
|
+
"RegScale Issue %i was updated with the ServiceNow incident #%s.",
|
|
787
|
+
issue.id,
|
|
788
|
+
issue.serviceNowId,
|
|
789
|
+
)
|
|
790
|
+
update_counter.append(issue)
|
|
791
|
+
# update progress bar
|
|
792
|
+
job_progress.update(task, advance=1)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def get_snow_incidents(snow_config: ServiceNowConfig, query: str = "", tag: Optional[dict] = None) -> List[dict]:
|
|
796
|
+
"""
|
|
797
|
+
Get all incidents from ServiceNow
|
|
798
|
+
|
|
799
|
+
:param ServiceNowConfig snow_config: ServiceNow Configuration object
|
|
800
|
+
:param str query: Query string, defaults to ""
|
|
801
|
+
:param dict tag: Tag to filter incidents by, defaults to None
|
|
802
|
+
:return: List of incidents
|
|
803
|
+
:rtype: List[dict]
|
|
804
|
+
"""
|
|
805
|
+
snow_api = snow_config.api
|
|
806
|
+
incident_url = urljoin(snow_config.url, INCIDENT_TABLE)
|
|
807
|
+
offset = 0
|
|
808
|
+
limit = 500
|
|
809
|
+
data = []
|
|
810
|
+
if tag:
|
|
811
|
+
query += f"&sysparm_query=sys_tags.{tag['sys_id']}={tag['sys_id']}"
|
|
812
|
+
|
|
813
|
+
while True:
|
|
814
|
+
result, offset = query_service_now(
|
|
815
|
+
api=snow_api,
|
|
816
|
+
snow_url=incident_url,
|
|
817
|
+
offset=offset,
|
|
818
|
+
limit=limit,
|
|
819
|
+
query=query,
|
|
820
|
+
)
|
|
821
|
+
data += result
|
|
822
|
+
if not result:
|
|
823
|
+
break
|
|
824
|
+
|
|
825
|
+
return data
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def sync_regscale_to_snow(
|
|
829
|
+
regscale_issues: list[Issue],
|
|
830
|
+
snow_config: ServiceNowConfig,
|
|
831
|
+
config: dict,
|
|
832
|
+
attachments: dict,
|
|
833
|
+
tag: dict,
|
|
834
|
+
sync_attachments: bool = True,
|
|
835
|
+
) -> list[Issue]:
|
|
836
|
+
"""
|
|
837
|
+
Sync issues from RegScale to SNOW
|
|
838
|
+
|
|
839
|
+
:param list[Issue] regscale_issues: list of RegScale issues to sync to SNOW
|
|
840
|
+
:param ServiceNowConfig snow_config: SNOW configuration
|
|
841
|
+
:param dict config: RegScale CLI configuration
|
|
842
|
+
:param dict attachments: Dict of attachments from RegScale and SNOW
|
|
843
|
+
:param dict tag: SNOW tag to add to new incidents
|
|
844
|
+
:param bool sync_attachments: Sync attachments from RegScale to SNOW, defaults to True
|
|
845
|
+
:return: list of RegScale issues that need to be updated
|
|
846
|
+
:rtype: list[Issue]
|
|
847
|
+
"""
|
|
848
|
+
new_issue_counter = 0
|
|
849
|
+
issuess_to_update = []
|
|
850
|
+
with job_progress:
|
|
851
|
+
# create task to create ServiceNow incidents
|
|
852
|
+
creating_issues = job_progress.add_task(
|
|
853
|
+
f"[#f8b737]Verifying {len(regscale_issues)} RegScale issue(s) exist in ServiceNow...",
|
|
854
|
+
total=len(regscale_issues),
|
|
855
|
+
)
|
|
856
|
+
for issue in regscale_issues:
|
|
857
|
+
# create_incident has logic to check if the issue already has serviceNowId populated
|
|
858
|
+
if new_issue := create_incident(
|
|
859
|
+
iss=issue.model_dump(),
|
|
860
|
+
snow_config=snow_config,
|
|
861
|
+
snow_assignment_group=snow_config.incident_group,
|
|
862
|
+
snow_incident_type=snow_config.incident_type,
|
|
863
|
+
config=config,
|
|
864
|
+
tag=tag,
|
|
865
|
+
add_attachments=sync_attachments,
|
|
866
|
+
attachments=attachments,
|
|
867
|
+
):
|
|
868
|
+
# log progress
|
|
869
|
+
new_issue_counter += 1
|
|
870
|
+
# get the ServiceNow incident ID
|
|
871
|
+
snow_id = new_issue["result"]["number"]
|
|
872
|
+
# update the RegScale issue for the ServiceNow link
|
|
873
|
+
issue.serviceNowId = snow_id
|
|
874
|
+
# add the issue to the update_issues global list
|
|
875
|
+
issuess_to_update.append(issue)
|
|
876
|
+
job_progress.update(creating_issues, advance=1)
|
|
877
|
+
# output the final result
|
|
878
|
+
logger.info("%i new incident(s) opened in ServiceNow.", new_issue_counter)
|
|
879
|
+
return issuess_to_update
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def compare_files_for_dupes_and_upload(
|
|
883
|
+
snow_issue: dict,
|
|
884
|
+
regscale_issue: dict,
|
|
885
|
+
snow_config: ServiceNowConfig,
|
|
886
|
+
attachments: dict,
|
|
887
|
+
) -> None:
|
|
888
|
+
"""
|
|
889
|
+
Compare files for duplicates and upload them to ServiceNow and RegScale
|
|
890
|
+
|
|
891
|
+
:param dict snow_issue: SNOW issue to upload the attachments to
|
|
892
|
+
:param dict regscale_issue: RegScale issue to upload the attachments from
|
|
893
|
+
:param ServiceNowConfig snow_config: SNOW configuration
|
|
894
|
+
:param dict attachments: Attachments from RegScale and ServiceNow
|
|
895
|
+
:rtype: None
|
|
896
|
+
"""
|
|
897
|
+
import tempfile
|
|
898
|
+
|
|
899
|
+
api = Api()
|
|
900
|
+
snow_uploaded_attachments = []
|
|
901
|
+
regscale_uploaded_attachments = []
|
|
902
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
903
|
+
snow_dir, regscale_dir = download_issue_attachments_to_directory(
|
|
904
|
+
directory=temp_dir,
|
|
905
|
+
regscale_issue=regscale_issue,
|
|
906
|
+
snow_issue=snow_issue,
|
|
907
|
+
api=api,
|
|
908
|
+
snow_config=snow_config,
|
|
909
|
+
attachments=attachments,
|
|
910
|
+
)
|
|
911
|
+
snow_attachment_hashes = compute_hashes_in_directory(snow_dir)
|
|
912
|
+
regscale_attachment_hashes = compute_hashes_in_directory(regscale_dir)
|
|
913
|
+
|
|
914
|
+
upload_files_to_snow(
|
|
915
|
+
snow_attachment_hashes=snow_attachment_hashes,
|
|
916
|
+
regscale_attachment_hashes=regscale_attachment_hashes,
|
|
917
|
+
snow_issue=snow_issue,
|
|
918
|
+
snow_config=snow_config,
|
|
919
|
+
regscale_issue=regscale_issue,
|
|
920
|
+
snow_uploaded_attachments=snow_uploaded_attachments,
|
|
921
|
+
)
|
|
922
|
+
upload_files_to_regscale(
|
|
923
|
+
snow_attachment_hashes=snow_attachment_hashes,
|
|
924
|
+
regscale_attachment_hashes=regscale_attachment_hashes,
|
|
925
|
+
regscale_issue=regscale_issue,
|
|
926
|
+
api=api,
|
|
927
|
+
regscale_uploaded_attachments=regscale_uploaded_attachments,
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
log_upload_results(regscale_uploaded_attachments, snow_uploaded_attachments, regscale_issue, snow_issue)
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def download_snow_attachment(attachment: dict, snow_config: ServiceNowConfig, save_dir: str) -> None:
|
|
934
|
+
"""
|
|
935
|
+
Download an attachment from ServiceNow
|
|
936
|
+
|
|
937
|
+
:param dict attachment: Attachment to download
|
|
938
|
+
:param ServiceNowConfig snow_config: SNOW configuration
|
|
939
|
+
:param str save_dir: Directory to save the attachment in
|
|
940
|
+
:rtype: None
|
|
941
|
+
"""
|
|
942
|
+
snow_api = snow_config.api
|
|
943
|
+
# check if the file_name has an extension
|
|
944
|
+
if not Path(attachment["file_name"]).suffix:
|
|
945
|
+
import mimetypes
|
|
946
|
+
|
|
947
|
+
suffix = mimetypes.guess_extension(attachment["content_type"])
|
|
948
|
+
attachment["file_name"] = attachment["file_name"] + suffix
|
|
949
|
+
with open(os.path.join(save_dir, attachment["file_name"]), "wb") as file:
|
|
950
|
+
res = snow_api.get(attachment["download_link"])
|
|
951
|
+
if res.ok:
|
|
952
|
+
file.write(res.content)
|
|
953
|
+
else:
|
|
954
|
+
logger.error("Unable to download %s from ServiceNow.", attachment["file_name"])
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def upload_files_to_snow(
|
|
958
|
+
snow_attachment_hashes: dict,
|
|
959
|
+
regscale_attachment_hashes: dict,
|
|
960
|
+
snow_issue: dict,
|
|
961
|
+
snow_config: ServiceNowConfig,
|
|
962
|
+
regscale_issue: dict,
|
|
963
|
+
snow_uploaded_attachments: list,
|
|
964
|
+
) -> None:
|
|
965
|
+
"""
|
|
966
|
+
Upload files to ServiceNow
|
|
967
|
+
|
|
968
|
+
:param dict snow_attachment_hashes: Dictionary of SNOW attachment hashes
|
|
969
|
+
:param dict regscale_attachment_hashes: Dictionary of RegScale attachment hashes
|
|
970
|
+
:param dict snow_issue: SNOW issue to upload the attachments to
|
|
971
|
+
:param ServiceNowConfig snow_config: SNOW configuration
|
|
972
|
+
:param dict regscale_issue: RegScale issue to upload the attachments from
|
|
973
|
+
:param list snow_uploaded_attachments: List of SNOW attachments that were uploaded
|
|
974
|
+
:rtype: None
|
|
975
|
+
"""
|
|
976
|
+
snow_api = snow_config.api
|
|
977
|
+
upload_url = urljoin(snow_config.url, "/api/now/attachment/file")
|
|
978
|
+
|
|
979
|
+
for file_hash, file in regscale_attachment_hashes.items():
|
|
980
|
+
if file_hash not in snow_attachment_hashes:
|
|
981
|
+
with open(file, "rb") as in_file:
|
|
982
|
+
path_file = Path(file)
|
|
983
|
+
data = in_file.read()
|
|
984
|
+
params = {
|
|
985
|
+
"table_name": "incident",
|
|
986
|
+
"table_sys_id": snow_issue["sys_id"],
|
|
987
|
+
"file_name": f"RegScale_Issue_{regscale_issue['id']}_{path_file.name}",
|
|
988
|
+
}
|
|
989
|
+
headers = {"Content-Type": File.determine_mime_type(path_file.suffix), "Accept": APP_JSON}
|
|
990
|
+
response = snow_api.post(url=upload_url, headers=headers, data=data, params=params) # type: ignore
|
|
991
|
+
if response.raise_for_status():
|
|
992
|
+
logger.error(
|
|
993
|
+
"Unable to upload %s to ServiceNow incident %s.",
|
|
994
|
+
path_file.name,
|
|
995
|
+
snow_issue["number"],
|
|
996
|
+
)
|
|
997
|
+
else:
|
|
998
|
+
logger.debug(
|
|
999
|
+
"Uploaded %s to ServiceNow incident %s.",
|
|
1000
|
+
path_file.name,
|
|
1001
|
+
snow_issue["number"],
|
|
1002
|
+
)
|
|
1003
|
+
snow_uploaded_attachments.append(file)
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def download_issue_attachments_to_directory(
|
|
1007
|
+
directory: str,
|
|
1008
|
+
regscale_issue: dict,
|
|
1009
|
+
snow_issue: dict,
|
|
1010
|
+
api: Api,
|
|
1011
|
+
snow_config: ServiceNowConfig,
|
|
1012
|
+
attachments: dict,
|
|
1013
|
+
) -> tuple[str, str]:
|
|
1014
|
+
"""
|
|
1015
|
+
Function to download attachments from ServiceNow and RegScale issues to a directory
|
|
1016
|
+
|
|
1017
|
+
:param str directory: Directory to store the files in
|
|
1018
|
+
:param dict regscale_issue: RegScale issue to download the attachments for
|
|
1019
|
+
:param dict snow_issue: SNOW issue to download the attachments for
|
|
1020
|
+
:param Api api: Api object to use for interacting with RegScale
|
|
1021
|
+
:param ServiceNowConfig snow_config: SNOW configuration
|
|
1022
|
+
:param dict attachments: Dictionary of attachments from RegScale and ServiceNow
|
|
1023
|
+
:return: Tuple of strings containing the SNOW and RegScale directories
|
|
1024
|
+
:rtype: tuple[str, str]
|
|
1025
|
+
"""
|
|
1026
|
+
# determine which attachments need to be uploaded to prevent duplicates by checking hashes
|
|
1027
|
+
snow_dir = os.path.join(directory, "snow")
|
|
1028
|
+
check_file_path(snow_dir, False)
|
|
1029
|
+
# download all attachments from ServiceNow to the snow directory in temp_dir
|
|
1030
|
+
for attachment in attachments["snow"].get(snow_issue.get("sys_id"), []):
|
|
1031
|
+
download_snow_attachment(attachment, snow_config, snow_dir)
|
|
1032
|
+
# get the regscale issue attachments
|
|
1033
|
+
regscale_issue_attachments = attachments["regscale"].get(regscale_issue["id"], [])
|
|
1034
|
+
# create a directory for the regscale attachments
|
|
1035
|
+
regscale_dir = os.path.join(directory, "regscale")
|
|
1036
|
+
check_file_path(regscale_dir, False)
|
|
1037
|
+
# download regscale attachments to the directory
|
|
1038
|
+
for attachment in regscale_issue_attachments:
|
|
1039
|
+
with open(os.path.join(regscale_dir, attachment.trustedDisplayName), "wb") as file:
|
|
1040
|
+
file.write(
|
|
1041
|
+
File.download_file_from_regscale_to_memory(
|
|
1042
|
+
api=api,
|
|
1043
|
+
record_id=regscale_issue["id"],
|
|
1044
|
+
module="issues",
|
|
1045
|
+
stored_name=attachment.trustedStorageName,
|
|
1046
|
+
file_hash=(attachment.fileHash if attachment.fileHash else attachment.shaHash),
|
|
1047
|
+
)
|
|
1048
|
+
)
|
|
1049
|
+
return snow_dir, regscale_dir
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def upload_files_to_regscale(
|
|
1053
|
+
snow_attachment_hashes: dict,
|
|
1054
|
+
regscale_attachment_hashes: dict,
|
|
1055
|
+
regscale_issue: dict,
|
|
1056
|
+
api: Api,
|
|
1057
|
+
regscale_uploaded_attachments: list,
|
|
1058
|
+
) -> None:
|
|
1059
|
+
"""
|
|
1060
|
+
Upload files to RegScale
|
|
1061
|
+
|
|
1062
|
+
:param dict snow_attachment_hashes: Dictionary of SNOW attachment hashes
|
|
1063
|
+
:param dict regscale_attachment_hashes: Dictionary of RegScale attachment hashes
|
|
1064
|
+
:param dict regscale_issue: RegScale issue to upload the attachments to
|
|
1065
|
+
:param Api api: Api object to use for interacting with RegScale
|
|
1066
|
+
:param list regscale_uploaded_attachments: List of RegScale attachments that were uploaded
|
|
1067
|
+
:rtype: None
|
|
1068
|
+
:return: None
|
|
1069
|
+
"""
|
|
1070
|
+
for file_hash, file in snow_attachment_hashes.items():
|
|
1071
|
+
if file_hash not in regscale_attachment_hashes:
|
|
1072
|
+
with open(file, "rb") as in_file:
|
|
1073
|
+
path_file = Path(file)
|
|
1074
|
+
if File.upload_file_to_regscale(
|
|
1075
|
+
file_name=f"ServiceNow_attachment_{path_file.name}",
|
|
1076
|
+
parent_id=regscale_issue["id"],
|
|
1077
|
+
parent_module="issues",
|
|
1078
|
+
api=api,
|
|
1079
|
+
file_data=in_file.read(),
|
|
1080
|
+
):
|
|
1081
|
+
regscale_uploaded_attachments.append(file)
|
|
1082
|
+
logger.debug(
|
|
1083
|
+
"Uploaded %s to RegScale issue #%i.",
|
|
1084
|
+
path_file.name,
|
|
1085
|
+
regscale_issue["id"],
|
|
1086
|
+
)
|
|
1087
|
+
else:
|
|
1088
|
+
logger.warning(
|
|
1089
|
+
"Unable to upload %s to RegScale issue #%i.",
|
|
1090
|
+
path_file.name,
|
|
1091
|
+
regscale_issue["id"],
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def log_upload_results(
|
|
1096
|
+
regscale_uploaded_attachments: list, snow_uploaded_attachments: list, regscale_issue: dict, snow_issue: dict
|
|
1097
|
+
) -> None:
|
|
1098
|
+
"""
|
|
1099
|
+
Log the results of the upload process
|
|
1100
|
+
|
|
1101
|
+
:param list regscale_uploaded_attachments: List of RegScale attachments that were uploaded
|
|
1102
|
+
:param list snow_uploaded_attachments: List of Snow attachments that were uploaded
|
|
1103
|
+
:param dict regscale_issue: RegScale issue that the attachments were uploaded to
|
|
1104
|
+
:param dict snow_issue: SNOW issue that the attachments were uploaded to
|
|
1105
|
+
:rtype: None
|
|
1106
|
+
"""
|
|
1107
|
+
if regscale_uploaded_attachments and snow_uploaded_attachments:
|
|
1108
|
+
logger.info(
|
|
1109
|
+
"%i file(s) uploaded to RegScale issue #%i and %i file(s) uploaded to ServiceNow incident %s.",
|
|
1110
|
+
len(regscale_uploaded_attachments),
|
|
1111
|
+
regscale_issue["id"],
|
|
1112
|
+
len(snow_uploaded_attachments),
|
|
1113
|
+
snow_issue["number"],
|
|
1114
|
+
)
|
|
1115
|
+
elif snow_uploaded_attachments:
|
|
1116
|
+
logger.info(
|
|
1117
|
+
"%i file(s) uploaded to ServiceNow incident %s.",
|
|
1118
|
+
len(snow_uploaded_attachments),
|
|
1119
|
+
snow_issue["number"],
|
|
1120
|
+
)
|
|
1121
|
+
elif regscale_uploaded_attachments:
|
|
1122
|
+
logger.info(
|
|
1123
|
+
"%i file(s) uploaded to RegScale issue #%i.",
|
|
1124
|
+
len(regscale_uploaded_attachments),
|
|
1125
|
+
regscale_issue["id"],
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def sync_snow_incidents_to_regscale_issues(
|
|
1130
|
+
incidents: list[dict],
|
|
1131
|
+
regscale_issues: list[Issue],
|
|
1132
|
+
sync_attachments: bool,
|
|
1133
|
+
attachments: dict,
|
|
1134
|
+
app: "Application",
|
|
1135
|
+
snow_config: ServiceNowConfig,
|
|
1136
|
+
parent_id: int,
|
|
1137
|
+
parent_module: str,
|
|
1138
|
+
) -> None:
|
|
1139
|
+
"""
|
|
1140
|
+
Sync incidents from ServiceNow to RegScale
|
|
1141
|
+
|
|
1142
|
+
:param list[dict] incidents: List of SNOW incidents to sync to RegScale
|
|
1143
|
+
:param list[Issue] regscale_issues: List of RegScale issues to compare to SNOW Incidents
|
|
1144
|
+
:param bool sync_attachments: Sync attachments from ServieNow to RegScale, defaults to True
|
|
1145
|
+
:param dict attachments: Attachments from RegScale and ServiceNow
|
|
1146
|
+
:param Application app: RegScale CLI application object
|
|
1147
|
+
:param dict snow_config: ServiceNow configuration
|
|
1148
|
+
:param int parent_id: Parent record ID in RegScale
|
|
1149
|
+
:param str parent_module: Parent record module in RegScale
|
|
1150
|
+
:rtype: None
|
|
1151
|
+
"""
|
|
1152
|
+
issues_closed = []
|
|
1153
|
+
with job_progress:
|
|
1154
|
+
creating_issues = job_progress.add_task(
|
|
1155
|
+
f"[#f8b737]Comparing {len(incidents)} ServiceNow incident(s)"
|
|
1156
|
+
f" and {len(regscale_issues)} RegScale issue(s)...",
|
|
1157
|
+
total=len(incidents),
|
|
1158
|
+
)
|
|
1159
|
+
create_threads(
|
|
1160
|
+
process=create_and_update_regscale_issues,
|
|
1161
|
+
args=(
|
|
1162
|
+
incidents,
|
|
1163
|
+
regscale_issues,
|
|
1164
|
+
snow_config,
|
|
1165
|
+
sync_attachments,
|
|
1166
|
+
attachments,
|
|
1167
|
+
app,
|
|
1168
|
+
parent_id,
|
|
1169
|
+
parent_module,
|
|
1170
|
+
creating_issues,
|
|
1171
|
+
job_progress,
|
|
1172
|
+
),
|
|
1173
|
+
thread_count=len(incidents),
|
|
1174
|
+
)
|
|
1175
|
+
logger.info(
|
|
1176
|
+
f"Analyzed {len(incidents)} ServiceNow incidents(s), created {len(new_regscale_objects)} issue(s), "
|
|
1177
|
+
f"updated {len(updated_regscale_objects)} issue(s), and closed {len(issues_closed)} issue(s) in RegScale.",
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def create_and_update_regscale_issues(args: Tuple, thread: int) -> None:
|
|
1182
|
+
"""
|
|
1183
|
+
Function to create or update issues in RegScale from ServiceNow
|
|
1184
|
+
|
|
1185
|
+
:param Tuple args: Tuple of args to use during the process
|
|
1186
|
+
:param int thread: Thread number of current thread
|
|
1187
|
+
:rtype: None
|
|
1188
|
+
"""
|
|
1189
|
+
# set up local variables from the passed args
|
|
1190
|
+
(
|
|
1191
|
+
incidents,
|
|
1192
|
+
regscale_issues,
|
|
1193
|
+
snow_config,
|
|
1194
|
+
add_attachments,
|
|
1195
|
+
attachments,
|
|
1196
|
+
app,
|
|
1197
|
+
parent_id,
|
|
1198
|
+
parent_module,
|
|
1199
|
+
task,
|
|
1200
|
+
progress,
|
|
1201
|
+
) = args
|
|
1202
|
+
# find which records should be executed by the current thread
|
|
1203
|
+
threads = thread_assignment(thread=thread, total_items=len(incidents))
|
|
1204
|
+
# iterate through the thread assignment items and process them
|
|
1205
|
+
for i in range(len(threads)):
|
|
1206
|
+
snow_incident: dict = incidents[threads[i]]
|
|
1207
|
+
regscale_issue: Optional[Issue] = next(
|
|
1208
|
+
(issue for issue in regscale_issues if issue.serviceNowId == snow_incident["number"]), None
|
|
1209
|
+
)
|
|
1210
|
+
data = Data(
|
|
1211
|
+
parentId=0,
|
|
1212
|
+
parentModule=Issue.get_module_string(),
|
|
1213
|
+
dataType="JSON",
|
|
1214
|
+
dataSource=f"ServiceNow Incident #{snow_incident['number']}",
|
|
1215
|
+
rawData=json.dumps(snow_incident),
|
|
1216
|
+
)
|
|
1217
|
+
# see if the incident needs to be created in RegScale
|
|
1218
|
+
if not regscale_issue:
|
|
1219
|
+
# map the SNOW incident to a RegScale issue object
|
|
1220
|
+
issue = map_incident_to_regscale_issue(
|
|
1221
|
+
incident=snow_incident,
|
|
1222
|
+
parent_id=parent_id,
|
|
1223
|
+
parent_module=parent_module,
|
|
1224
|
+
)
|
|
1225
|
+
# create the issue in RegScale
|
|
1226
|
+
if regscale_issue := issue.create():
|
|
1227
|
+
logger.debug(
|
|
1228
|
+
"Created issue #%i-%s in RegScale.",
|
|
1229
|
+
regscale_issue.id,
|
|
1230
|
+
regscale_issue.title,
|
|
1231
|
+
)
|
|
1232
|
+
data.parentId = regscale_issue.id
|
|
1233
|
+
data.create()
|
|
1234
|
+
new_regscale_objects.append(regscale_issue)
|
|
1235
|
+
else:
|
|
1236
|
+
logger.warning("Unable to create issue in RegScale.\nIssue: %s", issue.dict())
|
|
1237
|
+
elif snow_incident["state"].lower() == "closed" and regscale_issue.status not in ["Closed", "Cancelled"]:
|
|
1238
|
+
# update the status and date completed of the RegScale issue
|
|
1239
|
+
regscale_issue.status = "Closed"
|
|
1240
|
+
regscale_issue.dateCompleted = snow_incident["closed_at"]
|
|
1241
|
+
# update the issue in RegScale
|
|
1242
|
+
updated_regscale_objects.append(regscale_issue.save())
|
|
1243
|
+
data.parentId = regscale_issue.id
|
|
1244
|
+
data.create_or_update()
|
|
1245
|
+
elif regscale_issue:
|
|
1246
|
+
# update the issue in RegScale
|
|
1247
|
+
updated_regscale_objects.append(regscale_issue.save())
|
|
1248
|
+
data.parentId = regscale_issue.id
|
|
1249
|
+
data.create_or_update()
|
|
1250
|
+
|
|
1251
|
+
if add_attachments and regscale_issue and snow_incident["sys_id"] in attachments["snow"]:
|
|
1252
|
+
# determine which attachments need to be uploaded to prevent duplicates by
|
|
1253
|
+
# getting the hashes of all SNOW & RegScale attachments
|
|
1254
|
+
compare_files_for_dupes_and_upload(
|
|
1255
|
+
snow_issue=snow_incident,
|
|
1256
|
+
regscale_issue=regscale_issue.model_dump(),
|
|
1257
|
+
snow_config=snow_config,
|
|
1258
|
+
attachments=attachments,
|
|
1259
|
+
)
|
|
1260
|
+
# update progress bar
|
|
1261
|
+
progress.update(task, advance=1)
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def map_incident_to_regscale_issue(incident: dict, parent_id: int, parent_module: str) -> Issue:
|
|
1265
|
+
"""
|
|
1266
|
+
Map a ServiceNow incident to a RegScale issue
|
|
1267
|
+
|
|
1268
|
+
:param dict incident: ServiceNow incident to map to RegScale issue
|
|
1269
|
+
:param int parent_id: Parent record ID in RegScale
|
|
1270
|
+
:param str parent_module: Parent record module in RegScale
|
|
1271
|
+
:return: RegScale issue object
|
|
1272
|
+
:rtype: Issue
|
|
1273
|
+
"""
|
|
1274
|
+
default_due_date = datetime.datetime.now() + datetime.timedelta(days=30)
|
|
1275
|
+
new_issue = Issue(
|
|
1276
|
+
title=incident["short_description"],
|
|
1277
|
+
description=incident["description"],
|
|
1278
|
+
dueDate=incident["due_date"] or default_due_date.strftime("%Y-%m-%d %H:%M:%S"),
|
|
1279
|
+
parentId=parent_id,
|
|
1280
|
+
parentModule=parent_module,
|
|
1281
|
+
serviceNowId=incident["number"],
|
|
1282
|
+
status="Closed" if incident["state"].lower() == "closed" else "Open",
|
|
1283
|
+
severityLevel=Issue.assign_severity(incident["priority"].split(" ")[-1]),
|
|
1284
|
+
)
|
|
1285
|
+
# correct the status if it is canceled
|
|
1286
|
+
if incident["state"].lower() == "canceled":
|
|
1287
|
+
new_issue.status = "Cancelled"
|
|
1288
|
+
if new_issue.status in ["Closed", "Cancelled"]:
|
|
1289
|
+
new_issue.dateCompleted = incident.get("closed_at", get_current_datetime())
|
|
1290
|
+
return new_issue
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
def get_snow_attachment_metadata(snow_config: ServiceNowConfig) -> dict[str, list[dict]]:
|
|
1294
|
+
"""
|
|
1295
|
+
Get attachments for a ServiceNow incident
|
|
1296
|
+
|
|
1297
|
+
:param ServiceNowConfig snow_config: ServiceNow's configuration object
|
|
1298
|
+
:return: Dictionary of attachments with table_sys_id as the key and the attachments as the value
|
|
1299
|
+
:rtype: dict[str, list[dict]]
|
|
1300
|
+
"""
|
|
1301
|
+
snow_api = snow_config.api
|
|
1302
|
+
attachment_url = urljoin(snow_config.url, "api/now/attachment")
|
|
1303
|
+
offset = 0
|
|
1304
|
+
limit = 500
|
|
1305
|
+
data = []
|
|
1306
|
+
sorted_data = {}
|
|
1307
|
+
|
|
1308
|
+
while True:
|
|
1309
|
+
result, offset = query_service_now(
|
|
1310
|
+
api=snow_api,
|
|
1311
|
+
snow_url=attachment_url,
|
|
1312
|
+
offset=offset,
|
|
1313
|
+
limit=limit,
|
|
1314
|
+
query="&table_name=incident",
|
|
1315
|
+
)
|
|
1316
|
+
data += result
|
|
1317
|
+
if not result:
|
|
1318
|
+
break
|
|
1319
|
+
for item in data:
|
|
1320
|
+
key = item["table_sys_id"]
|
|
1321
|
+
if key in sorted_data:
|
|
1322
|
+
sorted_data[key].append(item)
|
|
1323
|
+
else:
|
|
1324
|
+
sorted_data[key] = [item]
|
|
1325
|
+
return sorted_data
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def sync_notes_to_regscale(regscale_id: int = None, regscale_module: str = None) -> None:
|
|
1329
|
+
"""
|
|
1330
|
+
Sync work notes from ServiceNow to existing issues
|
|
1331
|
+
|
|
1332
|
+
:param int regscale_id: RegScale record ID
|
|
1333
|
+
:param str regscale_module: RegScale record module
|
|
1334
|
+
:rtype: None
|
|
1335
|
+
"""
|
|
1336
|
+
app = Application()
|
|
1337
|
+
# get secrets
|
|
1338
|
+
snow_config = ServiceNowConfig(reg_config=app.config)
|
|
1339
|
+
query = ""
|
|
1340
|
+
data = get_service_now_incidents(snow_config, query=query)
|
|
1341
|
+
work_notes = get_service_now_work_notes(snow_config, data)
|
|
1342
|
+
work_notes_mapping = {}
|
|
1343
|
+
# change work_notes to a dictionary using the incident id as the key and a list of work notes as the value
|
|
1344
|
+
for work_note in work_notes:
|
|
1345
|
+
key = work_note["element_id"]
|
|
1346
|
+
if key in work_notes_mapping:
|
|
1347
|
+
work_notes_mapping[key].append(work_note)
|
|
1348
|
+
else:
|
|
1349
|
+
work_notes_mapping[key] = [work_note]
|
|
1350
|
+
process_work_notes(
|
|
1351
|
+
data=data,
|
|
1352
|
+
work_notes_mapping=work_notes_mapping,
|
|
1353
|
+
regscale_id=regscale_id,
|
|
1354
|
+
regscale_module=regscale_module,
|
|
1355
|
+
)
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
def get_service_now_work_notes(snow_config: ServiceNowConfig, incidents: list) -> list:
|
|
1359
|
+
"""
|
|
1360
|
+
Get all work notes from ServiceNow
|
|
1361
|
+
|
|
1362
|
+
:param ServiceNowConfig snow_config: ServiceNow's configuration dictionary
|
|
1363
|
+
:param list incidents: List of incidents from ServiceNow
|
|
1364
|
+
:return: List of work notes
|
|
1365
|
+
:rtype: list
|
|
1366
|
+
"""
|
|
1367
|
+
snow_api = snow_config.api
|
|
1368
|
+
work_notes_url = urljoin(snow_config.url, "api/now/table/sys_journal_field")
|
|
1369
|
+
offset = 0
|
|
1370
|
+
limit = 500
|
|
1371
|
+
data = []
|
|
1372
|
+
if sys_ids := [incident["sys_id"] for incident in incidents]:
|
|
1373
|
+
# filter work notes by using the sys_ids, and only get work notes for incidents
|
|
1374
|
+
query = f"&element_idIN{','.join(sys_ids)}&element=work_notes&name=incident"
|
|
1375
|
+
else:
|
|
1376
|
+
query = "element=work_notes"
|
|
1377
|
+
|
|
1378
|
+
while True:
|
|
1379
|
+
result, offset = query_service_now(
|
|
1380
|
+
api=snow_api,
|
|
1381
|
+
snow_url=work_notes_url,
|
|
1382
|
+
offset=offset,
|
|
1383
|
+
limit=limit,
|
|
1384
|
+
query=query,
|
|
1385
|
+
)
|
|
1386
|
+
data += result
|
|
1387
|
+
if not result:
|
|
1388
|
+
break
|
|
1389
|
+
|
|
1390
|
+
return data
|
|
1391
|
+
|
|
1392
|
+
|
|
1393
|
+
def process_work_notes(
|
|
1394
|
+
data: list,
|
|
1395
|
+
work_notes_mapping: dict[str, list[dict]],
|
|
1396
|
+
regscale_id: int = None,
|
|
1397
|
+
regscale_module: str = None,
|
|
1398
|
+
) -> None:
|
|
1399
|
+
"""
|
|
1400
|
+
Process and Sync the ServiceNow work notes to RegScale
|
|
1401
|
+
|
|
1402
|
+
:param list data: list of data from ServiceNow to sync with RegScale
|
|
1403
|
+
:param dict[str, list[dict]] work_notes_mapping: Mapping of work notes from SNOW with the incident sys_id as the key
|
|
1404
|
+
:param int regscale_id: RegScale record ID, defaults to None
|
|
1405
|
+
:param str regscale_module: RegScale record module, defaults to None
|
|
1406
|
+
:rtype: None
|
|
1407
|
+
"""
|
|
1408
|
+
update_issues: list[Issue] = []
|
|
1409
|
+
for dat in track(
|
|
1410
|
+
data,
|
|
1411
|
+
description=f"Processing {len(data):,} ServiceNow incidents",
|
|
1412
|
+
):
|
|
1413
|
+
incident_number = dat["number"]
|
|
1414
|
+
try:
|
|
1415
|
+
if regscale_id and regscale_module:
|
|
1416
|
+
regscale_issues = Issue.get_all_by_parent(regscale_id, regscale_module)
|
|
1417
|
+
else:
|
|
1418
|
+
regscale_issues = Issue.find_by_service_now_id(incident_number)
|
|
1419
|
+
logger.debug("Processing ServiceNow Issue # %s", incident_number)
|
|
1420
|
+
if updated_issue := determine_issue_description(
|
|
1421
|
+
incident=dat,
|
|
1422
|
+
regscale_issues=regscale_issues,
|
|
1423
|
+
work_notes_mapping=work_notes_mapping,
|
|
1424
|
+
):
|
|
1425
|
+
update_issues.append(updated_issue)
|
|
1426
|
+
except requests.HTTPError:
|
|
1427
|
+
logger.warning(
|
|
1428
|
+
"HTTP Error: Unable to find RegScale issue with ServiceNow incident ID of %s.",
|
|
1429
|
+
incident_number,
|
|
1430
|
+
)
|
|
1431
|
+
if len(update_issues) > 0:
|
|
1432
|
+
logger.debug(update_issues)
|
|
1433
|
+
_ = Issue.batch_update(update_issues)
|
|
1434
|
+
else:
|
|
1435
|
+
logger.warning("All ServiceNow work notes are already in RegScale. No updates needed.")
|
|
1436
|
+
sys.exit(0)
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
def determine_issue_description(
|
|
1440
|
+
incident: dict, regscale_issues: List[Issue], work_notes_mapping: dict[str, list[dict]]
|
|
1441
|
+
) -> Optional[Issue]:
|
|
1442
|
+
"""
|
|
1443
|
+
Determine if the issue description needs to be updated
|
|
1444
|
+
|
|
1445
|
+
:param dict incident: ServiceNow incident
|
|
1446
|
+
:param List[Issue] regscale_issues: List of RegScale issues to update the description for
|
|
1447
|
+
:param dict[str, list[dict]] work_notes_mapping: Mapping of work notes from SNOW with the incident sys_id as the key
|
|
1448
|
+
:return: Issue if description needs to be updated
|
|
1449
|
+
"""
|
|
1450
|
+
# legacy SNOW work notes are stored as a string in the incident object, check if it is populated
|
|
1451
|
+
# if not, check the work_notes_mapping for the incident sys_id which will return a list of work notes
|
|
1452
|
+
work_notes = incident.get("work_notes") or work_notes_mapping.get(incident["sys_id"], [])
|
|
1453
|
+
if not work_notes:
|
|
1454
|
+
return None
|
|
1455
|
+
|
|
1456
|
+
for issue in regscale_issues:
|
|
1457
|
+
if issue.serviceNowId != incident["number"]:
|
|
1458
|
+
continue
|
|
1459
|
+
# if work_notes is a list, convert it to a string
|
|
1460
|
+
if isinstance(work_notes, list):
|
|
1461
|
+
work_notes = build_issue_description_from_list(work_notes, issue)
|
|
1462
|
+
if work_notes not in issue.description:
|
|
1463
|
+
logger.info(
|
|
1464
|
+
"Updating work item for RegScale issue # %s and ServiceNow incident " + "# %s.",
|
|
1465
|
+
issue.id,
|
|
1466
|
+
incident["number"],
|
|
1467
|
+
)
|
|
1468
|
+
issue.description = f"<strong>ServiceNow Work Notes: </strong>{work_notes}<br/>" + issue.description
|
|
1469
|
+
return issue
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
def build_issue_description_from_list(work_notes: list[dict], issue: Issue) -> str:
|
|
1473
|
+
"""
|
|
1474
|
+
Build a new description from a list of work notes from ServiceNow
|
|
1475
|
+
|
|
1476
|
+
:param list[dict] work_notes: List of work notes from ServiceNow
|
|
1477
|
+
:param Issue issue: RegScale issue
|
|
1478
|
+
:return: New description
|
|
1479
|
+
:rtype: str
|
|
1480
|
+
"""
|
|
1481
|
+
new_description = ""
|
|
1482
|
+
# if work_notes is a list, convert it to a string
|
|
1483
|
+
for note in work_notes:
|
|
1484
|
+
if note["value"] not in issue.description:
|
|
1485
|
+
new_description += f"<br/>{note['value']}"
|
|
1486
|
+
return new_description
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
def query_service_now(api: Api, snow_url: str, offset: int, limit: int, query: str) -> Tuple[list, int]:
|
|
1490
|
+
"""
|
|
1491
|
+
Paginate through query results
|
|
1492
|
+
|
|
1493
|
+
:param Api api: API object
|
|
1494
|
+
:param str snow_url: URL for ServiceNow incidents
|
|
1495
|
+
:param int offset: Used in URL for ServiceNow API call
|
|
1496
|
+
:param int limit: Used in URL for ServiceNow API call
|
|
1497
|
+
:param str query: Query string for ServiceNow API call
|
|
1498
|
+
:return: Tuple[Result data from API call, offset integer provided]
|
|
1499
|
+
:rtype: Tuple[list, int]
|
|
1500
|
+
"""
|
|
1501
|
+
result = []
|
|
1502
|
+
offset_param = f"&sysparm_offset={offset}"
|
|
1503
|
+
url = urljoin(snow_url, f"?sysparm_limit={limit}{offset_param}{query}")
|
|
1504
|
+
logger.debug(url)
|
|
1505
|
+
response = api.get(url=url, headers=HEADERS)
|
|
1506
|
+
if response.status_code == 200:
|
|
1507
|
+
try:
|
|
1508
|
+
result = response.json().get("result", [])
|
|
1509
|
+
except JSONDecodeError as e:
|
|
1510
|
+
logger.error("Unable to decode JSON: %s\nResponse: %i: %s", e, response.status_code, response.text)
|
|
1511
|
+
else:
|
|
1512
|
+
logger.error(
|
|
1513
|
+
"Unable to query ServiceNow. Status code: %s, Reason: %s",
|
|
1514
|
+
response.status_code,
|
|
1515
|
+
response.reason,
|
|
1516
|
+
)
|
|
1517
|
+
offset += limit
|
|
1518
|
+
logger.debug(len(result))
|
|
1519
|
+
return result, offset
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
def get_service_now_changes(snow_config: ServiceNowConfig, query: str) -> List[dict]:
|
|
1523
|
+
"""
|
|
1524
|
+
Get all change requests from ServiceNow
|
|
1525
|
+
|
|
1526
|
+
:param dict snow_config: ServiceNow configuration
|
|
1527
|
+
:param str query: Query string
|
|
1528
|
+
:return: List of change requests
|
|
1529
|
+
:rtype: List[dict]
|
|
1530
|
+
"""
|
|
1531
|
+
snow_api = snow_config.api
|
|
1532
|
+
changes_url = urljoin(snow_config.url, "api/now/table/change_request")
|
|
1533
|
+
offset = 0
|
|
1534
|
+
limit = 500
|
|
1535
|
+
data = []
|
|
1536
|
+
|
|
1537
|
+
while True:
|
|
1538
|
+
result, offset = query_service_now(
|
|
1539
|
+
api=snow_api,
|
|
1540
|
+
snow_url=changes_url,
|
|
1541
|
+
offset=offset,
|
|
1542
|
+
limit=limit,
|
|
1543
|
+
query=query,
|
|
1544
|
+
)
|
|
1545
|
+
data += result
|
|
1546
|
+
if not result:
|
|
1547
|
+
break
|
|
1548
|
+
|
|
1549
|
+
return data
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
@servicenow.command(name="sync_changes")
|
|
1553
|
+
@click.option(
|
|
1554
|
+
"--start_date",
|
|
1555
|
+
"-s",
|
|
1556
|
+
type=click.DateTime(formats=["%Y-%m-%d"]),
|
|
1557
|
+
help="The start date to query ServiceNow for changes in YYYY-MM-DD format. Defaults to 30 days ago.",
|
|
1558
|
+
required=False,
|
|
1559
|
+
default=datetime.datetime.now() - datetime.timedelta(days=30),
|
|
1560
|
+
)
|
|
1561
|
+
@click.option(
|
|
1562
|
+
"--sync_all_changes",
|
|
1563
|
+
"-all",
|
|
1564
|
+
is_flag=True,
|
|
1565
|
+
help="Whether to Sync all change requests from ServiceNow into RegScale as Changes. Defaults to False.",
|
|
1566
|
+
default=False,
|
|
1567
|
+
)
|
|
1568
|
+
def sync_changes(
|
|
1569
|
+
start_date: datetime.datetime,
|
|
1570
|
+
# sync_attachments: bool,
|
|
1571
|
+
sync_all_changes: bool,
|
|
1572
|
+
):
|
|
1573
|
+
"""Sync change requests from ServiceNow into RegScale as Changes."""
|
|
1574
|
+
sync_snow_changes(
|
|
1575
|
+
start_date=start_date,
|
|
1576
|
+
sync_all_changes=sync_all_changes,
|
|
1577
|
+
)
|
|
1578
|
+
|
|
1579
|
+
|
|
1580
|
+
def sync_snow_changes(
|
|
1581
|
+
start_date: datetime.datetime,
|
|
1582
|
+
sync_all_changes: bool = False,
|
|
1583
|
+
) -> None:
|
|
1584
|
+
"""
|
|
1585
|
+
Sync change requests from ServiceNow into RegScale as Changes
|
|
1586
|
+
|
|
1587
|
+
:param datetime.datetime start_date: The start date to query SNOW for changes, ignored if sync_all_changes is True
|
|
1588
|
+
:param bool sync_all_changes: Whether to sync all change requests from SNOW into RegScale changes
|
|
1589
|
+
:rtype: None
|
|
1590
|
+
"""
|
|
1591
|
+
app = check_license()
|
|
1592
|
+
config = app.config
|
|
1593
|
+
snow_config = ServiceNowConfig(reg_config=config)
|
|
1594
|
+
query = "&sysparm_display_value=true"
|
|
1595
|
+
|
|
1596
|
+
if sync_all_changes:
|
|
1597
|
+
changes = get_service_now_changes(snow_config=snow_config, query=query)
|
|
1598
|
+
else:
|
|
1599
|
+
query += f"&sysparm_query=sys_created_on>={start_date.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
1600
|
+
changes = get_service_now_changes(snow_config=snow_config, query=query)
|
|
1601
|
+
|
|
1602
|
+
logger.info(f"Retrieved {len(changes)} change(s) from ServiceNow.")
|
|
1603
|
+
regscale_changes = Change.fetch_all_changes()
|
|
1604
|
+
|
|
1605
|
+
if changes:
|
|
1606
|
+
sync_snow_changes_to_regscale_issues(
|
|
1607
|
+
changes=changes,
|
|
1608
|
+
regscale_changes=regscale_changes,
|
|
1609
|
+
app=app,
|
|
1610
|
+
snow_config=snow_config,
|
|
1611
|
+
)
|
|
1612
|
+
current_date = get_current_datetime(dt_format="%Y%m%d_%H-%M-%S")
|
|
1613
|
+
# save the snow_changes to a xlsx file
|
|
1614
|
+
save_data_to(
|
|
1615
|
+
file=Path(f"artifacts/snow_changes_{current_date}.xlsx"),
|
|
1616
|
+
data=changes,
|
|
1617
|
+
transpose_data=False,
|
|
1618
|
+
)
|
|
1619
|
+
else:
|
|
1620
|
+
logger.info("No changes need to be analyzed from ServiceNow.")
|
|
1621
|
+
|
|
1622
|
+
|
|
1623
|
+
def sync_snow_changes_to_regscale_issues(
|
|
1624
|
+
changes: list[dict],
|
|
1625
|
+
regscale_changes: list[Change],
|
|
1626
|
+
app: "Application",
|
|
1627
|
+
snow_config: ServiceNowConfig,
|
|
1628
|
+
) -> None:
|
|
1629
|
+
"""
|
|
1630
|
+
Sync incidents from ServiceNow to RegScale
|
|
1631
|
+
|
|
1632
|
+
:param list[dict] changes: List of SNOW incidents to sync to RegScale
|
|
1633
|
+
:param list[Change] regscale_changes: List of RegScale issues to compare to SNOW Incidents
|
|
1634
|
+
:param Application app: RegScale CLI application object
|
|
1635
|
+
:param dict snow_config: ServiceNow configuration
|
|
1636
|
+
:rtype: None
|
|
1637
|
+
"""
|
|
1638
|
+
issues_closed = []
|
|
1639
|
+
with job_progress:
|
|
1640
|
+
creating_issues = job_progress.add_task(
|
|
1641
|
+
f"[#f8b737]Comparing {len(changes)} ServiceNow change(s)"
|
|
1642
|
+
f" and {len(regscale_changes)} RegScale change(s)...",
|
|
1643
|
+
total=len(changes),
|
|
1644
|
+
)
|
|
1645
|
+
app.thread_manager.submit_tasks_from_list(
|
|
1646
|
+
func=create_and_update_regscale_changes,
|
|
1647
|
+
items=changes,
|
|
1648
|
+
args=(
|
|
1649
|
+
changes,
|
|
1650
|
+
regscale_changes,
|
|
1651
|
+
snow_config,
|
|
1652
|
+
app,
|
|
1653
|
+
creating_issues,
|
|
1654
|
+
job_progress,
|
|
1655
|
+
),
|
|
1656
|
+
)
|
|
1657
|
+
_ = app.thread_manager.execute_and_verify(terminate_after=True)
|
|
1658
|
+
logger.info(
|
|
1659
|
+
f"Analyzed {len(changes)} ServiceNow change(s), created {len(new_regscale_objects)} change(s), "
|
|
1660
|
+
f"updated {len(updated_regscale_objects)} change(s), and closed {len(issues_closed)} change(s) in RegScale.",
|
|
1661
|
+
)
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
def create_and_update_regscale_changes(snow_change: dict, args: Tuple) -> None:
|
|
1665
|
+
"""
|
|
1666
|
+
Function to create or update changes in RegScale from ServiceNow
|
|
1667
|
+
|
|
1668
|
+
:param dict snow_change: ServiceNow change request object
|
|
1669
|
+
:param Tuple args: Tuple of args to use during the process
|
|
1670
|
+
:rtype: None
|
|
1671
|
+
"""
|
|
1672
|
+
# set up local variables from the passed args
|
|
1673
|
+
(
|
|
1674
|
+
snow_changes,
|
|
1675
|
+
regscale_changes,
|
|
1676
|
+
snow_config,
|
|
1677
|
+
app,
|
|
1678
|
+
task,
|
|
1679
|
+
progress,
|
|
1680
|
+
) = args
|
|
1681
|
+
regscale_change: Optional[Change] = next(
|
|
1682
|
+
(change for change in regscale_changes if snow_change["number"] in change.title), None
|
|
1683
|
+
)
|
|
1684
|
+
change = map_snow_change_to_regscale_change(
|
|
1685
|
+
change=snow_change,
|
|
1686
|
+
)
|
|
1687
|
+
if regscale_change:
|
|
1688
|
+
change.id = regscale_change.id
|
|
1689
|
+
change.save()
|
|
1690
|
+
updated_regscale_objects.append(change)
|
|
1691
|
+
else:
|
|
1692
|
+
new_change = change.create()
|
|
1693
|
+
new_regscale_objects.append(new_change)
|
|
1694
|
+
change = new_change
|
|
1695
|
+
_ = Data(
|
|
1696
|
+
parentId=change.id,
|
|
1697
|
+
parentModule=Change.get_module_string(),
|
|
1698
|
+
dataType="JSON",
|
|
1699
|
+
dataSource=f"ServiceNow Change #{snow_change['number']}",
|
|
1700
|
+
rawData=json.dumps(snow_change),
|
|
1701
|
+
).create_or_update()
|
|
1702
|
+
progress.update(task, advance=1)
|
|
1703
|
+
|
|
1704
|
+
|
|
1705
|
+
def map_snow_change_to_regscale_change(change: dict) -> Change:
|
|
1706
|
+
"""
|
|
1707
|
+
Map a ServiceNow change request to a RegScale change record
|
|
1708
|
+
|
|
1709
|
+
:param dict change: ServiceNow change request to map to RegScale change object
|
|
1710
|
+
:return: RegScale change object
|
|
1711
|
+
:rtype: Change
|
|
1712
|
+
"""
|
|
1713
|
+
from regscale.models.regscale_models.change import ChangePriority, ChangeStatus, ChangeType
|
|
1714
|
+
|
|
1715
|
+
status_map = {
|
|
1716
|
+
"Approved": ChangeStatus.approved.value,
|
|
1717
|
+
"Not Requested": ChangeStatus.draft.value,
|
|
1718
|
+
"Authorize": ChangeStatus.pending_approval.value,
|
|
1719
|
+
"Closed": ChangeStatus.complete.value,
|
|
1720
|
+
"Canceled": ChangeStatus.cancelled.value,
|
|
1721
|
+
}
|
|
1722
|
+
priority_map = {
|
|
1723
|
+
"1 - Critical": ChangePriority.critical.value,
|
|
1724
|
+
"2 - High": ChangePriority.high.value,
|
|
1725
|
+
"3 - Moderate": ChangePriority.moderate.value,
|
|
1726
|
+
"4 - Low": ChangePriority.low.value,
|
|
1727
|
+
}
|
|
1728
|
+
change_type_map = {
|
|
1729
|
+
"Standard": ChangeType.standard.value,
|
|
1730
|
+
"Emergency": ChangeType.emergency.value,
|
|
1731
|
+
"Normal": ChangeType.normal.value,
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
regscale_change = Change(
|
|
1735
|
+
title=f'{change["short_description"]} #{change["number"]}',
|
|
1736
|
+
description=change["description"],
|
|
1737
|
+
changeReason=change.get("reason") or "No reason provided.",
|
|
1738
|
+
dateRequested=change["sys_created_on"],
|
|
1739
|
+
startChangeWindow=change.get("start_date") or change.get("opened_at"),
|
|
1740
|
+
endChangeWindow=change.get("end_date"),
|
|
1741
|
+
dateWorkCompleted=change.get("work_end") or change.get("closed_at"),
|
|
1742
|
+
outageRequired="No",
|
|
1743
|
+
priority=priority_map.get(change["priority"], ChangePriority.moderate.value),
|
|
1744
|
+
changeType=change_type_map.get(change.get("type", "Normal")),
|
|
1745
|
+
status=status_map.get(change["state"], ChangeStatus.draft.value),
|
|
1746
|
+
changePlan=change.get("implementation_plan"),
|
|
1747
|
+
riskAssessment=change.get("risk_impact_analysis"),
|
|
1748
|
+
rollbackPlan=change.get("backout_plan"),
|
|
1749
|
+
testPlan=change.get("test_plan"),
|
|
1750
|
+
notes=change.get("comments_and_work_notes"),
|
|
1751
|
+
securityImpactAssessment=change.get("impact"),
|
|
1752
|
+
)
|
|
1753
|
+
if regscale_change.dateWorkCompleted and regscale_change.status != ChangeStatus.complete.value:
|
|
1754
|
+
regscale_change.dateWorkCompleted = None
|
|
1755
|
+
|
|
1756
|
+
return regscale_change
|