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,1378 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""2nd iteration to add functionality to upgrade application catalog information via API."""
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from os import path
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, List, Optional, Union
|
|
12
|
+
from urllib.parse import urljoin
|
|
13
|
+
|
|
14
|
+
from requests import Response
|
|
15
|
+
from rich.progress import track
|
|
16
|
+
|
|
17
|
+
from regscale.core.app import create_logger
|
|
18
|
+
from regscale.core.app.api import Api
|
|
19
|
+
from regscale.core.app.utils.app_utils import error_and_exit
|
|
20
|
+
|
|
21
|
+
SECURITY_CONTROL = "security control"
|
|
22
|
+
logger = create_logger()
|
|
23
|
+
API_SECURITY_CONTROLS_ = "api/SecurityControls/"
|
|
24
|
+
API_CATALOGUES_ = "api/catalogues/"
|
|
25
|
+
master_catalog_list_url = "https://regscaleblob.blob.core.windows.net/catalogs/catalog_registry.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def display_menu() -> None:
|
|
29
|
+
"""
|
|
30
|
+
Initial function called by the click command. Fires off functions to collect data for comparison and trigger updates
|
|
31
|
+
|
|
32
|
+
:rtype: None
|
|
33
|
+
"""
|
|
34
|
+
api = Api()
|
|
35
|
+
catalog_number_to_update = select_installed_catalogs(api)
|
|
36
|
+
update_sourcefile = get_update_file(api, catalog_number_to_update)
|
|
37
|
+
new_version_catalog_data = load_updated_catalog(update_sourcefile)
|
|
38
|
+
existing_catalog_data = load_existing_catalog(api, catalog_number_to_update)
|
|
39
|
+
dryrun = confirm_actions(new_version_catalog_data, existing_catalog_data)
|
|
40
|
+
process_catalog_update(
|
|
41
|
+
api=api,
|
|
42
|
+
new_version_catalog=new_version_catalog_data,
|
|
43
|
+
existing_catalog=existing_catalog_data,
|
|
44
|
+
dryrun=dryrun,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def import_catalog(catalog_path: Path) -> Response:
|
|
49
|
+
"""
|
|
50
|
+
Import a RegScale catalog from a json file. This file must be formatted as a RegScale catalog
|
|
51
|
+
|
|
52
|
+
:param Path catalog_path: Path to the catalog file to be imported
|
|
53
|
+
:return: Response from API call
|
|
54
|
+
:rtype: Response
|
|
55
|
+
"""
|
|
56
|
+
api = Api()
|
|
57
|
+
file_headers = {
|
|
58
|
+
"Authorization": api.config["token"],
|
|
59
|
+
"Accept": "application/json, text/plain, */*",
|
|
60
|
+
}
|
|
61
|
+
# increase the api timeout to 120 seconds
|
|
62
|
+
if api.timeout < 120:
|
|
63
|
+
api.timeout = 120
|
|
64
|
+
# set the files up for the RegScale API Call
|
|
65
|
+
with open(catalog_path, "rb") as file:
|
|
66
|
+
files = [
|
|
67
|
+
(
|
|
68
|
+
"file",
|
|
69
|
+
(
|
|
70
|
+
catalog_path.name,
|
|
71
|
+
file.read(),
|
|
72
|
+
"application/json",
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
]
|
|
76
|
+
response = api.post(
|
|
77
|
+
urljoin(api.config["domain"], API_CATALOGUES_ + "/import"),
|
|
78
|
+
headers=file_headers,
|
|
79
|
+
files=files,
|
|
80
|
+
data={},
|
|
81
|
+
)
|
|
82
|
+
if response.status_code == 401:
|
|
83
|
+
error_and_exit(f"Invalid authorization token. Unable to proceed. {catalog_path.name}")
|
|
84
|
+
elif response.status_code == 400 and response.text == "Catalog already exists":
|
|
85
|
+
api.logger.warning(f"Skipping {catalog_path.name} as it is already installed.")
|
|
86
|
+
elif not response.ok:
|
|
87
|
+
error_and_exit(
|
|
88
|
+
f"Unexpected response from server. Unable to upload {catalog_path.name}."
|
|
89
|
+
f"\n{response.status_code}: {response.reason}\n{response.text}"
|
|
90
|
+
)
|
|
91
|
+
return response
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def select_installed_catalogs(api: Api) -> int:
|
|
95
|
+
"""
|
|
96
|
+
Fetches the list of currently installed catalogs on the target RegScale installation so user can select for update
|
|
97
|
+
|
|
98
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
99
|
+
:return: catalog number on the target intallation that user has selected for update
|
|
100
|
+
:rtype: int
|
|
101
|
+
"""
|
|
102
|
+
response = api.get(
|
|
103
|
+
urljoin(api.config["domain"], "api/catalogues/getList")
|
|
104
|
+
) # returns catalog IDs & titles on target system
|
|
105
|
+
if response.status_code == 401:
|
|
106
|
+
error_and_exit("Invalid authorization token. Unable to proceed.")
|
|
107
|
+
elif not response.ok:
|
|
108
|
+
error_and_exit("Unexpected response from server. Unable to proceed.")
|
|
109
|
+
catalogs = json.loads(response.content)
|
|
110
|
+
print("The following catalogs are currently available on your RegScale installation:\n")
|
|
111
|
+
ids = sorted([x["id"] for x in catalogs])
|
|
112
|
+
while True:
|
|
113
|
+
for catalog in catalogs:
|
|
114
|
+
print(str(catalog["id"]).rjust(10, " ") + ": " + catalog["title"])
|
|
115
|
+
catalog_number_to_update = input(
|
|
116
|
+
"\nEnter the # of the catalog you wish to update on your target system, or type STOP to exit: "
|
|
117
|
+
)
|
|
118
|
+
if catalog_number_to_update.isdigit() and int(catalog_number_to_update) in ids:
|
|
119
|
+
return int(catalog_number_to_update)
|
|
120
|
+
elif catalog_number_to_update.lower() == "stop":
|
|
121
|
+
logger.info("Exiting program. Goodbye!")
|
|
122
|
+
else:
|
|
123
|
+
logger.warning("\nNot a valid catalog ID number. Please try again:\n")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_update_file(api: Api, catalog_number_to_update: int) -> Union[str, bytes]:
|
|
127
|
+
"""
|
|
128
|
+
Retrieves the source file for the catalog update file source, whether online or by file on disk
|
|
129
|
+
|
|
130
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
131
|
+
:param int catalog_number_to_update: The localized catalog integer ID to be updated
|
|
132
|
+
on the local RegScale installation
|
|
133
|
+
:return: catalog update source file as a string or bytes
|
|
134
|
+
:rtype: Union[str, bytes]
|
|
135
|
+
"""
|
|
136
|
+
response = api.get(urljoin(api.config["domain"], API_CATALOGUES_ + str(catalog_number_to_update)))
|
|
137
|
+
uuid = json.loads(response.content)["uuid"]
|
|
138
|
+
while True:
|
|
139
|
+
update_sourcefile = input(
|
|
140
|
+
"\nEnter the filepath and name of the new version of the catalog file you wish to use,\n or "
|
|
141
|
+
"press ENTER to automatically pull the latest version from RegScale servers: "
|
|
142
|
+
)
|
|
143
|
+
if update_sourcefile.lower() == "stop":
|
|
144
|
+
logger.info("Exiting program. Goodbye!")
|
|
145
|
+
sys.exit(0)
|
|
146
|
+
elif update_sourcefile == "":
|
|
147
|
+
logger.info("Checking online for latest file version..")
|
|
148
|
+
return find_update_online(api, uuid)
|
|
149
|
+
elif path.isfile(update_sourcefile):
|
|
150
|
+
logger.info("Located input file.")
|
|
151
|
+
return read_update_from_disk(update_sourcefile)
|
|
152
|
+
else:
|
|
153
|
+
logger.warning("\nNot a valid source input. Type 'STOP' to exit, or make a valid entry.")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def find_update_online(api: Api, uuid: str) -> bytes:
|
|
157
|
+
"""
|
|
158
|
+
Receives the UUID of the original catalog that is to be updated, and searches for a matching uuid from the master
|
|
159
|
+
catalog list found on the anonymous read azure blob storage
|
|
160
|
+
|
|
161
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
162
|
+
:param str uuid: uuid string from the original catalog, used to find a matching source for update
|
|
163
|
+
:return: byte string of update source catalog file retrieved online from azure blob storage
|
|
164
|
+
:rtype: bytes
|
|
165
|
+
"""
|
|
166
|
+
response = api.get(url=master_catalog_list_url, headers={})
|
|
167
|
+
master_catalogs = json.loads(response.text)
|
|
168
|
+
for catalog in master_catalogs["catalogs"]:
|
|
169
|
+
if catalog["uuid"] == uuid:
|
|
170
|
+
logger.info("Found current version of catalog. Downloading now.")
|
|
171
|
+
return api.get(catalog["downloadURL"], headers={}).content
|
|
172
|
+
error_and_exit(
|
|
173
|
+
"Problem locating a matching catalog. Please contact customer service or try downloading the current catalog "
|
|
174
|
+
"file from our website: https://regscale.com/regulations/"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def read_update_from_disk(update_sourcefile: str) -> bytes:
|
|
179
|
+
"""
|
|
180
|
+
Reads the catalog update source file from disk
|
|
181
|
+
|
|
182
|
+
:param str update_sourcefile: filepath of current catalog version if reading from disk instead of retrieving online
|
|
183
|
+
:return: bytes of json file catalog contents
|
|
184
|
+
:rtype: bytes
|
|
185
|
+
"""
|
|
186
|
+
logger.info("Loading new version of catalog.")
|
|
187
|
+
try:
|
|
188
|
+
with open(update_sourcefile, "rb") as json_file:
|
|
189
|
+
# Read the content of the JSON file & return
|
|
190
|
+
return json_file.read()
|
|
191
|
+
except Exception as e:
|
|
192
|
+
error_and_exit(f"Error encountered when trying to read {update_sourcefile}. Unable to continue: {e}")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def load_updated_catalog(update_source: Union[str, bytes]) -> dict:
|
|
196
|
+
"""
|
|
197
|
+
This function translates the json dict string of update catalog source file to a JSON dict. As of 10/31/2023, the
|
|
198
|
+
catalogs are still possibly in two different formats, so there is additional logic to get to the same end either way
|
|
199
|
+
|
|
200
|
+
:param Union[str, bytes] update_source: a byte string which has previously been either
|
|
201
|
+
read from disk or retrieved by requests
|
|
202
|
+
:return: The update source (current version catalog)
|
|
203
|
+
:rtype: dict
|
|
204
|
+
"""
|
|
205
|
+
updated_catalog = json.loads(update_source)
|
|
206
|
+
logger.info("Loading new version of catalog.")
|
|
207
|
+
try:
|
|
208
|
+
if "securityControls" in updated_catalog: # if current catalog format
|
|
209
|
+
return updated_catalog
|
|
210
|
+
# TODO: Go back and reformat all the legacy catalogs so I can get rid of this hacky stuff
|
|
211
|
+
else: # if this is old format of catalog
|
|
212
|
+
new_format_updated_catalog = {}
|
|
213
|
+
for key in updated_catalog["catalog"].keys():
|
|
214
|
+
new_format_updated_catalog[key] = updated_catalog["catalog"][key]
|
|
215
|
+
return new_format_updated_catalog
|
|
216
|
+
except Exception as e:
|
|
217
|
+
error_and_exit(f"Error encountered. Unable to continue: {e}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def load_existing_catalog(api: Api, catalog_number_to_update: int) -> dict:
|
|
221
|
+
"""
|
|
222
|
+
Loads the existing catalog in the database that is intended to be replaced, matching format of new catalog ingest
|
|
223
|
+
|
|
224
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
225
|
+
:param int catalog_number_to_update: RegScale catalog ID to be updated in the local RegScale installation
|
|
226
|
+
:return: Dict containing the entire catalog structure in same format as catalog import files
|
|
227
|
+
:rtype: dict
|
|
228
|
+
"""
|
|
229
|
+
# TODO: When get parameters/tests/cci by catalog API endpoint is created, should update this logic to speed up
|
|
230
|
+
logger.info("Loading data from existing version of catalog on RegScale installation.")
|
|
231
|
+
existing_catalog = api.get(urljoin(api.config["domain"], API_CATALOGUES_ + str(catalog_number_to_update))).json()
|
|
232
|
+
# Controls
|
|
233
|
+
# returns a list of abbreviated records
|
|
234
|
+
controls = api.get(f"{api.config['domain']}/api/SecurityControls/getList/{catalog_number_to_update}").json()
|
|
235
|
+
logger.info("Fetching %i security control(s) from RegScale...", len(controls))
|
|
236
|
+
controls_list = []
|
|
237
|
+
for control in track(
|
|
238
|
+
controls,
|
|
239
|
+
description=f"Fetching {len(controls)} security control(s) from RegScale...",
|
|
240
|
+
):
|
|
241
|
+
# returns complete record
|
|
242
|
+
control = api.get(urljoin(api.config["domain"], API_SECURITY_CONTROLS_ + str(control["id"]))).json()
|
|
243
|
+
if "objectives" in control: # currently is a bug where api get endpoint returning empty lists for these
|
|
244
|
+
del control["objectives"]
|
|
245
|
+
if "parameters" in control:
|
|
246
|
+
del control["parameters"]
|
|
247
|
+
if "tests" in control:
|
|
248
|
+
del control["tests"]
|
|
249
|
+
if "ccis" in control:
|
|
250
|
+
del control["ccis"]
|
|
251
|
+
controls_list.append(control)
|
|
252
|
+
existing_catalog["securityControls"] = controls_list
|
|
253
|
+
# Objectives
|
|
254
|
+
existing_catalog["objectives"] = api.get(
|
|
255
|
+
urljoin(
|
|
256
|
+
api.config["domain"],
|
|
257
|
+
f"api/controlObjectives/getByCatalog/{catalog_number_to_update}",
|
|
258
|
+
)
|
|
259
|
+
).json()
|
|
260
|
+
logger.info("Received %i objective(s) from RegScale...", len(existing_catalog["objectives"]))
|
|
261
|
+
|
|
262
|
+
control_parameters: List[dict] = []
|
|
263
|
+
control_test_plans: List[dict] = []
|
|
264
|
+
control_cci: List[dict] = []
|
|
265
|
+
logger.info(
|
|
266
|
+
"Loading Parameters, Tests, and checking for CCIs for %s security control(s)...",
|
|
267
|
+
len(existing_catalog["securityControls"]),
|
|
268
|
+
)
|
|
269
|
+
for control in track(
|
|
270
|
+
existing_catalog["securityControls"],
|
|
271
|
+
description=f"Loading Parameters, Tests, and checking for CCIs for "
|
|
272
|
+
f"{len(existing_catalog['securityControls'])} security control(s)...",
|
|
273
|
+
):
|
|
274
|
+
# Parameters
|
|
275
|
+
more_parameters = api.get(
|
|
276
|
+
urljoin(
|
|
277
|
+
api.config["domain"],
|
|
278
|
+
f"api/controlParameters/getByControl/{control['id']}",
|
|
279
|
+
)
|
|
280
|
+
).json()
|
|
281
|
+
control_parameters = control_parameters + more_parameters
|
|
282
|
+
# Tests
|
|
283
|
+
more_tests = api.get(
|
|
284
|
+
urljoin(
|
|
285
|
+
api.config["domain"],
|
|
286
|
+
f"api/controlTestPlans/getByControl/{control['id']}",
|
|
287
|
+
)
|
|
288
|
+
).json()
|
|
289
|
+
control_test_plans = control_test_plans + more_tests
|
|
290
|
+
# CCIs
|
|
291
|
+
more_ccis = api.get(urljoin(api.config["domain"], f"api/cci/getByControl/{control['id']}")).json()
|
|
292
|
+
control_cci = control_cci + more_ccis
|
|
293
|
+
|
|
294
|
+
existing_catalog["parameters"] = control_parameters
|
|
295
|
+
existing_catalog["tests"] = control_test_plans
|
|
296
|
+
existing_catalog["ccis"] = control_cci
|
|
297
|
+
|
|
298
|
+
return existing_catalog
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def confirm_actions(new_version_catalog: dict, existing_catalog: dict) -> bool:
|
|
302
|
+
"""
|
|
303
|
+
Display title of existing catalog and update source for confirmation. Also determines if doing a dry run is true
|
|
304
|
+
or false
|
|
305
|
+
|
|
306
|
+
:param dict new_version_catalog: catalog used as update source
|
|
307
|
+
:param dict existing_catalog: existing catalog pulled from target installation
|
|
308
|
+
:return: True for do a dry run or false to NOT do a dry run (make updates for real)
|
|
309
|
+
:rtype: bool
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
logger.info(
|
|
313
|
+
f"Updating: {str(existing_catalog['id']).rjust(8, ' ')} - {existing_catalog['title']}"
|
|
314
|
+
f"\nWith: {''.rjust(8, ' ')} {new_version_catalog['title']} (Latest Version)"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
logger.info(
|
|
318
|
+
"It is possible to do a dry run. A dry run will report any changes found without updating the data in RegScale."
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
while True:
|
|
322
|
+
proceed = input(
|
|
323
|
+
"Would you like to proceed with updating your catalog? Enter 'Y' to proceed, 'N' to do a dry run, or 'STOP'"
|
|
324
|
+
"to cancel this program: "
|
|
325
|
+
)
|
|
326
|
+
if proceed.lower() == "n":
|
|
327
|
+
return True
|
|
328
|
+
elif proceed.lower() == "y":
|
|
329
|
+
return False
|
|
330
|
+
elif proceed.lower() == "stop":
|
|
331
|
+
logger.info("Ending Program.")
|
|
332
|
+
sys.exit(0)
|
|
333
|
+
else:
|
|
334
|
+
logger.warning("Not a valid selection. Please try again.")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def process_catalog_update(api: Api, new_version_catalog: dict, existing_catalog: dict, dryrun: bool) -> None:
|
|
338
|
+
"""
|
|
339
|
+
Initiates catalog update checks and processing on each record type within the catalog.
|
|
340
|
+
|
|
341
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
342
|
+
:param dict new_version_catalog: update source catalog
|
|
343
|
+
:param dict existing_catalog: existing catalog to be updated, pulled from RegScale installation
|
|
344
|
+
:param bool dryrun: True if a dry run (don't do updates for real, just report changes) or false (do updates)
|
|
345
|
+
:rtype: None
|
|
346
|
+
"""
|
|
347
|
+
output_filename = f"catalog_{existing_catalog['id']}_updates_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.csv"
|
|
348
|
+
track_changes = []
|
|
349
|
+
archived_controls = check_controls(
|
|
350
|
+
api=api,
|
|
351
|
+
existing_controls=existing_catalog["securityControls"],
|
|
352
|
+
new_controls=new_version_catalog["securityControls"],
|
|
353
|
+
track_changes=track_changes,
|
|
354
|
+
dryrun=dryrun,
|
|
355
|
+
)
|
|
356
|
+
if "objectives" in existing_catalog:
|
|
357
|
+
check_child_records(
|
|
358
|
+
api=api,
|
|
359
|
+
archived_controls=archived_controls,
|
|
360
|
+
existing_records=existing_catalog["objectives"],
|
|
361
|
+
new_records=new_version_catalog["objectives"],
|
|
362
|
+
track_changes=track_changes,
|
|
363
|
+
dryrun=dryrun,
|
|
364
|
+
record_type="objective",
|
|
365
|
+
record_id_field="name",
|
|
366
|
+
endpoint="api/ControlObjectives",
|
|
367
|
+
existing_controls=existing_catalog["securityControls"],
|
|
368
|
+
new_controls=new_version_catalog["securityControls"],
|
|
369
|
+
)
|
|
370
|
+
if "parameters" in existing_catalog:
|
|
371
|
+
check_child_records(
|
|
372
|
+
api=api,
|
|
373
|
+
archived_controls=archived_controls,
|
|
374
|
+
existing_records=existing_catalog["parameters"],
|
|
375
|
+
new_records=new_version_catalog["parameters"],
|
|
376
|
+
track_changes=track_changes,
|
|
377
|
+
dryrun=dryrun,
|
|
378
|
+
record_type="parameter",
|
|
379
|
+
record_id_field="parameterId",
|
|
380
|
+
endpoint="api/ControlParameters",
|
|
381
|
+
existing_controls=existing_catalog["securityControls"],
|
|
382
|
+
new_controls=new_version_catalog["securityControls"],
|
|
383
|
+
)
|
|
384
|
+
if "tests" in existing_catalog:
|
|
385
|
+
check_child_records(
|
|
386
|
+
api=api,
|
|
387
|
+
archived_controls=archived_controls,
|
|
388
|
+
existing_records=existing_catalog["tests"],
|
|
389
|
+
new_records=new_version_catalog["tests"],
|
|
390
|
+
track_changes=track_changes,
|
|
391
|
+
dryrun=dryrun,
|
|
392
|
+
record_type="test",
|
|
393
|
+
record_id_field="testId",
|
|
394
|
+
endpoint="api/ControlTestPlans",
|
|
395
|
+
existing_controls=existing_catalog["securityControls"],
|
|
396
|
+
new_controls=new_version_catalog["securityControls"],
|
|
397
|
+
)
|
|
398
|
+
# TODO: Dealing with empty lists of CCIs returned by API.
|
|
399
|
+
# Need to improve this logic because What if the CCIs or other record type were
|
|
400
|
+
# left off initial catalog and added later?
|
|
401
|
+
if "ccis" in existing_catalog and len(existing_catalog["ccis"]) > 0:
|
|
402
|
+
check_child_records(
|
|
403
|
+
api=api,
|
|
404
|
+
archived_controls=archived_controls,
|
|
405
|
+
existing_records=existing_catalog["ccis"],
|
|
406
|
+
new_records=new_version_catalog["ccis"],
|
|
407
|
+
track_changes=track_changes,
|
|
408
|
+
dryrun=dryrun,
|
|
409
|
+
record_type="CCI",
|
|
410
|
+
record_id_field="name",
|
|
411
|
+
endpoint="api/cci/",
|
|
412
|
+
existing_controls=existing_catalog["securityControls"],
|
|
413
|
+
new_controls=new_version_catalog["securityControls"],
|
|
414
|
+
)
|
|
415
|
+
check_catalog_metadata(
|
|
416
|
+
api=api,
|
|
417
|
+
existing_catalog=existing_catalog,
|
|
418
|
+
new_version_catalog=new_version_catalog,
|
|
419
|
+
track_changes=track_changes,
|
|
420
|
+
dryrun=dryrun,
|
|
421
|
+
)
|
|
422
|
+
if len(track_changes) > 0:
|
|
423
|
+
write_outcomes_to_file(changes=track_changes, output_filename=output_filename)
|
|
424
|
+
else:
|
|
425
|
+
logger.info("No updates found at this time.")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def check_controls(
|
|
429
|
+
api: Api,
|
|
430
|
+
existing_controls: list,
|
|
431
|
+
new_controls: list,
|
|
432
|
+
track_changes: list,
|
|
433
|
+
dryrun: bool,
|
|
434
|
+
) -> list:
|
|
435
|
+
"""
|
|
436
|
+
Manages several function for checking which controls may need to be updated, archived, or created.
|
|
437
|
+
|
|
438
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
439
|
+
:param list existing_controls: existing security controls from target of catalog updates in RegScale installation
|
|
440
|
+
:param list new_controls: controls extracted from the update source catalog
|
|
441
|
+
:param list track_changes: list containing a record of changes that were noted between old and new, for reporting
|
|
442
|
+
:param bool dryrun: True if dry run (don't make changes to data just report changes) or False (make updates)
|
|
443
|
+
:return: List of archived controls
|
|
444
|
+
:rtype: list
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
logger.info("Checking for updates within Security Control fields.")
|
|
448
|
+
(
|
|
449
|
+
existing_map,
|
|
450
|
+
new_map,
|
|
451
|
+
archive_ids_set,
|
|
452
|
+
create_ids_set,
|
|
453
|
+
update_ids_set,
|
|
454
|
+
) = define_operations(id_key_name="controlId", old_records=existing_controls, new_records=new_controls)
|
|
455
|
+
|
|
456
|
+
# PROCESS UPDATES
|
|
457
|
+
# ignore localized system metadata & ids when comparing new and old
|
|
458
|
+
ignore_keys = {
|
|
459
|
+
"dateCreated",
|
|
460
|
+
"createdBy",
|
|
461
|
+
"createdById",
|
|
462
|
+
"lastUpdatedBy",
|
|
463
|
+
"lastUpdatedById",
|
|
464
|
+
"dateLastUpdated",
|
|
465
|
+
"uuid",
|
|
466
|
+
"id",
|
|
467
|
+
"tenantsId",
|
|
468
|
+
# "catalogueID",
|
|
469
|
+
"controlId",
|
|
470
|
+
"controlType",
|
|
471
|
+
}
|
|
472
|
+
check_controls_do_updates(api, dryrun, existing_map, ignore_keys, new_map, track_changes, update_ids_set)
|
|
473
|
+
|
|
474
|
+
# CREATE NEW
|
|
475
|
+
archived = [] # don't upload a control if it's already in archived status
|
|
476
|
+
check_controls_do_create(api, archived, create_ids_set, dryrun, existing_controls, new_map, track_changes)
|
|
477
|
+
|
|
478
|
+
# PROCESS ARCHIVED
|
|
479
|
+
check_controls_do_archived(api, archive_ids_set, dryrun, existing_map, track_changes)
|
|
480
|
+
|
|
481
|
+
# The only purpose of this section is to keep a running list of controlIds that were archived. Later we want to make
|
|
482
|
+
# sure that any child records inherit the archival status of their parent control.
|
|
483
|
+
archived_controls = []
|
|
484
|
+
for change in track_changes:
|
|
485
|
+
if change["field"] == "archived" and change["new_value"] is True:
|
|
486
|
+
archived_controls.append(existing_map[change["id"]]["id"]) # note id of control w/ archived updated to true
|
|
487
|
+
return archived_controls
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def check_controls_do_archived(
|
|
491
|
+
api: Api, archive_ids_set: set, dryrun: bool, existing_map: dict, track_changes: list
|
|
492
|
+
) -> None:
|
|
493
|
+
"""
|
|
494
|
+
Function to archive controls that were found in the old catalog but not in the new catalog
|
|
495
|
+
|
|
496
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
497
|
+
:param set archive_ids_set: set of IDs for records identified for archiving
|
|
498
|
+
:param bool dryrun: True for do a dryrun, false for not a dry run
|
|
499
|
+
:param dict existing_map: Contains the existing records in a hashmap of identifiers and complete records
|
|
500
|
+
:param list track_changes: List containing a record of changes that were noted between old and new, for reporting
|
|
501
|
+
:rtype: None
|
|
502
|
+
"""
|
|
503
|
+
if len(archive_ids_set) > 0:
|
|
504
|
+
logger.info(
|
|
505
|
+
"Checking for security controls in the old version of catalog which do not exist in the new version."
|
|
506
|
+
)
|
|
507
|
+
for control_id in archive_ids_set:
|
|
508
|
+
handle_control_archiving(api, control_id, dryrun, existing_map, track_changes)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def handle_control_archiving(api: Api, control_id: int, dryrun: bool, existing_map: dict, track_changes: list) -> None:
|
|
512
|
+
"""
|
|
513
|
+
Function to archive a control that was found in the old catalog but not in the new catalog
|
|
514
|
+
|
|
515
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
516
|
+
:param int control_id: ID of the control to be archived
|
|
517
|
+
:param bool dryrun: True for do a dryrun, false for not a dry run
|
|
518
|
+
:param dict existing_map: Contains the existing records in a hashmap of identifiers and complete records
|
|
519
|
+
:param list track_changes: List containing a record of changes that were noted between old and new, for reporting
|
|
520
|
+
:rtype: None
|
|
521
|
+
"""
|
|
522
|
+
if existing_map[control_id]["archived"] is False: # skip if already archived status
|
|
523
|
+
archive_record(
|
|
524
|
+
existing_record=existing_map[control_id],
|
|
525
|
+
track_changes=track_changes,
|
|
526
|
+
record_id=control_id,
|
|
527
|
+
record_type=SECURITY_CONTROL,
|
|
528
|
+
justification="Control from old catalog no longer found in new catalog.",
|
|
529
|
+
)
|
|
530
|
+
if not dryrun: # but ONLY if this is NOT a dry run
|
|
531
|
+
update_archived_status(api, control_id, existing_map)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def update_archived_status(api: Api, control_id: int, existing_map: dict) -> None:
|
|
535
|
+
"""
|
|
536
|
+
Function to update the archived status of a control in the RegScale installation
|
|
537
|
+
|
|
538
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
539
|
+
:param int control_id: ID of the control to be archived
|
|
540
|
+
:param dict existing_map: Contains the existing records in a hashmap of identifiers and complete records
|
|
541
|
+
:rtype: None
|
|
542
|
+
"""
|
|
543
|
+
existing_map[control_id]["archived"] = True
|
|
544
|
+
response = api.put(
|
|
545
|
+
url=urljoin(
|
|
546
|
+
api.config["domain"],
|
|
547
|
+
API_SECURITY_CONTROLS_ + str(existing_map[control_id]["id"]),
|
|
548
|
+
),
|
|
549
|
+
json=existing_map[control_id],
|
|
550
|
+
)
|
|
551
|
+
if not response.ok:
|
|
552
|
+
logger.error(f"Response {response.status_code} - Trouble archiving with URL: {response.request.url}")
|
|
553
|
+
else:
|
|
554
|
+
logger.info(f'Archived Control #{existing_map[control_id]["id"]}: {existing_map[control_id]["controlId"]}')
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def check_controls_do_create(
|
|
558
|
+
api: Api,
|
|
559
|
+
archived: list,
|
|
560
|
+
create_ids_set: set,
|
|
561
|
+
dryrun: bool,
|
|
562
|
+
existing_controls: list,
|
|
563
|
+
new_map: dict,
|
|
564
|
+
track_changes: list,
|
|
565
|
+
) -> None:
|
|
566
|
+
"""
|
|
567
|
+
Function to create new controls that were found in the new catalog but not in the old catalog
|
|
568
|
+
|
|
569
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
570
|
+
:param list archived: list of control IDs that were archived
|
|
571
|
+
:param set create_ids_set: set of IDs for records identified for creation
|
|
572
|
+
:param bool dryrun: True for do a dryrun, false for not a dry run
|
|
573
|
+
:param list existing_controls: list of existing controls
|
|
574
|
+
:param dict new_map: hashmap of identifiers and complete records
|
|
575
|
+
:param list track_changes: list containing a record of changes that were noted between old and new, for reporting
|
|
576
|
+
:rtype: None
|
|
577
|
+
"""
|
|
578
|
+
for control_id in create_ids_set:
|
|
579
|
+
if new_map[control_id]["archived"] is True:
|
|
580
|
+
archived.append(control_id)
|
|
581
|
+
for control_id in archived:
|
|
582
|
+
create_ids_set.remove(control_id)
|
|
583
|
+
if len(create_ids_set) > 0:
|
|
584
|
+
logger.info(
|
|
585
|
+
f"Found the following security controls in the new version of catalog which do not exist in the old "
|
|
586
|
+
f"version. These will be created as new controls: {create_ids_set} "
|
|
587
|
+
)
|
|
588
|
+
for control_id in create_ids_set:
|
|
589
|
+
track_changes.append(
|
|
590
|
+
{
|
|
591
|
+
"operation": "create new record",
|
|
592
|
+
"record_type": SECURITY_CONTROL,
|
|
593
|
+
"id": control_id,
|
|
594
|
+
"field": "",
|
|
595
|
+
"old_value": "",
|
|
596
|
+
"new_value": "",
|
|
597
|
+
"justification": "New Security Control found which does not exist in old catalog.",
|
|
598
|
+
}
|
|
599
|
+
)
|
|
600
|
+
#
|
|
601
|
+
if dryrun is False: # only post updates if this is not a dry run
|
|
602
|
+
new_map[control_id]["catalogueID"] = existing_controls[0]["catalogueID"]
|
|
603
|
+
response = api.post(
|
|
604
|
+
url=urljoin(api.config["domain"], API_SECURITY_CONTROLS_),
|
|
605
|
+
json=new_map[control_id],
|
|
606
|
+
)
|
|
607
|
+
if not response.ok:
|
|
608
|
+
logger.error(f"Response {response.status_code} - Trouble posting to URL: {response.request.url}")
|
|
609
|
+
else:
|
|
610
|
+
response_id = json.loads(response.content)["id"]
|
|
611
|
+
logger.info(f'Created Control {new_map[control_id]["controlId"]} (ID# {response_id})')
|
|
612
|
+
new_map[control_id]["id"] = response_id
|
|
613
|
+
existing_controls.append(new_map[control_id])
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def check_controls_do_updates(
|
|
617
|
+
api: Api,
|
|
618
|
+
dryrun: bool,
|
|
619
|
+
existing_map: dict,
|
|
620
|
+
ignore_keys: set,
|
|
621
|
+
new_map: dict,
|
|
622
|
+
track_changes: list,
|
|
623
|
+
update_ids_set: set,
|
|
624
|
+
) -> None:
|
|
625
|
+
"""
|
|
626
|
+
Function to update existing controls that were found in both the old and new catalogs
|
|
627
|
+
|
|
628
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
629
|
+
:param bool dryrun: True for do a dryrun, false for not a dry run
|
|
630
|
+
:param dict existing_map: Contains the existing records in a hashmap of identifiers and complete records
|
|
631
|
+
:param set ignore_keys: set of keys to ignore when comparing old and new records
|
|
632
|
+
:param dict new_map: Contains the new records in a hashmap of identifiers and complete records
|
|
633
|
+
:param list track_changes: List containing a record of changes that were noted between old and new, for reporting
|
|
634
|
+
:param set update_ids_set: Set of IDs for records identified for update
|
|
635
|
+
:rtype: None
|
|
636
|
+
"""
|
|
637
|
+
for control_id in update_ids_set:
|
|
638
|
+
current_changes_count = len(track_changes) # note size of track changes before updates
|
|
639
|
+
update_record(
|
|
640
|
+
existing_record=existing_map[control_id],
|
|
641
|
+
new_record=new_map[control_id],
|
|
642
|
+
ignore_keys=ignore_keys,
|
|
643
|
+
record_id=control_id,
|
|
644
|
+
record_type=SECURITY_CONTROL,
|
|
645
|
+
track_changes=track_changes,
|
|
646
|
+
)
|
|
647
|
+
if current_changes_count < len(track_changes): # if any changes were recorded for this control
|
|
648
|
+
if dryrun is False: # but ONLY if this is NOT a dry run
|
|
649
|
+
response = api.put(
|
|
650
|
+
url=urljoin(
|
|
651
|
+
api.config["domain"],
|
|
652
|
+
API_SECURITY_CONTROLS_ + str(existing_map[control_id]["id"]),
|
|
653
|
+
),
|
|
654
|
+
json=existing_map[control_id],
|
|
655
|
+
)
|
|
656
|
+
if not response.ok:
|
|
657
|
+
logger.error(
|
|
658
|
+
f"Response {response.status_code} -(276) Trouble updating to URL: {response.request.url}"
|
|
659
|
+
)
|
|
660
|
+
else:
|
|
661
|
+
logger.info(
|
|
662
|
+
f'Updated Control #{existing_map[control_id]["id"]}: {existing_map[control_id]["controlId"]}'
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def check_child_records(
|
|
667
|
+
api: Api,
|
|
668
|
+
archived_controls: list,
|
|
669
|
+
existing_records: list,
|
|
670
|
+
new_records: list,
|
|
671
|
+
track_changes: list,
|
|
672
|
+
dryrun: bool,
|
|
673
|
+
record_type: str,
|
|
674
|
+
record_id_field: str,
|
|
675
|
+
endpoint: str,
|
|
676
|
+
existing_controls: list,
|
|
677
|
+
new_controls: list,
|
|
678
|
+
) -> None:
|
|
679
|
+
"""
|
|
680
|
+
Check child records of controls for updates, archivals, or new records
|
|
681
|
+
|
|
682
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
683
|
+
:param list archived_controls: list of controls identified for archival
|
|
684
|
+
:param list existing_records: list of dicts for existing records from regscale installation
|
|
685
|
+
:param list new_records: list of dicts for records of corresponding type from new source of updates
|
|
686
|
+
:param list track_changes: list containing a record of changes that were noted between old and new, for reporting
|
|
687
|
+
:param bool dryrun: True for yes a dry run False for no not a dry run (real updates)
|
|
688
|
+
:param str record_type: Indicates if updating objectives, parameters, tests, or CCIs
|
|
689
|
+
:param str record_id_field: name of id field appropriate for the record type
|
|
690
|
+
:param str endpoint: the API endpoint associated with this record type
|
|
691
|
+
:param list existing_controls: list of existing controls
|
|
692
|
+
:param list new_controls: list of new controls from update source
|
|
693
|
+
:rtype: None
|
|
694
|
+
"""
|
|
695
|
+
logger.info(f"Now checking {record_type}s for new data.")
|
|
696
|
+
(
|
|
697
|
+
existing_map,
|
|
698
|
+
new_map,
|
|
699
|
+
archive_ids_set,
|
|
700
|
+
create_ids_set,
|
|
701
|
+
update_ids_set,
|
|
702
|
+
) = define_operations(
|
|
703
|
+
id_key_name=record_id_field,
|
|
704
|
+
old_records=existing_records,
|
|
705
|
+
new_records=new_records,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
# PROCESS UPDATES
|
|
709
|
+
# ignore localized system metadata & ids when comparing new and old
|
|
710
|
+
ignore_keys = {
|
|
711
|
+
"dateCreated",
|
|
712
|
+
"createdBy",
|
|
713
|
+
"createdById",
|
|
714
|
+
"lastUpdatedBy",
|
|
715
|
+
"lastUpdatedById",
|
|
716
|
+
"dateLastUpdated",
|
|
717
|
+
"uuid",
|
|
718
|
+
"id",
|
|
719
|
+
"tenantsId",
|
|
720
|
+
"securityControlId",
|
|
721
|
+
record_id_field,
|
|
722
|
+
}
|
|
723
|
+
check_child_do_updates(
|
|
724
|
+
api,
|
|
725
|
+
archived_controls,
|
|
726
|
+
dryrun,
|
|
727
|
+
endpoint,
|
|
728
|
+
existing_map,
|
|
729
|
+
ignore_keys,
|
|
730
|
+
new_map,
|
|
731
|
+
record_type,
|
|
732
|
+
track_changes,
|
|
733
|
+
update_ids_set,
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
# PROCESS ARCHIVES
|
|
737
|
+
check_child_do_archives(api, archive_ids_set, dryrun, endpoint, existing_map, record_type, track_changes)
|
|
738
|
+
|
|
739
|
+
# CREATE NEW
|
|
740
|
+
check_child_do_create(
|
|
741
|
+
api,
|
|
742
|
+
create_ids_set,
|
|
743
|
+
dryrun,
|
|
744
|
+
endpoint,
|
|
745
|
+
existing_controls,
|
|
746
|
+
new_controls,
|
|
747
|
+
new_map,
|
|
748
|
+
record_type,
|
|
749
|
+
track_changes,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def check_child_do_create(
|
|
754
|
+
api: Api,
|
|
755
|
+
create_ids_set: set,
|
|
756
|
+
dryrun: bool,
|
|
757
|
+
endpoint: str,
|
|
758
|
+
existing_controls: list,
|
|
759
|
+
new_controls: list,
|
|
760
|
+
new_map: dict,
|
|
761
|
+
record_type: str,
|
|
762
|
+
track_changes: list,
|
|
763
|
+
) -> None:
|
|
764
|
+
"""
|
|
765
|
+
Function to create new child records that were found in the new catalog but not in the old catalog
|
|
766
|
+
|
|
767
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
768
|
+
:param set create_ids_set: list of ids that were identified for creating a new record
|
|
769
|
+
:param bool dryrun: True for yes a dry run False for no not a dry run (real updates)
|
|
770
|
+
:param str endpoint: the API endpoint associated with this record type
|
|
771
|
+
:param list existing_controls: list of dicts for existing controls from regscale installation
|
|
772
|
+
:param list new_controls: list of dicts for controls of corresponding type from new source of updates
|
|
773
|
+
:param dict new_map: hashmap of identifiers and records
|
|
774
|
+
:param str record_type: Indicates if updating objectives, parameters, tests, or CCIs
|
|
775
|
+
:param list track_changes: dict containing a record of changes that were noted between old and new, for reporting
|
|
776
|
+
:rtype: None
|
|
777
|
+
"""
|
|
778
|
+
if len(create_ids_set) > 0:
|
|
779
|
+
logger.info(
|
|
780
|
+
f"Looking for {record_type}s in the new version of catalog which do not exist in the old "
|
|
781
|
+
f"version. These would be created as new {record_type}s."
|
|
782
|
+
)
|
|
783
|
+
for identifier in create_ids_set:
|
|
784
|
+
# Begin hacky solutions to mapping newly created child records to correct parents :(
|
|
785
|
+
control_mapped = hacky_fix_for_catalog_data_structure(existing_controls, identifier, new_controls, new_map)
|
|
786
|
+
# End of said hacky solution
|
|
787
|
+
do_create(
|
|
788
|
+
api,
|
|
789
|
+
control_mapped,
|
|
790
|
+
dryrun,
|
|
791
|
+
endpoint,
|
|
792
|
+
identifier,
|
|
793
|
+
new_map,
|
|
794
|
+
record_type,
|
|
795
|
+
track_changes,
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def do_create(
|
|
800
|
+
api: Api,
|
|
801
|
+
control_mapped: bool,
|
|
802
|
+
dryrun: bool,
|
|
803
|
+
endpoint: str,
|
|
804
|
+
identifier: int,
|
|
805
|
+
new_map: dict,
|
|
806
|
+
record_type: str,
|
|
807
|
+
track_changes: list,
|
|
808
|
+
) -> None:
|
|
809
|
+
"""
|
|
810
|
+
Function to create new child records that were found in the new catalog but not in the old catalog
|
|
811
|
+
|
|
812
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
813
|
+
:param bool control_mapped: True if the control was mapped to a new control ID, False if not
|
|
814
|
+
:param bool dryrun: True for yes a dry run False for no not a dry run (real updates)
|
|
815
|
+
:param str endpoint: the API endpoint associated with this record type
|
|
816
|
+
:param int identifier: the identifier of the record to be created
|
|
817
|
+
:param dict new_map: hashmap of identifiers and records
|
|
818
|
+
:param str record_type: The type of record being created, used for reporting
|
|
819
|
+
:param list track_changes: List containing a record of changes that were noted between old and new, for reporting
|
|
820
|
+
:rtype: None
|
|
821
|
+
"""
|
|
822
|
+
if control_mapped is True and new_map[identifier]["archived"] is False:
|
|
823
|
+
track_changes.append(
|
|
824
|
+
{
|
|
825
|
+
"operation": "create new record",
|
|
826
|
+
"record_type": record_type,
|
|
827
|
+
"id": identifier,
|
|
828
|
+
"field": "",
|
|
829
|
+
"old_value": "",
|
|
830
|
+
"new_value": "",
|
|
831
|
+
"justification": f"New {record_type} found which did not exist in old catalog.",
|
|
832
|
+
}
|
|
833
|
+
)
|
|
834
|
+
if dryrun is False:
|
|
835
|
+
response = api.post(url=urljoin(api.config["domain"], endpoint), json=new_map[identifier])
|
|
836
|
+
if not response.ok:
|
|
837
|
+
logger.error(f"{response.status_code} - Trouble creating new record with: {response.request.url}")
|
|
838
|
+
else:
|
|
839
|
+
logger.info(f'Created {record_type} {identifier} ID {json.loads(response.content)["id"]})')
|
|
840
|
+
else:
|
|
841
|
+
logger.warning(
|
|
842
|
+
f"Skipped creating {record_type} {identifier}. Either record or it's parent control is archived."
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def hacky_fix_for_catalog_data_structure(
|
|
847
|
+
existing_controls: list,
|
|
848
|
+
identifier: Union[str, int],
|
|
849
|
+
new_controls: list,
|
|
850
|
+
new_map: dict,
|
|
851
|
+
) -> bool:
|
|
852
|
+
"""
|
|
853
|
+
Function to map newly created controls to the previously existing controls
|
|
854
|
+
|
|
855
|
+
:param list existing_controls:
|
|
856
|
+
:param Union[str, int] identifier: Unique identifier for the record
|
|
857
|
+
:param list new_controls: list of new controls
|
|
858
|
+
:param dict new_map: dict containing ids and records
|
|
859
|
+
:return: Whether the control was mapped to a new control ID
|
|
860
|
+
:rtype: bool
|
|
861
|
+
"""
|
|
862
|
+
control_mapped = False
|
|
863
|
+
for control in new_controls:
|
|
864
|
+
if control["id"] == new_map[identifier]["securityControlId"]:
|
|
865
|
+
control_match_field = control["controlId"]
|
|
866
|
+
|
|
867
|
+
for old_control in existing_controls:
|
|
868
|
+
if control_match_field == old_control["controlId"]:
|
|
869
|
+
new_map[identifier]["securityControlId"] = old_control["id"]
|
|
870
|
+
control_mapped = True
|
|
871
|
+
return control_mapped
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def check_child_do_archives(
|
|
875
|
+
api: Api,
|
|
876
|
+
archive_ids_set: set,
|
|
877
|
+
dryrun: bool,
|
|
878
|
+
endpoint: str,
|
|
879
|
+
existing_map: dict,
|
|
880
|
+
record_type: str,
|
|
881
|
+
track_changes: list,
|
|
882
|
+
) -> None:
|
|
883
|
+
"""
|
|
884
|
+
Function to archive child records that were found in the old catalog but not in the new catalog
|
|
885
|
+
|
|
886
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
887
|
+
:param set archive_ids_set: set of IDs for records identified for archiving
|
|
888
|
+
:param bool dryrun: True for do a dryrun, false for not a dry run
|
|
889
|
+
:param str endpoint: the API endpoint associated with this record type
|
|
890
|
+
:param dict existing_map: Contains the existing records in a hashmap of identifiers and complete records
|
|
891
|
+
:param str record_type: Indicates if updating objectives, parameters, tests, or CCIs
|
|
892
|
+
:param list track_changes: List containing a record of changes that were noted between old and new, for reporting
|
|
893
|
+
:rtype: None
|
|
894
|
+
"""
|
|
895
|
+
if len(archive_ids_set) > 0:
|
|
896
|
+
logger.info(
|
|
897
|
+
f"Checking for {record_type}s in the old version of catalog which do not exist in the new "
|
|
898
|
+
f"version. These would be archived."
|
|
899
|
+
)
|
|
900
|
+
for identifier in archive_ids_set:
|
|
901
|
+
handle_archive_records(api, identifier, dryrun, endpoint, existing_map, record_type, track_changes)
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def handle_archive_records(
|
|
905
|
+
api: Api, identifier: int, dryrun: bool, endpoint: str, existing_map: dict, record_type: str, track_changes: list
|
|
906
|
+
) -> None:
|
|
907
|
+
"""
|
|
908
|
+
Function to archive child records that were found in the old catalog but not in the new catalog
|
|
909
|
+
|
|
910
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
911
|
+
:param int identifier: ID of the record to be archived
|
|
912
|
+
:param bool dryrun: True for do a dryrun, false for not a dry run
|
|
913
|
+
:param str endpoint: the API endpoint associated with this record type
|
|
914
|
+
:param dict existing_map: Contains the existing records in a hashmap of identifiers and complete records
|
|
915
|
+
:param str record_type: Indicates if updating objectives, parameters, tests, or CCIs
|
|
916
|
+
:param list track_changes: List containing a record of changes that were noted between old and new, for reporting
|
|
917
|
+
:rtype: None
|
|
918
|
+
"""
|
|
919
|
+
if existing_map[identifier]["archived"] is False: # skip if already archived status
|
|
920
|
+
archive_record(
|
|
921
|
+
existing_record=existing_map[identifier],
|
|
922
|
+
track_changes=track_changes,
|
|
923
|
+
record_id=identifier,
|
|
924
|
+
record_type=record_type,
|
|
925
|
+
justification=f"{record_type} from old catalog no longer found in new catalog.",
|
|
926
|
+
)
|
|
927
|
+
if not dryrun: # but ONLY if this is NOT a dry run
|
|
928
|
+
update_archive_status(api, identifier, endpoint, existing_map, record_type)
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def update_archive_status(api: Api, identifier: int, endpoint: str, existing_map: dict, record_type: str) -> None:
|
|
932
|
+
"""
|
|
933
|
+
Function to update the archived status of a child record in the RegScale installation
|
|
934
|
+
|
|
935
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
936
|
+
:param int identifier: ID of the record to be archived
|
|
937
|
+
:param str endpoint: the API endpoint associated with this record type
|
|
938
|
+
:param dict existing_map: Contains the existing records in a hashmap of identifiers and complete records
|
|
939
|
+
:param str record_type: Indicates if updating objectives, parameters, tests, or CCIs
|
|
940
|
+
:rtype: None
|
|
941
|
+
|
|
942
|
+
"""
|
|
943
|
+
|
|
944
|
+
existing_map[identifier]["archived"] = True
|
|
945
|
+
response = api.put(
|
|
946
|
+
url=urljoin(
|
|
947
|
+
api.config["domain"],
|
|
948
|
+
endpoint + str(existing_map[identifier]["id"]),
|
|
949
|
+
),
|
|
950
|
+
json=existing_map[identifier],
|
|
951
|
+
)
|
|
952
|
+
if not response.ok:
|
|
953
|
+
logger.error(f"Response {response.status_code} - Trouble archiving with URL: {response.request.url}")
|
|
954
|
+
else:
|
|
955
|
+
logger.info(f'Archived {record_type} #{existing_map[identifier]["id"]}: {identifier}')
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def check_child_do_updates(
|
|
959
|
+
api: Api,
|
|
960
|
+
archived_controls: list,
|
|
961
|
+
dryrun: bool,
|
|
962
|
+
endpoint: str,
|
|
963
|
+
existing_map: dict,
|
|
964
|
+
ignore_keys: set,
|
|
965
|
+
new_map: dict,
|
|
966
|
+
record_type: str,
|
|
967
|
+
track_changes: list,
|
|
968
|
+
update_ids_set: set,
|
|
969
|
+
) -> None:
|
|
970
|
+
"""
|
|
971
|
+
Function to update existing child records that were found in both the old and new catalogs
|
|
972
|
+
|
|
973
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
974
|
+
:param list archived_controls: list of archived controls
|
|
975
|
+
:param bool dryrun: True for yes a dry run False for no not a dry run (real updates)
|
|
976
|
+
:param str endpoint: the API endpoint associated with this record type
|
|
977
|
+
:param dict existing_map: hashmap of identifiers and complete records for existing child records
|
|
978
|
+
:param set ignore_keys: set of field keys to be ignored for purposes of comparison
|
|
979
|
+
:param dict new_map: hashmap of identifier and complete records for new source of update data
|
|
980
|
+
:param str record_type: Indicates if updating objectives, parameters, tests, or CCIs
|
|
981
|
+
:param list track_changes: list containing a record of changes that were noted between old and new, for reporting
|
|
982
|
+
:param set update_ids_set: set of ids to be updated
|
|
983
|
+
:rtype: None
|
|
984
|
+
"""
|
|
985
|
+
for identifier in update_ids_set:
|
|
986
|
+
current_changes_count = len(track_changes)
|
|
987
|
+
update_record(
|
|
988
|
+
existing_record=existing_map[identifier],
|
|
989
|
+
new_record=new_map[identifier],
|
|
990
|
+
ignore_keys=ignore_keys,
|
|
991
|
+
record_id=identifier,
|
|
992
|
+
record_type=record_type,
|
|
993
|
+
track_changes=track_changes,
|
|
994
|
+
)
|
|
995
|
+
# If the parent control was archived, should also archive all child objectives associated with that control
|
|
996
|
+
handle_archived(archived_controls, existing_map, identifier, record_type, track_changes)
|
|
997
|
+
#
|
|
998
|
+
if current_changes_count < len(track_changes): # if changes recorded for this objective
|
|
999
|
+
if dryrun is False: # but ONLY if this is NOT a dry run
|
|
1000
|
+
response = api.put(
|
|
1001
|
+
url=urljoin(
|
|
1002
|
+
api.config["domain"],
|
|
1003
|
+
f"{endpoint}/{existing_map[identifier]['id']}",
|
|
1004
|
+
),
|
|
1005
|
+
json=existing_map[identifier],
|
|
1006
|
+
)
|
|
1007
|
+
if not response.ok:
|
|
1008
|
+
logger.error(f"Response {response.status_code} - Trouble updating to URL: {response.request.url}")
|
|
1009
|
+
else:
|
|
1010
|
+
logger.info(f'Updated {record_type} #{existing_map[identifier]["id"]}: {identifier}')
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def handle_archived(
|
|
1014
|
+
archived_controls: list,
|
|
1015
|
+
existing_map: dict,
|
|
1016
|
+
identifier: Union[int, str],
|
|
1017
|
+
record_type: str,
|
|
1018
|
+
track_changes: list,
|
|
1019
|
+
) -> None:
|
|
1020
|
+
"""
|
|
1021
|
+
Function to handle archiving of child records when original control is archived
|
|
1022
|
+
|
|
1023
|
+
:param list archived_controls: list of archived controls
|
|
1024
|
+
:param dict existing_map: hashmap of identifiers and complete records for existing child records
|
|
1025
|
+
:param Union[int, str] identifier: unique identifier for the record
|
|
1026
|
+
:param str record_type: Indicates if updating objectives, parameters, tests, or CCIs
|
|
1027
|
+
:param list track_changes: list containing a record of changes that were noted between old and new, for reporting
|
|
1028
|
+
:rtype: None
|
|
1029
|
+
"""
|
|
1030
|
+
for control_id in archived_controls:
|
|
1031
|
+
if (
|
|
1032
|
+
existing_map[identifier]["securityControlId"] == control_id
|
|
1033
|
+
and existing_map[identifier]["archived"] is False
|
|
1034
|
+
):
|
|
1035
|
+
logger.info(f"Inheriting archived status of parent control for {record_type}: {identifier}")
|
|
1036
|
+
archive_record(
|
|
1037
|
+
existing_record=existing_map[identifier],
|
|
1038
|
+
track_changes=track_changes,
|
|
1039
|
+
record_id=identifier,
|
|
1040
|
+
record_type=record_type,
|
|
1041
|
+
justification="Archived because parent control was archived",
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def check_catalog_metadata(
|
|
1046
|
+
api: Api,
|
|
1047
|
+
existing_catalog: dict,
|
|
1048
|
+
new_version_catalog: dict,
|
|
1049
|
+
track_changes: list,
|
|
1050
|
+
dryrun: bool,
|
|
1051
|
+
) -> None:
|
|
1052
|
+
"""
|
|
1053
|
+
Function to check catalog metadata for updates
|
|
1054
|
+
|
|
1055
|
+
:param Api api: Api object for making requests to the target RegScale installation
|
|
1056
|
+
:param dict existing_catalog: catalog being targeted for updates, retrieved from regscale installation
|
|
1057
|
+
:param dict new_version_catalog: catalog being
|
|
1058
|
+
:param list track_changes: dict containing a record of changes that were noted between old and new, for reporting
|
|
1059
|
+
:param bool dryrun: True for yes a dry run False for no not a dry run (real updates)
|
|
1060
|
+
:rtype: None
|
|
1061
|
+
"""
|
|
1062
|
+
current_changes_count = len(track_changes)
|
|
1063
|
+
# ignore localized system metadata, PIDs, + child records handled elsewhere. archived doesn't apply to catalog
|
|
1064
|
+
ignore_keys = {
|
|
1065
|
+
"dateCreated",
|
|
1066
|
+
"createdBy",
|
|
1067
|
+
"createdById",
|
|
1068
|
+
"lastUpdatedById",
|
|
1069
|
+
"lastUpdatedBy",
|
|
1070
|
+
"dateLastUpdated",
|
|
1071
|
+
"tenantsId",
|
|
1072
|
+
"uuid",
|
|
1073
|
+
"id",
|
|
1074
|
+
"securityControls",
|
|
1075
|
+
"objectives",
|
|
1076
|
+
"tests",
|
|
1077
|
+
"parameters",
|
|
1078
|
+
"ccis",
|
|
1079
|
+
"archived",
|
|
1080
|
+
}
|
|
1081
|
+
update_record(
|
|
1082
|
+
existing_record=existing_catalog,
|
|
1083
|
+
new_record=new_version_catalog,
|
|
1084
|
+
ignore_keys=ignore_keys,
|
|
1085
|
+
record_id=existing_catalog["uuid"],
|
|
1086
|
+
record_type="catalog",
|
|
1087
|
+
track_changes=track_changes,
|
|
1088
|
+
)
|
|
1089
|
+
if current_changes_count < len(track_changes): # if changes recorded in this section
|
|
1090
|
+
del existing_catalog["securityControls"]
|
|
1091
|
+
del existing_catalog["parameters"]
|
|
1092
|
+
del existing_catalog["objectives"]
|
|
1093
|
+
del existing_catalog["tests"]
|
|
1094
|
+
if dryrun is False: # ONLY if this is NOT a dry run
|
|
1095
|
+
response = api.put(
|
|
1096
|
+
url=urljoin(api.config["domain"], API_CATALOGUES_ + str(existing_catalog["id"])),
|
|
1097
|
+
json=existing_catalog,
|
|
1098
|
+
)
|
|
1099
|
+
if not response.ok:
|
|
1100
|
+
logger.error(f"Response {response.status_code} - 424 Trouble updating catalog: {response.request.url}")
|
|
1101
|
+
else:
|
|
1102
|
+
logger.info(f'Updated catalog metadata for #{existing_catalog["id"]}: {existing_catalog["title"]}')
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
# -------------------------------------------------------------------------------------------------------------------- #
|
|
1106
|
+
# Begin Utility Functions
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def define_operations(
|
|
1110
|
+
id_key_name: str, old_records: list[dict], new_records: list[dict]
|
|
1111
|
+
) -> tuple[dict, dict, set, set, set]:
|
|
1112
|
+
"""
|
|
1113
|
+
Uses set logic to identify which identifiers should be created, updated, or archived (soft delete).
|
|
1114
|
+
Works the same for all record types: control, objective, parameter, test, cci
|
|
1115
|
+
|
|
1116
|
+
:param str id_key_name: name of the field that contains the unique identifier for the record type
|
|
1117
|
+
:param list[dict] old_records: list of dicts for existing records from RegScale installation
|
|
1118
|
+
:param list[dict] new_records: list of dicts for records of corresponding type from new source of updates
|
|
1119
|
+
:return: existing_map, new_map, archive_ids_set, create_ids_set, update_ids_set
|
|
1120
|
+
:rtype: tuple[dict, dict, set, set, set]
|
|
1121
|
+
"""
|
|
1122
|
+
existing_map = {d[id_key_name]: d for d in old_records}
|
|
1123
|
+
existing_id_set = set(existing_map.keys())
|
|
1124
|
+
|
|
1125
|
+
new_map = {d[id_key_name]: d for d in new_records}
|
|
1126
|
+
new_id_set = set(new_map.keys())
|
|
1127
|
+
archive_ids_set = existing_id_set - new_id_set # archive objects found in old version of catalog but not in the new
|
|
1128
|
+
create_ids_set = new_id_set - existing_id_set # create as new objects found in new but not old
|
|
1129
|
+
update_ids_set = existing_id_set & new_id_set # update existing if found in both old and new
|
|
1130
|
+
|
|
1131
|
+
return existing_map, new_map, archive_ids_set, create_ids_set, update_ids_set
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def update_record(
|
|
1135
|
+
existing_record: dict,
|
|
1136
|
+
new_record: dict,
|
|
1137
|
+
ignore_keys: set,
|
|
1138
|
+
record_id: int,
|
|
1139
|
+
record_type: str,
|
|
1140
|
+
track_changes: list,
|
|
1141
|
+
) -> None:
|
|
1142
|
+
"""
|
|
1143
|
+
Function to update existing RegScale records with new data
|
|
1144
|
+
|
|
1145
|
+
:param dict existing_record: Existing record from RegScale to be updated
|
|
1146
|
+
:param dict new_record: New record from update source to be used for updating existing record
|
|
1147
|
+
:param set ignore_keys: Keys to ignore when comparing old and new records
|
|
1148
|
+
:param int record_id: Record ID of the record being updated in RegScale
|
|
1149
|
+
:param str record_type: Type of record being updated
|
|
1150
|
+
:param list track_changes: List containing a record of changes that were noted between old and new, for reporting
|
|
1151
|
+
:rtype: None
|
|
1152
|
+
"""
|
|
1153
|
+
existing_keys = set(existing_record.keys())
|
|
1154
|
+
new_keys = set(new_record.keys())
|
|
1155
|
+
#
|
|
1156
|
+
update_record_latest_field_data(
|
|
1157
|
+
existing_keys,
|
|
1158
|
+
existing_record,
|
|
1159
|
+
ignore_keys,
|
|
1160
|
+
new_keys,
|
|
1161
|
+
new_record,
|
|
1162
|
+
record_id,
|
|
1163
|
+
record_type,
|
|
1164
|
+
track_changes,
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
#
|
|
1168
|
+
update_record_new_fields(
|
|
1169
|
+
existing_keys,
|
|
1170
|
+
existing_record,
|
|
1171
|
+
ignore_keys,
|
|
1172
|
+
new_keys,
|
|
1173
|
+
new_record,
|
|
1174
|
+
record_id,
|
|
1175
|
+
record_type,
|
|
1176
|
+
track_changes,
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
#
|
|
1180
|
+
update_record_fields_removed(
|
|
1181
|
+
existing_keys,
|
|
1182
|
+
existing_record,
|
|
1183
|
+
ignore_keys,
|
|
1184
|
+
new_keys,
|
|
1185
|
+
record_id,
|
|
1186
|
+
record_type,
|
|
1187
|
+
track_changes,
|
|
1188
|
+
)
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def check_for_truncation(value: Any, char_limit: Optional[int] = 100) -> Any:
|
|
1192
|
+
"""
|
|
1193
|
+
Function to check if a string is too long to be stored in RegScale and will truncate it if so
|
|
1194
|
+
|
|
1195
|
+
:param Any value: The value to be checked for truncation
|
|
1196
|
+
:param Optional[int] char_limit: The character limit for the field in RegScale, defaults to 100
|
|
1197
|
+
:return: Truncated string if necessary, otherwise the original value
|
|
1198
|
+
:rtype: Any
|
|
1199
|
+
"""
|
|
1200
|
+
if isinstance(value, str) and len(value) > char_limit:
|
|
1201
|
+
return f"{value[:char_limit]}... [truncated]"
|
|
1202
|
+
else:
|
|
1203
|
+
return value
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
def update_record_fields_removed(
|
|
1207
|
+
existing_keys: set,
|
|
1208
|
+
existing_record: dict,
|
|
1209
|
+
ignore_keys: set,
|
|
1210
|
+
new_keys: set,
|
|
1211
|
+
record_id: int,
|
|
1212
|
+
record_type: str,
|
|
1213
|
+
track_changes: list,
|
|
1214
|
+
) -> None:
|
|
1215
|
+
"""
|
|
1216
|
+
Function to update existing RegScale records with new data
|
|
1217
|
+
|
|
1218
|
+
:param set existing_keys: Set of keys for the existing record
|
|
1219
|
+
:param dict existing_record: Existing record from RegScale to be updated
|
|
1220
|
+
:param set ignore_keys: Set of keys to ignore when comparing old and new records
|
|
1221
|
+
:param set new_keys: Set of keys for the new record
|
|
1222
|
+
:param int record_id: Record ID of the record being updated in RegScale
|
|
1223
|
+
:param str record_type: Type of record being updated
|
|
1224
|
+
:param list track_changes: List containing a record of changes that were noted between old and new, for reporting
|
|
1225
|
+
:rtype: None
|
|
1226
|
+
"""
|
|
1227
|
+
for key in (existing_keys - new_keys) - ignore_keys: # fields that were in old version but not new are set to null
|
|
1228
|
+
if existing_record[key] != "" and existing_record[key] is not None: # would ignore if it field already empty
|
|
1229
|
+
track_changes.append(
|
|
1230
|
+
{
|
|
1231
|
+
"operation": "update",
|
|
1232
|
+
"record_type": record_type,
|
|
1233
|
+
"id": record_id,
|
|
1234
|
+
"field": key,
|
|
1235
|
+
"old_value": check_for_truncation(existing_record[key]),
|
|
1236
|
+
"new_value": "",
|
|
1237
|
+
"justification": "field no longer exists in new version",
|
|
1238
|
+
}
|
|
1239
|
+
)
|
|
1240
|
+
existing_record[key] = None
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def update_record_new_fields(
|
|
1244
|
+
existing_keys: set,
|
|
1245
|
+
existing_record: dict,
|
|
1246
|
+
ignore_keys: set,
|
|
1247
|
+
new_keys: set,
|
|
1248
|
+
new_record: dict,
|
|
1249
|
+
record_id: int,
|
|
1250
|
+
record_type: str,
|
|
1251
|
+
track_changes: list,
|
|
1252
|
+
) -> None:
|
|
1253
|
+
"""
|
|
1254
|
+
Function to update existing RegScale records with new data
|
|
1255
|
+
|
|
1256
|
+
:param set existing_keys: Set of keys for the existing record
|
|
1257
|
+
:param dict existing_record: Existing record from RegScale to be updated
|
|
1258
|
+
:param set ignore_keys: Set of keys to ignore when comparing old and new records
|
|
1259
|
+
:param set new_keys: Set of keys for the new record
|
|
1260
|
+
:param dict new_record: New record from update source to be used for updating existing record
|
|
1261
|
+
:param int record_id: Record ID of the record being updated in RegScale
|
|
1262
|
+
:param str record_type: Type of record being updated
|
|
1263
|
+
:param list track_changes: List containing a record of changes that were noted between old and new, for reporting
|
|
1264
|
+
:rtype: None
|
|
1265
|
+
"""
|
|
1266
|
+
for key in (new_keys - existing_keys) - ignore_keys: # are any new fields present that were not in the old version?
|
|
1267
|
+
# ----
|
|
1268
|
+
track_changes.append(
|
|
1269
|
+
{
|
|
1270
|
+
"operation": "update",
|
|
1271
|
+
"record_type": record_type,
|
|
1272
|
+
"id": record_id,
|
|
1273
|
+
"field": key,
|
|
1274
|
+
"old_value": "",
|
|
1275
|
+
"new_value": check_for_truncation(new_record[key]),
|
|
1276
|
+
"justification": f"new field added to this {record_type} in latest version",
|
|
1277
|
+
}
|
|
1278
|
+
) # if so record the change
|
|
1279
|
+
existing_record[key] = new_record[
|
|
1280
|
+
key
|
|
1281
|
+
] # update the existing version of record with field and data from update source
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
def update_record_latest_field_data(
|
|
1285
|
+
existing_keys: set,
|
|
1286
|
+
existing_record: dict,
|
|
1287
|
+
ignore_keys: set,
|
|
1288
|
+
new_keys: set,
|
|
1289
|
+
new_record: dict,
|
|
1290
|
+
record_id: int,
|
|
1291
|
+
record_type: str,
|
|
1292
|
+
track_changes: list,
|
|
1293
|
+
) -> None:
|
|
1294
|
+
"""
|
|
1295
|
+
Function to update existing RegScale records with new data
|
|
1296
|
+
|
|
1297
|
+
:param set existing_keys: Set of keys for the existing record
|
|
1298
|
+
:param dict existing_record: Existing record from RegScale to be updated
|
|
1299
|
+
:param set ignore_keys: Set of keys to ignore when comparing old and new records
|
|
1300
|
+
:param set new_keys: Set of keys for the new record
|
|
1301
|
+
:param dict new_record: New record from update source to be used for updating existing record
|
|
1302
|
+
:param int record_id: Record ID of the record being updated in RegScale
|
|
1303
|
+
:param str record_type: Type of record being updated
|
|
1304
|
+
:param list track_changes: List containing a record of changes that were noted between old and new, for reporting
|
|
1305
|
+
:rtype: None
|
|
1306
|
+
"""
|
|
1307
|
+
for key in (existing_keys & new_keys) - ignore_keys: # where same keys in both, check each field for new data
|
|
1308
|
+
if (
|
|
1309
|
+
existing_record[key] != new_record[key]
|
|
1310
|
+
): # if current version data different the new version data for a field
|
|
1311
|
+
track_changes.append(
|
|
1312
|
+
{
|
|
1313
|
+
"operation": "archive" if key == "archived" else "update",
|
|
1314
|
+
"record_type": record_type,
|
|
1315
|
+
"id": record_id,
|
|
1316
|
+
"field": key,
|
|
1317
|
+
"old_value": check_for_truncation(existing_record[key]),
|
|
1318
|
+
"new_value": check_for_truncation(new_record[key]),
|
|
1319
|
+
"justification": "field data has changed",
|
|
1320
|
+
}
|
|
1321
|
+
) # then record change
|
|
1322
|
+
existing_record[key] = new_record[key] # and overwrite existing with new data for this field
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
def archive_record(
|
|
1326
|
+
existing_record: dict,
|
|
1327
|
+
track_changes: list,
|
|
1328
|
+
record_id: int,
|
|
1329
|
+
record_type: str,
|
|
1330
|
+
justification: str,
|
|
1331
|
+
) -> None:
|
|
1332
|
+
"""
|
|
1333
|
+
Function to archive a record
|
|
1334
|
+
|
|
1335
|
+
:param dict existing_record: Record to be archived
|
|
1336
|
+
:param list track_changes: List containing a record of changes that were noted between old and new, for reporting
|
|
1337
|
+
:param int record_id: Record ID of the record being archived in RegScale
|
|
1338
|
+
:param str record_type: Type of record being archived
|
|
1339
|
+
:param str justification: Justification for archiving the record
|
|
1340
|
+
:rtype: None
|
|
1341
|
+
"""
|
|
1342
|
+
existing_record["archived"] = True
|
|
1343
|
+
track_changes.append(
|
|
1344
|
+
{
|
|
1345
|
+
"operation": "archive",
|
|
1346
|
+
"record_type": record_type,
|
|
1347
|
+
"id": record_id,
|
|
1348
|
+
"field": "archived",
|
|
1349
|
+
"old_value": False,
|
|
1350
|
+
"new_value": True,
|
|
1351
|
+
"justification": justification,
|
|
1352
|
+
}
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def write_outcomes_to_file(changes: list, output_filename: str) -> None:
|
|
1357
|
+
"""
|
|
1358
|
+
Function to write out the changes to a CSV file
|
|
1359
|
+
|
|
1360
|
+
:param list changes: List of changes to be written to file
|
|
1361
|
+
:param str output_filename: Name of the file to be written to
|
|
1362
|
+
:rtype: None
|
|
1363
|
+
"""
|
|
1364
|
+
logger.info(f"\nWriting change report to file: {output_filename}")
|
|
1365
|
+
with open(output_filename, "w", newline="") as csvfile:
|
|
1366
|
+
fieldnames = [
|
|
1367
|
+
"operation",
|
|
1368
|
+
"record_type",
|
|
1369
|
+
"id",
|
|
1370
|
+
"field",
|
|
1371
|
+
"old_value",
|
|
1372
|
+
"new_value",
|
|
1373
|
+
"justification",
|
|
1374
|
+
]
|
|
1375
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
1376
|
+
writer.writeheader()
|
|
1377
|
+
for change in changes:
|
|
1378
|
+
writer.writerow(change)
|