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,3181 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""standard python imports"""
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import zipfile
|
|
10
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
11
|
+
from datetime import date, datetime
|
|
12
|
+
from io import StringIO
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from tempfile import gettempdir
|
|
15
|
+
from threading import Lock
|
|
16
|
+
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
|
17
|
+
from urllib.parse import urljoin
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
import requests
|
|
21
|
+
from dateutil.relativedelta import relativedelta
|
|
22
|
+
from docx import Document
|
|
23
|
+
from docx.table import Table
|
|
24
|
+
from lxml import etree
|
|
25
|
+
from pydantic import BaseModel
|
|
26
|
+
from ssp import SSP
|
|
27
|
+
|
|
28
|
+
from regscale.core.app.api import Api
|
|
29
|
+
from regscale.core.app.application import Application
|
|
30
|
+
from regscale.core.app.utils.app_utils import (
|
|
31
|
+
capitalize_words,
|
|
32
|
+
check_file_path,
|
|
33
|
+
download_file,
|
|
34
|
+
error_and_exit,
|
|
35
|
+
get_current_datetime,
|
|
36
|
+
)
|
|
37
|
+
from regscale.integrations.public.fedramp.ssp_logger import SSPLogger
|
|
38
|
+
from regscale.models.regscale_models import (
|
|
39
|
+
Component,
|
|
40
|
+
ControlImplementation,
|
|
41
|
+
ControlParameter,
|
|
42
|
+
File,
|
|
43
|
+
InterConnection,
|
|
44
|
+
LeveragedAuthorization,
|
|
45
|
+
Parameter,
|
|
46
|
+
PortsProtocol,
|
|
47
|
+
Privacy,
|
|
48
|
+
ProfileMapping,
|
|
49
|
+
Requirement,
|
|
50
|
+
SecurityControl,
|
|
51
|
+
SecurityPlan,
|
|
52
|
+
SystemRole,
|
|
53
|
+
)
|
|
54
|
+
from regscale.models.regscale_models.control_implementation import ControlImplementationStatus
|
|
55
|
+
|
|
56
|
+
ssp_logger = SSPLogger()
|
|
57
|
+
logger = ssp_logger
|
|
58
|
+
|
|
59
|
+
namespaces = {
|
|
60
|
+
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
|
61
|
+
"w14": "http://schemas.microsoft.com/office/word/2010/wordml",
|
|
62
|
+
"pic": "http://schemas.openxmlformats.org/drawingml/2006/picture",
|
|
63
|
+
"a14": "http://schemas.microsoft.com/office/drawing/2010/main",
|
|
64
|
+
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
|
|
65
|
+
"wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
|
|
66
|
+
}
|
|
67
|
+
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
68
|
+
NEW_LINE_OUTPUT = "\n------------------------------\n"
|
|
69
|
+
SYSTEM_TYPE = "Major Application"
|
|
70
|
+
SYSTEM_STATUS = "System Status"
|
|
71
|
+
SERVICE_ARCHS = "Service Provider Architecture Layers"
|
|
72
|
+
DEPLOY_MODEL = "Service Provider Cloud Deployment Model"
|
|
73
|
+
END_MARKER = "!!!"
|
|
74
|
+
SSP_URL_SUFFIX = "/api/securityplans/getList"
|
|
75
|
+
XPATH_TAG = "//w:r/w:t"
|
|
76
|
+
TABLE_TAG = "//w:tbl/w:tr"
|
|
77
|
+
ORGANIZATION_TAG = "Organization Name"
|
|
78
|
+
CONTROL_ID = "Control ID"
|
|
79
|
+
TBD = "To be determinned"
|
|
80
|
+
IMPACT_LEVEL = "Impact Level"
|
|
81
|
+
YES = "Yes"
|
|
82
|
+
CSP = "CSP"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def decode_access_level(key: str) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Decodes the access level from the FedRAMP document
|
|
88
|
+
|
|
89
|
+
:param str key: Key used to decode the access level
|
|
90
|
+
:return: Access level as a string
|
|
91
|
+
:rtype: str
|
|
92
|
+
"""
|
|
93
|
+
access_levels = {
|
|
94
|
+
"P": "Privileged",
|
|
95
|
+
"NP": "Non-Privileged",
|
|
96
|
+
"NLA": "No Logical Access",
|
|
97
|
+
}
|
|
98
|
+
return access_levels.get(key, "Non-Privileged")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def create_responsible_roles(app: Application, table_data: list, ssp_id: int) -> None:
|
|
102
|
+
"""
|
|
103
|
+
[BETA] Inserts the actual the Responsible Roles into the Security Plan.
|
|
104
|
+
|
|
105
|
+
:param Application app: Application object
|
|
106
|
+
:param list table_data: list of dicts
|
|
107
|
+
:param int ssp_id: RegScale SSP ID
|
|
108
|
+
:rtype: None
|
|
109
|
+
"""
|
|
110
|
+
na_text = ControlImplementationStatus.NA
|
|
111
|
+
roles = [table for table in table_data if "Role" in table.keys() and "Internal or External" in table.keys()]
|
|
112
|
+
logger.info(
|
|
113
|
+
event_msg=f"Found {len(roles)} Responsible Roles",
|
|
114
|
+
record_type="role",
|
|
115
|
+
model_layer="system-roles",
|
|
116
|
+
)
|
|
117
|
+
user_id = app.config.get("userId")
|
|
118
|
+
with ThreadPoolExecutor(max_workers=20) as executor:
|
|
119
|
+
futures = []
|
|
120
|
+
for role in roles:
|
|
121
|
+
try:
|
|
122
|
+
access_level = decode_access_level(
|
|
123
|
+
role.get(
|
|
124
|
+
"Privileged (P), Non-Privileged (NP), or No Logical Access (NLA)",
|
|
125
|
+
"Unknown",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
future = executor.submit(
|
|
129
|
+
SystemRole.get_or_create,
|
|
130
|
+
app=app,
|
|
131
|
+
role_name=role.get("Role"),
|
|
132
|
+
ssp_id=ssp_id,
|
|
133
|
+
roleType=role.get("Internal or External", "Internal"),
|
|
134
|
+
accessLevel=access_level,
|
|
135
|
+
sensitivityLevel=role.get("Sensitivity Level", na_text),
|
|
136
|
+
assignedUserId=user_id,
|
|
137
|
+
privilegeDescription=role.get("Authorized Privileges", na_text),
|
|
138
|
+
securityPlanId=ssp_id,
|
|
139
|
+
functions=role.get("Functions Performed", na_text),
|
|
140
|
+
createdById=user_id,
|
|
141
|
+
logger=logger,
|
|
142
|
+
)
|
|
143
|
+
futures.append(future)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.error(
|
|
146
|
+
f"Failed to create Responsible Roles with error: {str(e)}",
|
|
147
|
+
record_type="role",
|
|
148
|
+
model_layer="system-roles",
|
|
149
|
+
)
|
|
150
|
+
for future in as_completed(futures):
|
|
151
|
+
try:
|
|
152
|
+
future.result()
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.error(
|
|
155
|
+
f"Failed to create Responsible Roles with error: {str(e)}",
|
|
156
|
+
record_type="role",
|
|
157
|
+
model_layer="system-roles",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
logger.info(
|
|
161
|
+
"Successfully Created Responsible Roles",
|
|
162
|
+
record_type="role",
|
|
163
|
+
model_layer="system-roles",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def assign_role_to_control(
|
|
168
|
+
control: Any,
|
|
169
|
+
system_role: dict,
|
|
170
|
+
ctrl_roles: dict,
|
|
171
|
+
ctrl_roles_lock: Lock,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Assign control_roles using the system_role specified
|
|
175
|
+
|
|
176
|
+
:param Any control: The control to process
|
|
177
|
+
:param dict system_role: The system role to assign to control
|
|
178
|
+
:param dict ctrl_roles: A dict of control roles
|
|
179
|
+
:param Lock ctrl_roles_lock: A lock to protect the shared resource
|
|
180
|
+
:rtype: None
|
|
181
|
+
"""
|
|
182
|
+
friendly_control_id = get_friendly_control_id(control)
|
|
183
|
+
with ctrl_roles_lock: # Acquire the lock to modify shared resource
|
|
184
|
+
if friendly_control_id in ctrl_roles:
|
|
185
|
+
ctrl_roles[friendly_control_id].append(system_role["id"])
|
|
186
|
+
else:
|
|
187
|
+
ctrl_roles[friendly_control_id] = [system_role["id"]]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def process_role(
|
|
191
|
+
value: Any,
|
|
192
|
+
control: Any,
|
|
193
|
+
unique_values: set,
|
|
194
|
+
system_roles: List,
|
|
195
|
+
app: Application,
|
|
196
|
+
ssp_id: int,
|
|
197
|
+
ctrl_roles: dict,
|
|
198
|
+
ctrl_roles_lock: Lock,
|
|
199
|
+
) -> None:
|
|
200
|
+
"""
|
|
201
|
+
Process the Responsible Role
|
|
202
|
+
|
|
203
|
+
:param Any value: The value to process
|
|
204
|
+
:param Any control: The control to process
|
|
205
|
+
:param set unique_values: A set of unique values
|
|
206
|
+
:param List system_roles: A list of system roles
|
|
207
|
+
:param Application app: The application object
|
|
208
|
+
:param int ssp_id: The SSP ID
|
|
209
|
+
:param dict ctrl_roles: A dict of control roles
|
|
210
|
+
:param Lock ctrl_roles_lock: A lock to protect the shared resource
|
|
211
|
+
:rtype: None
|
|
212
|
+
"""
|
|
213
|
+
if type(value) is str and value.startswith("Responsible Role:"):
|
|
214
|
+
role = value.split(":", 1)[1].strip()
|
|
215
|
+
|
|
216
|
+
# Handle case whwere there are multiples comma delimited
|
|
217
|
+
myrolelist = role.split(",")
|
|
218
|
+
|
|
219
|
+
for role in myrolelist:
|
|
220
|
+
role = role.strip(":")
|
|
221
|
+
|
|
222
|
+
if role.lower() not in unique_values:
|
|
223
|
+
unique_values.add(role.lower())
|
|
224
|
+
system_roles.append(role.strip())
|
|
225
|
+
|
|
226
|
+
system_role = SystemRole.get_or_create(
|
|
227
|
+
app=app,
|
|
228
|
+
role_name=role.strip(),
|
|
229
|
+
ssp_id=ssp_id,
|
|
230
|
+
roleType="Internal",
|
|
231
|
+
accessLevel="Privileged",
|
|
232
|
+
sensitivityLevel=ControlImplementationStatus.NA,
|
|
233
|
+
assignedUserId=app.config.get("userId"),
|
|
234
|
+
privilegeDescription=role,
|
|
235
|
+
securityPlanId=ssp_id,
|
|
236
|
+
functions=role,
|
|
237
|
+
createdById=app.config.get("userId"),
|
|
238
|
+
logger=logger,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if isinstance(system_role, SystemRole):
|
|
242
|
+
system_role = system_role.dict()
|
|
243
|
+
|
|
244
|
+
if control:
|
|
245
|
+
assign_role_to_control(
|
|
246
|
+
control=control, system_role=system_role, ctrl_roles=ctrl_roles, ctrl_roles_lock=ctrl_roles_lock
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def post_responsible_roles(app: Application, table_data: list, ssp_id: int) -> dict:
|
|
251
|
+
"""
|
|
252
|
+
[BETA] Insert the Responsible Roles into the Security Plan
|
|
253
|
+
|
|
254
|
+
:param Application app: Application object
|
|
255
|
+
:param list table_data: list of dicts
|
|
256
|
+
:param int ssp_id: RegScale SSP ID
|
|
257
|
+
:return: dict of the control to role mappings
|
|
258
|
+
:rtype: dict
|
|
259
|
+
"""
|
|
260
|
+
data = [table for table in table_data if "Control Summary Information" in table.keys()]
|
|
261
|
+
system_roles = list()
|
|
262
|
+
|
|
263
|
+
unique_values = set(system_roles)
|
|
264
|
+
ctrl_roles = dict()
|
|
265
|
+
ctrl_roles_lock = Lock() # Create a lock to protect shared resource
|
|
266
|
+
|
|
267
|
+
for obj in data:
|
|
268
|
+
try:
|
|
269
|
+
control = list(obj.keys())[0] if isinstance(obj, dict) and obj.keys() else None
|
|
270
|
+
for value in obj.values():
|
|
271
|
+
process_role(
|
|
272
|
+
value,
|
|
273
|
+
control,
|
|
274
|
+
unique_values,
|
|
275
|
+
system_roles,
|
|
276
|
+
app,
|
|
277
|
+
ssp_id,
|
|
278
|
+
ctrl_roles,
|
|
279
|
+
ctrl_roles_lock,
|
|
280
|
+
)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
logger.error(
|
|
283
|
+
f"Failed to parse Responsible Roles with error: {str(e)}",
|
|
284
|
+
record_type="role",
|
|
285
|
+
model_layer="system-roles",
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return ctrl_roles
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def process_fedramp_oscal_ssp(file_path: click.Path, submission_date: date, expiration_date: date) -> None:
|
|
292
|
+
"""
|
|
293
|
+
OSCAL FedRAMP to RegScale SSP
|
|
294
|
+
|
|
295
|
+
:param click.Path file_path: A click file path object to the oscal file
|
|
296
|
+
:param date submission_date: The Submission date YYYY-MM-DD
|
|
297
|
+
:param date expiration_date: The Expiration date YYYY-MM-DD
|
|
298
|
+
:rtype: None
|
|
299
|
+
"""
|
|
300
|
+
app = Application()
|
|
301
|
+
api = Api()
|
|
302
|
+
config = app.config
|
|
303
|
+
try:
|
|
304
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
|
305
|
+
ssp_dict = json.load(file)
|
|
306
|
+
except FileNotFoundError:
|
|
307
|
+
error_and_exit(f"File not found!\n{file_path}")
|
|
308
|
+
except json.JSONDecodeError as jex:
|
|
309
|
+
logger.error("JSONDecodeError, something is wrong with the file: %s\n%s", file_path, jex)
|
|
310
|
+
|
|
311
|
+
# Create SSP
|
|
312
|
+
create_ssp(api, config, ssp_dict, submission_date, expiration_date)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def check_profile(api: Api, config: dict, title: str) -> list:
|
|
316
|
+
"""
|
|
317
|
+
Check if the profile exists in RegScale
|
|
318
|
+
|
|
319
|
+
:param Api api: The api instance
|
|
320
|
+
:param dict config: The application configuration
|
|
321
|
+
:param str title: The title of the profile in question
|
|
322
|
+
:raises: ValueError if the provided title doesn't exist in RegScale
|
|
323
|
+
:return: List of filtered profiles
|
|
324
|
+
:rtype: list
|
|
325
|
+
"""
|
|
326
|
+
profiles = []
|
|
327
|
+
profiles_response = api.get(config["domain"] + "/api/profiles/getList")
|
|
328
|
+
if profiles_response.ok:
|
|
329
|
+
profiles = profiles_response.json()
|
|
330
|
+
if filtered := [dat for dat in profiles if dat["name"] == title]:
|
|
331
|
+
return filtered[0]["id"]
|
|
332
|
+
else:
|
|
333
|
+
raise ValueError(
|
|
334
|
+
f"The profile {title} does not exist in RegScale, \
|
|
335
|
+
please create it and re-run this task."
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def create_port(api: Api, config: dict, dat: PortsProtocol) -> None:
|
|
340
|
+
"""Create a port and protocol for a component
|
|
341
|
+
|
|
342
|
+
:param Api api: An api instance
|
|
343
|
+
:param dict config: Configuration
|
|
344
|
+
:param PortsProtocol dat: Port and protocol data
|
|
345
|
+
:rtype: None
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
existing_ports = api.get(
|
|
349
|
+
url=config["domain"] + f"/api/portsProtocols/getAllByParent/{dat.parentId}/components",
|
|
350
|
+
).json()
|
|
351
|
+
if dat not in [PortsProtocol.from_dict(port, True) for port in existing_ports]:
|
|
352
|
+
# Check if obj exists
|
|
353
|
+
port_res = api.post(
|
|
354
|
+
url=config["domain"] + "/api/portsProtocols",
|
|
355
|
+
data=json.dumps(dat.__dict__),
|
|
356
|
+
)
|
|
357
|
+
if port_res.status_code == 200:
|
|
358
|
+
logger.info("Port and Protocol for component %i added!", dat.parentId)
|
|
359
|
+
else:
|
|
360
|
+
logger.warning(
|
|
361
|
+
"Unable to post Port and Protocol: %s.",
|
|
362
|
+
json.dumps(dat),
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def create_ssp_components(api: Api, config: dict, components: list[dict], ssp_id: int) -> None:
|
|
367
|
+
"""
|
|
368
|
+
Creates SSP Components
|
|
369
|
+
|
|
370
|
+
:param Api api: The API instance
|
|
371
|
+
:param dict config: The application's configuration
|
|
372
|
+
:param list[dict] components: The components
|
|
373
|
+
:param int ssp_id: The ID of the SSP in RegScale
|
|
374
|
+
:rtype: None
|
|
375
|
+
"""
|
|
376
|
+
component_types = [
|
|
377
|
+
"hardware",
|
|
378
|
+
"software",
|
|
379
|
+
"policy",
|
|
380
|
+
"service",
|
|
381
|
+
"process",
|
|
382
|
+
"procedure",
|
|
383
|
+
"compliance artifact",
|
|
384
|
+
]
|
|
385
|
+
ports = set()
|
|
386
|
+
for component in components:
|
|
387
|
+
comp_type = component["type"] if component["type"].lower() in component_types else "compliance artifact"
|
|
388
|
+
status = "Inactive/Retired"
|
|
389
|
+
if component["status"]["state"] == "operational":
|
|
390
|
+
status = "Active"
|
|
391
|
+
|
|
392
|
+
comp = Component(
|
|
393
|
+
title=component["title"],
|
|
394
|
+
securityPlansId=ssp_id,
|
|
395
|
+
componentType=comp_type,
|
|
396
|
+
lastUpdatedById=config["userId"],
|
|
397
|
+
createdById=config["userId"],
|
|
398
|
+
cmmcExclusion=False,
|
|
399
|
+
componentOwnerId=config["userId"],
|
|
400
|
+
description=component["description"],
|
|
401
|
+
status=status,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# save component
|
|
405
|
+
cmp_id = None
|
|
406
|
+
url = urljoin(config["domain"], "/api/components")
|
|
407
|
+
cmp_response = api.post(
|
|
408
|
+
url=url,
|
|
409
|
+
json=comp.dict(),
|
|
410
|
+
)
|
|
411
|
+
if cmp_response.ok:
|
|
412
|
+
cmp = cmp_response.json()
|
|
413
|
+
cmp_id = cmp["id"]
|
|
414
|
+
logger.info(
|
|
415
|
+
"Successfully posted new component# %i as %s for ssp# %i.",
|
|
416
|
+
cmp_id,
|
|
417
|
+
cmp["title"],
|
|
418
|
+
ssp_id,
|
|
419
|
+
)
|
|
420
|
+
if cmp_id and "protocols" in component.keys():
|
|
421
|
+
for protocol in component["protocols"]:
|
|
422
|
+
ports_protocols = PortsProtocol(
|
|
423
|
+
service="",
|
|
424
|
+
usedBy="",
|
|
425
|
+
parentId=cmp_id,
|
|
426
|
+
purpose=component["type"],
|
|
427
|
+
startPort=int(protocol["port-ranges"][0]["start"]),
|
|
428
|
+
endPort=int(protocol["port-ranges"][0]["end"]),
|
|
429
|
+
protocol=protocol["name"],
|
|
430
|
+
parentModule="components",
|
|
431
|
+
lastUpdatedById=config["userId"],
|
|
432
|
+
createdById=config["userId"],
|
|
433
|
+
)
|
|
434
|
+
ports.add(ports_protocols)
|
|
435
|
+
|
|
436
|
+
if component["type"].lower() == "interconnection" and cmp_id:
|
|
437
|
+
# Create ports and protocols object
|
|
438
|
+
ports_protocols = PortsProtocol(
|
|
439
|
+
service="",
|
|
440
|
+
usedBy="",
|
|
441
|
+
parentId=cmp_id,
|
|
442
|
+
purpose=component["type"],
|
|
443
|
+
startPort=0,
|
|
444
|
+
endPort=0,
|
|
445
|
+
protocol="",
|
|
446
|
+
parentModule="components",
|
|
447
|
+
lastUpdatedById=config["userId"],
|
|
448
|
+
createdById=config["userId"],
|
|
449
|
+
)
|
|
450
|
+
ports_protocols.parentId = cmp_id
|
|
451
|
+
ports_protocols.purpose = component["type"]
|
|
452
|
+
# loop through properties to find port number
|
|
453
|
+
if "props" in component.keys():
|
|
454
|
+
for prop in component["props"]:
|
|
455
|
+
if prop["name"] == "information":
|
|
456
|
+
ports_protocols.purpose = prop["value"]
|
|
457
|
+
if prop["name"] == "port":
|
|
458
|
+
ports_protocols.startPort = int(prop["value"])
|
|
459
|
+
ports_protocols.endPort = int(prop["value"])
|
|
460
|
+
ports.add(ports_protocols)
|
|
461
|
+
create_component_mapping(api, config, ssp_id, cmp_id)
|
|
462
|
+
if ports:
|
|
463
|
+
for dat in ports:
|
|
464
|
+
create_port(api, config, dat)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def create_component_mapping(api: Api, config: dict, ssp_id: int, cmp_id: int) -> None:
|
|
468
|
+
"""
|
|
469
|
+
Create Component Mapping
|
|
470
|
+
|
|
471
|
+
:param Api api: The api instance.
|
|
472
|
+
:param dict config: The application configuration.
|
|
473
|
+
:param int ssp_id: The SSP ID.
|
|
474
|
+
:param int cmp_id: The component ID.
|
|
475
|
+
:rtype: None
|
|
476
|
+
"""
|
|
477
|
+
mapping = {
|
|
478
|
+
"securityPlanId": ssp_id,
|
|
479
|
+
"componentId": cmp_id,
|
|
480
|
+
"isPublic": True,
|
|
481
|
+
"createdById": config["userId"],
|
|
482
|
+
"lastUpdatedById": config["userId"],
|
|
483
|
+
}
|
|
484
|
+
mapping_response = api.post(
|
|
485
|
+
url=config["domain"] + "/api/componentmapping",
|
|
486
|
+
data=mapping,
|
|
487
|
+
)
|
|
488
|
+
if mapping_response.status_code != 200:
|
|
489
|
+
logger.warning("Unable to post Mapping Response: %s.", json.dumps(mapping))
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def create_ssp_stakeholders(api: Api, config: dict, ssp_id: int, ssp_dict: dict) -> None:
|
|
493
|
+
"""
|
|
494
|
+
Create Stakeholders in RegScale
|
|
495
|
+
|
|
496
|
+
:param Api api: The api instance.
|
|
497
|
+
:param dict config: The application configuration.
|
|
498
|
+
:param int ssp_id: The SSP ID.
|
|
499
|
+
:param dict ssp_dict: An SSP Dictionary.
|
|
500
|
+
:rtype: None
|
|
501
|
+
"""
|
|
502
|
+
parties = ssp_dict["system-security-plan"]["metadata"]["parties"]
|
|
503
|
+
filtered_parties = list(filter(lambda x: x["type"] == "person", parties))
|
|
504
|
+
for party in filtered_parties:
|
|
505
|
+
title = [dat["value"] for dat in party["props"] if dat["name"] == "job-title"]
|
|
506
|
+
phone = [dat["number"] for dat in party["telephone-numbers"]]
|
|
507
|
+
email = list(party["email-addresses"])
|
|
508
|
+
addresses = list(party["addresses"]) if "addresses" in party.keys() else None
|
|
509
|
+
stakeholder = {
|
|
510
|
+
"name": party["name"],
|
|
511
|
+
"title": title[0] if title else "",
|
|
512
|
+
"phone": phone[0] if phone else "",
|
|
513
|
+
"email": email[0] if email else "",
|
|
514
|
+
"address": (
|
|
515
|
+
addresses[0]["addr-lines"][0]
|
|
516
|
+
+ " "
|
|
517
|
+
+ addresses[0]["city"]
|
|
518
|
+
+ " "
|
|
519
|
+
+ addresses[0]["state"]
|
|
520
|
+
+ ", "
|
|
521
|
+
+ addresses[0]["postal-code"]
|
|
522
|
+
if addresses
|
|
523
|
+
else ""
|
|
524
|
+
),
|
|
525
|
+
"otherID": party["uuid"],
|
|
526
|
+
"notes": email[0] if email else "",
|
|
527
|
+
"parentId": ssp_id,
|
|
528
|
+
"parentModule": "securityplans",
|
|
529
|
+
}
|
|
530
|
+
post_stakeholder(api, config, stakeholder)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def post_stakeholder(api: Api, config: dict, stakeholder: dict) -> Optional[list]:
|
|
534
|
+
"""Post Stakeholders to RegScale
|
|
535
|
+
|
|
536
|
+
:param Api api: API instance
|
|
537
|
+
:param dict config: An application configuration
|
|
538
|
+
:param dict stakeholder: A stakeholder dictionary
|
|
539
|
+
:return: A list of stakeholders, if any
|
|
540
|
+
:rtype: Optional[list]
|
|
541
|
+
"""
|
|
542
|
+
response = api.post(
|
|
543
|
+
url=urljoin(config["domain"], "/api/stakeholders"),
|
|
544
|
+
json=stakeholder,
|
|
545
|
+
)
|
|
546
|
+
if response.ok:
|
|
547
|
+
logger.info(
|
|
548
|
+
f"Created Stakeholder {response.json()} ",
|
|
549
|
+
record_type="stakeholder",
|
|
550
|
+
model_layer="stakeholder",
|
|
551
|
+
)
|
|
552
|
+
return response.json()
|
|
553
|
+
else:
|
|
554
|
+
logger.warning(
|
|
555
|
+
f"Unable to create stakeholder: {stakeholder}",
|
|
556
|
+
record_type="stakeholder",
|
|
557
|
+
model_layer="stakeholder",
|
|
558
|
+
)
|
|
559
|
+
return None
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def create_ssp_control_implementations(
|
|
563
|
+
api: Api,
|
|
564
|
+
config: dict,
|
|
565
|
+
ssp_id: int,
|
|
566
|
+
controls: dict,
|
|
567
|
+
ssp_dict: dict,
|
|
568
|
+
) -> None:
|
|
569
|
+
"""
|
|
570
|
+
Create the control implementations from the oscal SSP object
|
|
571
|
+
|
|
572
|
+
:param Api api: The api instance.
|
|
573
|
+
:param dict config: The application configuration.
|
|
574
|
+
:param int ssp_id: The SSP ID.
|
|
575
|
+
:param dict controls: A dict of existing controls in RegScale.
|
|
576
|
+
:param dict ssp_dict: An SSP Dictionary.
|
|
577
|
+
:rtype: None
|
|
578
|
+
"""
|
|
579
|
+
if not controls:
|
|
580
|
+
return
|
|
581
|
+
control_implementations = ssp_dict["system-security-plan"]["control-implementation"]["implemented-requirements"]
|
|
582
|
+
|
|
583
|
+
for implementation in control_implementations:
|
|
584
|
+
status = ControlImplementationStatus.NotImplemented
|
|
585
|
+
|
|
586
|
+
for prop in implementation["props"]:
|
|
587
|
+
if prop["name"] == "implementation-status":
|
|
588
|
+
status = capitalize_words(prop["value"].replace("-", " "))
|
|
589
|
+
if prop["value"].lower() == "implemented":
|
|
590
|
+
status = ControlImplementationStatus.FullyImplemented
|
|
591
|
+
if prop["value"].lower() == "partial":
|
|
592
|
+
status = ControlImplementationStatus.PartiallyImplemented
|
|
593
|
+
|
|
594
|
+
control_id = [
|
|
595
|
+
control["controlID"]
|
|
596
|
+
for control in controls
|
|
597
|
+
if control["controlId"].lower() == implementation["control-id"].lower()
|
|
598
|
+
][0]
|
|
599
|
+
imp = ControlImplementation(
|
|
600
|
+
parentId=ssp_id,
|
|
601
|
+
parentModule="securityplans",
|
|
602
|
+
controlID=control_id,
|
|
603
|
+
controlOwnerId=config["userId"],
|
|
604
|
+
lastUpdatedById=config["userId"],
|
|
605
|
+
createdById=config["userId"],
|
|
606
|
+
status=status,
|
|
607
|
+
)
|
|
608
|
+
# Post Implementation
|
|
609
|
+
post_regscale_object(api=api, config=config, obj=imp)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def post_regscale_object(
|
|
613
|
+
api: Api, config: dict, obj: Any, endpoint: str = "controlimplementation"
|
|
614
|
+
) -> requests.Response:
|
|
615
|
+
"""
|
|
616
|
+
Post RegScale control implementation
|
|
617
|
+
|
|
618
|
+
:param Api api: API instance
|
|
619
|
+
:param dict config: Application config
|
|
620
|
+
:param Any obj: data object
|
|
621
|
+
:param str endpoint: Endpoint to use in RegScale, defaults to "controlimplementation"
|
|
622
|
+
:raises: TypeError if obj is not a dataclass, BaseModel, or dict
|
|
623
|
+
:return: Response from API call to RegScale
|
|
624
|
+
:rtype: requests.Response
|
|
625
|
+
"""
|
|
626
|
+
response = None
|
|
627
|
+
if dataclasses.is_dataclass(obj):
|
|
628
|
+
dat = dataclasses.asdict(obj)
|
|
629
|
+
elif isinstance(obj, BaseModel):
|
|
630
|
+
dat = obj.dict()
|
|
631
|
+
elif isinstance(obj, dict):
|
|
632
|
+
dat = obj
|
|
633
|
+
else:
|
|
634
|
+
raise TypeError("Object must be a dataclass, BaseModel, or dict to post to RegScale.")
|
|
635
|
+
try:
|
|
636
|
+
response = api.post(config["domain"] + f"/api/{endpoint}", json=dat)
|
|
637
|
+
except Exception as ex:
|
|
638
|
+
logger.error("Unable to Post %s: %s to RegScale.\n%s", endpoint, dat, ex)
|
|
639
|
+
|
|
640
|
+
return response
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def create_ssp(api: Api, config: dict, ssp_dict: dict, submission_date: date, expiration_date: date) -> int:
|
|
644
|
+
"""
|
|
645
|
+
Create a basic SSP in RegScale
|
|
646
|
+
|
|
647
|
+
:param Api api: The api instance.
|
|
648
|
+
:param dict config: The application configuration.
|
|
649
|
+
:param dict ssp_dict: An SSP Dictionary.
|
|
650
|
+
:param date submission_date: The Submission date YYYY-MM-DD
|
|
651
|
+
:param date expiration_date: The Expiration date YYYY-MM-DD
|
|
652
|
+
:return: A newly created RegScale security plan id.
|
|
653
|
+
:rtype: int
|
|
654
|
+
"""
|
|
655
|
+
existing_ssps = []
|
|
656
|
+
metadata = ssp_dict["system-security-plan"]["metadata"]
|
|
657
|
+
system = ssp_dict["system-security-plan"]["system-characteristics"]
|
|
658
|
+
fedramp_profile = get_profile(ssp_dict["system-security-plan"]["import-profile"]["href"])["profile"]
|
|
659
|
+
profile_id = check_profile(api, config, fedramp_profile["metadata"]["title"])
|
|
660
|
+
components = ssp_dict["system-security-plan"]["system-implementation"]["components"]
|
|
661
|
+
ssp_payload = {
|
|
662
|
+
"uuid": ssp_dict["system-security-plan"]["uuid"],
|
|
663
|
+
"systemName": system.get("system-name", None), # Required
|
|
664
|
+
"planInformationSystemSecurityOfficerId": config["userId"],
|
|
665
|
+
"planAuthorizingOfficialId": config["userId"],
|
|
666
|
+
"systemOwnerId": config["userId"],
|
|
667
|
+
"otherIdentifier": system["system-ids"][0]["id"],
|
|
668
|
+
"confidentiality": capitalize_words(
|
|
669
|
+
system["system-information"]["information-types"][0]["confidentiality-impact"]["selected"].split("-")[2]
|
|
670
|
+
), # Required
|
|
671
|
+
"integrity": capitalize_words(
|
|
672
|
+
system["system-information"]["information-types"][0]["integrity-impact"]["selected"].split("-")[2]
|
|
673
|
+
), # Required
|
|
674
|
+
"availability": capitalize_words(
|
|
675
|
+
system["system-information"]["information-types"][0]["availability-impact"]["selected"].split("-")[2]
|
|
676
|
+
), # Required
|
|
677
|
+
"status": capitalize_words(system["status"].get("state", "operational")), # Required
|
|
678
|
+
"description": system.get("description", None),
|
|
679
|
+
"dateSubmitted": submission_date.strftime(DATE_FORMAT),
|
|
680
|
+
"approvalDate": (submission_date + relativedelta(years=1)).strftime(DATE_FORMAT), # User must be changed
|
|
681
|
+
"expirationDate": expiration_date.strftime(DATE_FORMAT),
|
|
682
|
+
"systemType": SYSTEM_TYPE, # User must change
|
|
683
|
+
"purpose": metadata.get("", None),
|
|
684
|
+
"conditionsOfApproval": metadata.get("", None),
|
|
685
|
+
"environment": metadata.get("", None),
|
|
686
|
+
"lawsAndRegulations": metadata.get("", None),
|
|
687
|
+
"authorizationBoundary": metadata.get("", None),
|
|
688
|
+
"networkArchitecture": metadata.get("", None),
|
|
689
|
+
"dataFlow": metadata.get("", None),
|
|
690
|
+
"overallCategorization": capitalize_words(system["security-sensitivity-level"].split("-")[2]),
|
|
691
|
+
"maturityTier": metadata.get("", None),
|
|
692
|
+
"createdById": config["userId"],
|
|
693
|
+
"hva": False,
|
|
694
|
+
"practiceLevel": metadata.get("", None),
|
|
695
|
+
"processLevel": metadata.get("", None),
|
|
696
|
+
"cmmcLevel": metadata.get("", None),
|
|
697
|
+
"cmmcStatus": metadata.get("", None),
|
|
698
|
+
"isPublic": True,
|
|
699
|
+
"executiveSummary": metadata.get("", None),
|
|
700
|
+
"recommendations": metadata.get("", None),
|
|
701
|
+
"importProfile": metadata.get("version", "fedramp1.1.0-oscal1.0.0"),
|
|
702
|
+
"parentId": profile_id,
|
|
703
|
+
"parentModule": "profiles",
|
|
704
|
+
}
|
|
705
|
+
logger.warning("Unknown System Type, defaulting to %s.", ssp_payload["systemType"])
|
|
706
|
+
logger.warning("Unknown HVA status, defaulting to %r.", ssp_payload["hva"])
|
|
707
|
+
|
|
708
|
+
existing_ssp_response = api.get(url=urljoin(config["domain"], SSP_URL_SUFFIX))
|
|
709
|
+
if existing_ssp_response.ok:
|
|
710
|
+
existing_ssps = existing_ssp_response.json()
|
|
711
|
+
|
|
712
|
+
if system["system-name"] in {ssp["systemName"] for ssp in existing_ssps}:
|
|
713
|
+
dat = {ssp["id"] for ssp in existing_ssps if ssp["systemName"] == system["system-name"]}
|
|
714
|
+
click.confirm(
|
|
715
|
+
f"This SSP Title already exists in the system, \
|
|
716
|
+
SSP: {dat.pop() if len(dat) < 2 else dat}. Would you still like to continue?",
|
|
717
|
+
abort=True,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
response = api.post(url=urljoin(config["domain"], "/api/securityplans"), json=ssp_payload)
|
|
721
|
+
if response.ok:
|
|
722
|
+
logger.info("SSP Created with an id of %i!", response.json()["id"])
|
|
723
|
+
ssp_id = response.json()["id"]
|
|
724
|
+
controls_response = api.get(urljoin(config["domain"], f"/api/profilemapping/getByProfile/{profile_id}"))
|
|
725
|
+
controls = controls_response.json() if controls_response.ok else []
|
|
726
|
+
create_ssp_components(api, config, components, ssp_id)
|
|
727
|
+
create_ssp_control_implementations(api, config, ssp_id, controls, ssp_dict)
|
|
728
|
+
create_ssp_stakeholders(api, config, ssp_id, ssp_dict)
|
|
729
|
+
# update_ssp_contacts(api, config, ssp_id, ssp_dict)
|
|
730
|
+
|
|
731
|
+
return ssp_id
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def get_profile(url: str) -> dict:
|
|
735
|
+
"""
|
|
736
|
+
Downloads the FedRAMP profile
|
|
737
|
+
|
|
738
|
+
:param str url: A profile URL.
|
|
739
|
+
:return: A dictionary with the profile json data.
|
|
740
|
+
:rtype: dict
|
|
741
|
+
"""
|
|
742
|
+
dl_path = download_file(url)
|
|
743
|
+
with open(dl_path, encoding="utf-8") as json_file:
|
|
744
|
+
data = json.load(json_file)
|
|
745
|
+
return data
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def get_tables(document: Any) -> list:
|
|
749
|
+
"""
|
|
750
|
+
Return all document tables
|
|
751
|
+
|
|
752
|
+
:param Any document: document object
|
|
753
|
+
:return: List of all document tables
|
|
754
|
+
:rtype: list
|
|
755
|
+
"""
|
|
756
|
+
tables = list(document.tables)
|
|
757
|
+
for t_table in document.tables:
|
|
758
|
+
for row in t_table.rows:
|
|
759
|
+
for cell in row.cells:
|
|
760
|
+
tables.extend(iter(cell.tables))
|
|
761
|
+
return tables
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def get_xpath_privacy_detailed(tables: Table, key: str, xpath: str, count_array: list[int] = [0, 2, 4, 6]) -> dict:
|
|
765
|
+
"""
|
|
766
|
+
Use Xpath to pull data from XML tables
|
|
767
|
+
|
|
768
|
+
:param Table tables: XML tables
|
|
769
|
+
:param str key: specific key in XML table
|
|
770
|
+
:param str xpath: xpath of the element
|
|
771
|
+
:param list count_array: array of numbers, default is [0, 2, 4, 6]
|
|
772
|
+
:return: Dictionary of specific items found
|
|
773
|
+
:rtype: dict
|
|
774
|
+
"""
|
|
775
|
+
result = {"piishare": None, "piipublic": None, "piaperformed": None, "piasorn": None}
|
|
776
|
+
for t_var in tables:
|
|
777
|
+
if key in t_var._element.xml:
|
|
778
|
+
tree = etree.parse(StringIO(t_var._element.xml))
|
|
779
|
+
tags = tree.xpath(xpath, namespaces=namespaces)
|
|
780
|
+
for p_var in tags:
|
|
781
|
+
t_tags = p_var.xpath(XPATH_TAG, namespaces=namespaces)
|
|
782
|
+
for idx, t_var in enumerate(t_tags):
|
|
783
|
+
if idx in count_array:
|
|
784
|
+
result[list(result.keys())[count_array.index(idx)]] = t_var.text
|
|
785
|
+
|
|
786
|
+
return result
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def get_xpath_data_detailed(tables: Table, key: str, ident: str, xpath: str, count_array: list = None) -> dict:
|
|
790
|
+
"""
|
|
791
|
+
Use Xpath to pull data from XML tables
|
|
792
|
+
|
|
793
|
+
:param Table tables: XML tables
|
|
794
|
+
:param str key: specific key in XML table
|
|
795
|
+
:param str ident:
|
|
796
|
+
:param str xpath: xpath of the element
|
|
797
|
+
:param list count_array: array of numbers, default is [2, 3, 4]
|
|
798
|
+
:return: Dictionary of items found
|
|
799
|
+
:rtype: dict
|
|
800
|
+
"""
|
|
801
|
+
if count_array is None:
|
|
802
|
+
count_array = [2, 3, 4]
|
|
803
|
+
tables = iter(tables)
|
|
804
|
+
confidentiality = None
|
|
805
|
+
integrity = None
|
|
806
|
+
availability = None
|
|
807
|
+
for t_var in tables:
|
|
808
|
+
if key in t_var._element.xml:
|
|
809
|
+
f = StringIO(t_var._element.xml)
|
|
810
|
+
tree = etree.parse(f)
|
|
811
|
+
tags = tree.xpath(xpath, namespaces=namespaces)
|
|
812
|
+
for p_var in tags:
|
|
813
|
+
t_tags = p_var.xpath(XPATH_TAG, namespaces=namespaces)
|
|
814
|
+
count = 0
|
|
815
|
+
for t_var in t_tags:
|
|
816
|
+
if t_var.text == ident or count > 0:
|
|
817
|
+
count += 1
|
|
818
|
+
if count == count_array[0]:
|
|
819
|
+
confidentiality = t_var.text
|
|
820
|
+
if count == count_array[1]:
|
|
821
|
+
integrity = t_var.text
|
|
822
|
+
if count == count_array[2]:
|
|
823
|
+
availability = t_var.text
|
|
824
|
+
return {
|
|
825
|
+
"type": key,
|
|
826
|
+
"nist_ident": ident,
|
|
827
|
+
"confidentiality": confidentiality,
|
|
828
|
+
"integrity": integrity,
|
|
829
|
+
"availability": availability,
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def extract_between_strings(text: str, start_marker: str, end_marker: str) -> Optional[str]:
|
|
834
|
+
"""
|
|
835
|
+
Extract sub string between start marker and end marker strings
|
|
836
|
+
|
|
837
|
+
:param str text: source string
|
|
838
|
+
:param str start_marker: start string to look for
|
|
839
|
+
:param str end_marker: end string to look for
|
|
840
|
+
:return: extracted sub string or None
|
|
841
|
+
:rtype: Optional[str]
|
|
842
|
+
"""
|
|
843
|
+
start_index = text.find(start_marker)
|
|
844
|
+
if start_index == -1:
|
|
845
|
+
return None # Start marker not found
|
|
846
|
+
start_index += len(start_marker)
|
|
847
|
+
end_index = text.find(end_marker, start_index)
|
|
848
|
+
if end_index == -1:
|
|
849
|
+
return None # End marker not found
|
|
850
|
+
return text[start_index:end_index]
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def get_xpath_sysinfo_detailed(tables: Table, key: str, xpath: str, count_array: list = None) -> dict:
|
|
854
|
+
"""
|
|
855
|
+
Use Xpath to pull data from XML tables
|
|
856
|
+
|
|
857
|
+
:param Table tables: XML tables
|
|
858
|
+
:param str key: specific key in XML table
|
|
859
|
+
:param str xpath: xpath of the element
|
|
860
|
+
:param list count_array: array of numbers, default is [0,2, 3, 4]
|
|
861
|
+
:return: Dictionary of specific items found
|
|
862
|
+
:rtype: dict
|
|
863
|
+
"""
|
|
864
|
+
if count_array is None:
|
|
865
|
+
count_array = [0, 2, 4, 6]
|
|
866
|
+
tables = iter(tables)
|
|
867
|
+
uniqueident = None
|
|
868
|
+
systemname = None
|
|
869
|
+
keycount = 0
|
|
870
|
+
for t_var in tables:
|
|
871
|
+
# there are multiple occurences of the key in the document
|
|
872
|
+
# we only want the first one.
|
|
873
|
+
if keycount > 0:
|
|
874
|
+
break
|
|
875
|
+
if key in t_var._element.xml:
|
|
876
|
+
f = StringIO(t_var._element.xml)
|
|
877
|
+
tree = etree.parse(f)
|
|
878
|
+
tags = tree.xpath(xpath, namespaces=namespaces)
|
|
879
|
+
for p_var in tags:
|
|
880
|
+
t_tags = p_var.xpath(XPATH_TAG, namespaces=namespaces)
|
|
881
|
+
count = 0
|
|
882
|
+
for t_var in t_tags:
|
|
883
|
+
if count == count_array[0]:
|
|
884
|
+
uniqueident = t_var.text.strip()
|
|
885
|
+
if count == count_array[1]:
|
|
886
|
+
systemname = t_var.text.strip()
|
|
887
|
+
count += 1
|
|
888
|
+
keycount += 1
|
|
889
|
+
|
|
890
|
+
return {
|
|
891
|
+
"uniqueidentifier": uniqueident,
|
|
892
|
+
"systemname": systemname,
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def get_xpath_prepdata_detailed(tables: Table, key: str, ident: str, xpath: str) -> dict:
|
|
897
|
+
"""
|
|
898
|
+
Use Xpath to pull data from XML tables
|
|
899
|
+
|
|
900
|
+
:param Table tables: XML tables
|
|
901
|
+
:param str key: specific key in XML table
|
|
902
|
+
:param str ident: document identifier tag
|
|
903
|
+
:param str xpath: xpath of the element
|
|
904
|
+
:return: Dictionary of items found
|
|
905
|
+
:rtype: dict
|
|
906
|
+
"""
|
|
907
|
+
tables = iter(tables)
|
|
908
|
+
orgname = ""
|
|
909
|
+
street = ""
|
|
910
|
+
office = ""
|
|
911
|
+
citystate = ""
|
|
912
|
+
for t_var in tables:
|
|
913
|
+
if key in t_var._element.xml:
|
|
914
|
+
f = StringIO(t_var._element.xml)
|
|
915
|
+
tree = etree.parse(f)
|
|
916
|
+
tags = tree.xpath(xpath, namespaces=namespaces)
|
|
917
|
+
for p_var in tags:
|
|
918
|
+
t_tags = p_var.xpath(XPATH_TAG, namespaces=namespaces)
|
|
919
|
+
preptext = ""
|
|
920
|
+
for t_var in t_tags:
|
|
921
|
+
preptext += t_var.text
|
|
922
|
+
preptext += END_MARKER
|
|
923
|
+
orgname = extract_between_strings(preptext, ORGANIZATION_TAG, "Street Address")
|
|
924
|
+
street = extract_between_strings(preptext, "Street Address", "Suite/Room/Building")
|
|
925
|
+
office = extract_between_strings(preptext, "Suite/Room/Building", "City, State Zip")
|
|
926
|
+
citystate = extract_between_strings(preptext, "City, State Zip", END_MARKER)
|
|
927
|
+
|
|
928
|
+
return {
|
|
929
|
+
"type": key,
|
|
930
|
+
"nist_ident": ident,
|
|
931
|
+
"orgname": orgname,
|
|
932
|
+
"office": office,
|
|
933
|
+
"street": street,
|
|
934
|
+
"citystate": citystate,
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def get_contact_info(tables: list, key: str, xpath: str) -> dict:
|
|
939
|
+
"""
|
|
940
|
+
Use Xpath to pull data from XML tables
|
|
941
|
+
|
|
942
|
+
:param list tables: XML tables
|
|
943
|
+
:param str key: key to look for
|
|
944
|
+
:param str xpath: xpath of the element
|
|
945
|
+
:return: Dictionary of sorted data
|
|
946
|
+
:rtype: dict
|
|
947
|
+
"""
|
|
948
|
+
idents = [
|
|
949
|
+
"Name",
|
|
950
|
+
"Title",
|
|
951
|
+
"Company / Organization",
|
|
952
|
+
"Address",
|
|
953
|
+
"Phone Number",
|
|
954
|
+
"Email Address",
|
|
955
|
+
]
|
|
956
|
+
dat = {}
|
|
957
|
+
|
|
958
|
+
def loop_and_update(element_list: list) -> dict:
|
|
959
|
+
"""
|
|
960
|
+
Loop through the element list and update the data dictionary
|
|
961
|
+
|
|
962
|
+
:param list element_list: List of elements
|
|
963
|
+
:return: Updated dictionary
|
|
964
|
+
:rtype: dict
|
|
965
|
+
"""
|
|
966
|
+
value = ""
|
|
967
|
+
if idents:
|
|
968
|
+
count = 0
|
|
969
|
+
field = idents.pop(0)
|
|
970
|
+
|
|
971
|
+
while count < len(element_list) - 1:
|
|
972
|
+
if element_list[count].text == field:
|
|
973
|
+
value = "".join([value, element_list[count + 1].text])
|
|
974
|
+
dat[field] = value
|
|
975
|
+
value = ""
|
|
976
|
+
count += 1
|
|
977
|
+
try:
|
|
978
|
+
if element_list[count + 1].text in idents:
|
|
979
|
+
field = idents.pop(0)
|
|
980
|
+
else:
|
|
981
|
+
if field in dat:
|
|
982
|
+
dat[field] = "".join([dat[field], element_list[count + 1].text])
|
|
983
|
+
count += 1
|
|
984
|
+
except IndexError:
|
|
985
|
+
logger.debug("Unable to continue, index error on row: %i.", count)
|
|
986
|
+
|
|
987
|
+
tables = iter(tables)
|
|
988
|
+
|
|
989
|
+
tag_data = []
|
|
990
|
+
for _, t_enum in enumerate(tables):
|
|
991
|
+
if key in t_enum._element.xml:
|
|
992
|
+
f_var = StringIO(t_enum._element.xml)
|
|
993
|
+
tree = etree.parse(f_var)
|
|
994
|
+
tags = tree.xpath(xpath, namespaces=namespaces)
|
|
995
|
+
for tag in tags:
|
|
996
|
+
p_tags = tag.xpath("//w:p", namespaces=namespaces)
|
|
997
|
+
for p_var in p_tags:
|
|
998
|
+
t_tags = p_var.xpath(XPATH_TAG, namespaces=namespaces)
|
|
999
|
+
for tags in t_tags:
|
|
1000
|
+
tag_data.append(tags)
|
|
1001
|
+
loop_and_update(tag_data)
|
|
1002
|
+
|
|
1003
|
+
return dat
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def extract_title_from_row(row_text: list) -> Optional[str]:
|
|
1007
|
+
"""
|
|
1008
|
+
Extracts the title from the first row's data.
|
|
1009
|
+
|
|
1010
|
+
:param list row_text: List of cell text from the row
|
|
1011
|
+
:return: Title or None if not found
|
|
1012
|
+
:rtype: Optional[str]
|
|
1013
|
+
"""
|
|
1014
|
+
title_data = [x for x in row_text if x] # Ignore empty values
|
|
1015
|
+
if title_data:
|
|
1016
|
+
title_parts = title_data.pop().split("\n")
|
|
1017
|
+
return title_parts[1] if len(title_parts) > 1 else title_parts[0]
|
|
1018
|
+
return None
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def process_contact_info(table, contact_mapping: Dict[int, str], result: Dict[str, str]) -> Dict[str, str]:
|
|
1022
|
+
"""
|
|
1023
|
+
Processes contact information from the table based on the mapping of cell indices to fields.
|
|
1024
|
+
|
|
1025
|
+
:param table: The table from the Word document
|
|
1026
|
+
:param contact_mapping: Dictionary mapping cell indices to contact information fields
|
|
1027
|
+
:param result: Dictionary to store the contact information
|
|
1028
|
+
:return: Updated result dictionary with contact information
|
|
1029
|
+
:rtype: dict
|
|
1030
|
+
"""
|
|
1031
|
+
for cell_index, cell in enumerate(table._cells):
|
|
1032
|
+
field = contact_mapping.get(cell_index)
|
|
1033
|
+
if field:
|
|
1034
|
+
result[field] = cell.text.strip()
|
|
1035
|
+
return result
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def get_base_contact(document: Document, key: Optional[str] = "Point of Contact") -> Dict[str, str]:
|
|
1039
|
+
"""
|
|
1040
|
+
Gets contact information from a document.
|
|
1041
|
+
|
|
1042
|
+
:param Document document: SSP document
|
|
1043
|
+
:param Optional[str] key: key to parse for, defaults to 'Point of Contact'
|
|
1044
|
+
:return: dictionary with contact information
|
|
1045
|
+
:rtype: dict
|
|
1046
|
+
"""
|
|
1047
|
+
result = {}
|
|
1048
|
+
title = None
|
|
1049
|
+
|
|
1050
|
+
# Define a mapping of cell indices to contact information fields
|
|
1051
|
+
contact_mapping = {3: "name", 5: "title", 7: "company", 9: "address", 11: "phone", 13: "email"}
|
|
1052
|
+
|
|
1053
|
+
for table in document.tables:
|
|
1054
|
+
for row_index, row in enumerate(table.rows):
|
|
1055
|
+
row_text = [cell.text.strip() for cell in row.cells]
|
|
1056
|
+
|
|
1057
|
+
if row_index == 0 and not title:
|
|
1058
|
+
# Extract the title from the first row
|
|
1059
|
+
title = extract_title_from_row(row_text)
|
|
1060
|
+
if title:
|
|
1061
|
+
result["title"] = title
|
|
1062
|
+
continue
|
|
1063
|
+
|
|
1064
|
+
if row_text and key == row_text[0]:
|
|
1065
|
+
# Process the contact information
|
|
1066
|
+
result = process_contact_info(table, contact_mapping, result)
|
|
1067
|
+
return result # Early return after contact info is processed
|
|
1068
|
+
|
|
1069
|
+
return result
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
def post_interconnects(app: Application, table_data: list, regscale_ssp: dict) -> None:
|
|
1073
|
+
"""
|
|
1074
|
+
Interconnects map to SSP in RegScale
|
|
1075
|
+
|
|
1076
|
+
:param Application app: Application object
|
|
1077
|
+
:param list table_data: List of tables
|
|
1078
|
+
:param dict regscale_ssp: SecurityPlan object
|
|
1079
|
+
:rtype: None
|
|
1080
|
+
"""
|
|
1081
|
+
api = Api()
|
|
1082
|
+
user_id = app.config["userId"]
|
|
1083
|
+
key = "SP* IP Address and Interface"
|
|
1084
|
+
existing_interconnects = []
|
|
1085
|
+
dat = [table for table in table_data if key in table.keys()]
|
|
1086
|
+
existing_interconnect_response = api.get(
|
|
1087
|
+
app.config["domain"] + f"/api/interconnections/getAllByParent/{regscale_ssp['id']}/securityplans"
|
|
1088
|
+
)
|
|
1089
|
+
if not existing_interconnect_response.raise_for_status() and (
|
|
1090
|
+
existing_interconnect_response.headers.get("content-type") == "application/json; charset=utf-8"
|
|
1091
|
+
):
|
|
1092
|
+
existing_interconnects = existing_interconnect_response.json()
|
|
1093
|
+
|
|
1094
|
+
logger.info(
|
|
1095
|
+
f"Found {len(existing_interconnects)} existing interconnects",
|
|
1096
|
+
record_type="interconnects",
|
|
1097
|
+
model_layer="interconnects",
|
|
1098
|
+
)
|
|
1099
|
+
for interconnect in dat:
|
|
1100
|
+
interconnection = InterConnection(
|
|
1101
|
+
name=interconnect[key],
|
|
1102
|
+
aOId=user_id,
|
|
1103
|
+
interconnectOwnerId=user_id,
|
|
1104
|
+
dateCreated=get_current_datetime(),
|
|
1105
|
+
dateLastUpdated=get_current_datetime(),
|
|
1106
|
+
lastUpdatedById=user_id,
|
|
1107
|
+
createdById=user_id,
|
|
1108
|
+
description=(
|
|
1109
|
+
interconnect["Information Being Transmitted"]
|
|
1110
|
+
if "Information Being Transmitted" in interconnect.keys()
|
|
1111
|
+
else ""
|
|
1112
|
+
),
|
|
1113
|
+
parentId=regscale_ssp["id"],
|
|
1114
|
+
parentModule="securityplans",
|
|
1115
|
+
agreementDate=get_current_datetime(),
|
|
1116
|
+
expirationDate=(datetime.now() + relativedelta(years=3)).strftime(DATE_FORMAT),
|
|
1117
|
+
status="Approved",
|
|
1118
|
+
organization=regscale_ssp["systemName"],
|
|
1119
|
+
categorization=regscale_ssp["overallCategorization"],
|
|
1120
|
+
connectionType="Web Service or API",
|
|
1121
|
+
authorizationType="Interconnect Security Agreement (ISA)",
|
|
1122
|
+
)
|
|
1123
|
+
if interconnection.name + interconnection.description not in {
|
|
1124
|
+
inter["name"] + inter["description"] for inter in existing_interconnects
|
|
1125
|
+
}:
|
|
1126
|
+
post_regscale_object(
|
|
1127
|
+
api=api,
|
|
1128
|
+
config=app.config,
|
|
1129
|
+
obj=interconnection.dict(),
|
|
1130
|
+
endpoint="interconnections",
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def create_privacy_data(app: Application, privacy_data: dict, ssp_id: int) -> None:
|
|
1135
|
+
"""
|
|
1136
|
+
Post Privacy settings for SSP
|
|
1137
|
+
|
|
1138
|
+
:param Application app: Application object
|
|
1139
|
+
:param list privacy_data: list of tables
|
|
1140
|
+
:param int ssp_id: RegScale SSP ID
|
|
1141
|
+
:rtype: None
|
|
1142
|
+
"""
|
|
1143
|
+
use_default = YES.lower() == privacy_data.get("piishare", "").lower()
|
|
1144
|
+
|
|
1145
|
+
privacy = Privacy(
|
|
1146
|
+
id=0,
|
|
1147
|
+
piiCollection=privacy_data["piishare"],
|
|
1148
|
+
piiPublicCollection=privacy_data["piipublic"],
|
|
1149
|
+
piaConducted=privacy_data["piaperformed"],
|
|
1150
|
+
sornExists=privacy_data["piasorn"],
|
|
1151
|
+
sornId=None,
|
|
1152
|
+
ombControlId=None,
|
|
1153
|
+
infoCollected="Collection info not supplied" if use_default else None,
|
|
1154
|
+
justification="Justification not supplied" if use_default else None,
|
|
1155
|
+
businessUse="Business use case not found" if use_default else None,
|
|
1156
|
+
pointOfContactId=app.config["userId"],
|
|
1157
|
+
privacyOfficerId=app.config["userId"],
|
|
1158
|
+
informationSharing="System sharing info not found" if use_default else None,
|
|
1159
|
+
consent="Consent info not found" if use_default else None,
|
|
1160
|
+
security="Security Information not found" if use_default else None,
|
|
1161
|
+
privacyActSystem=YES if YES in privacy_data else "No",
|
|
1162
|
+
recordsSchedule=None,
|
|
1163
|
+
securityPlanId=ssp_id,
|
|
1164
|
+
status="Not Applicable",
|
|
1165
|
+
dateApproved=None,
|
|
1166
|
+
notes="Imported from NIST 800.r3 SSP document.",
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
if not Privacy.get_all_by_parent(ssp_id):
|
|
1170
|
+
if new_privacy := privacy.create():
|
|
1171
|
+
logger.info(
|
|
1172
|
+
f"Privacy #{new_privacy.id} created.",
|
|
1173
|
+
record_type="privacy",
|
|
1174
|
+
model_layer="privacy",
|
|
1175
|
+
)
|
|
1176
|
+
else:
|
|
1177
|
+
logger.info(
|
|
1178
|
+
"Privacy settings already exist, skipping...",
|
|
1179
|
+
record_type="privacy",
|
|
1180
|
+
model_layer="privacy",
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
def post_ports(app: Application, table_data: list, ssp_id: int) -> None:
|
|
1185
|
+
"""
|
|
1186
|
+
Ports map to interconnects
|
|
1187
|
+
|
|
1188
|
+
:param Application app: Application object
|
|
1189
|
+
:param list table_data: list of tables
|
|
1190
|
+
:param int ssp_id: RegScale SSP ID
|
|
1191
|
+
:rtype: None
|
|
1192
|
+
"""
|
|
1193
|
+
dat = [table for table in table_data if "Protocols" in table.keys()]
|
|
1194
|
+
existing_ports = PortsProtocol.get_all_by_parent(parent_id=ssp_id, parent_module="securityplans")
|
|
1195
|
+
|
|
1196
|
+
logger.info(
|
|
1197
|
+
f"Found {len(existing_ports)} existing ports",
|
|
1198
|
+
record_type="ports-protocols",
|
|
1199
|
+
model_layer="ports-protocols",
|
|
1200
|
+
)
|
|
1201
|
+
process_port_protocols(protocols=dat, ssp_id=ssp_id, app_config=app.config)
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def process_port_protocols(protocols: List[dict], ssp_id: int, app_config: Dict) -> None:
|
|
1205
|
+
"""
|
|
1206
|
+
Process port protocols
|
|
1207
|
+
:param List[dict] protocols: List of protocols
|
|
1208
|
+
:param int ssp_id: SSP ID
|
|
1209
|
+
:param dict app_config: Application configuration
|
|
1210
|
+
:rtype: None
|
|
1211
|
+
"""
|
|
1212
|
+
key = "Ports (TCP/UDP)*"
|
|
1213
|
+
|
|
1214
|
+
for protocol in protocols:
|
|
1215
|
+
try:
|
|
1216
|
+
port_field, port_protocol, purpose = extract_protocol_details(protocol, key)
|
|
1217
|
+
port_entries, start = parse_port_field(port_field)
|
|
1218
|
+
|
|
1219
|
+
if start:
|
|
1220
|
+
start, port_entries = update_start_port(start, port_entries)
|
|
1221
|
+
|
|
1222
|
+
create_ports_protocol_records(protocol, port_entries, start, ssp_id, port_protocol, purpose, app_config)
|
|
1223
|
+
|
|
1224
|
+
if not port_entries and port_field.strip().isdigit():
|
|
1225
|
+
single_port = int(port_field.strip())
|
|
1226
|
+
create_single_port_record(protocol, single_port, ssp_id, port_protocol, purpose, app_config)
|
|
1227
|
+
|
|
1228
|
+
except Exception as e:
|
|
1229
|
+
logger.info(f"Error processing port: {port_field} with error: {e}")
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
def extract_protocol_details(protocol: dict, key: str) -> tuple:
|
|
1233
|
+
"""
|
|
1234
|
+
Extracts and cleans up the port field, protocol, and purpose.
|
|
1235
|
+
|
|
1236
|
+
:param dict protocol: Protocol data
|
|
1237
|
+
:param str key: Key to extract the port field
|
|
1238
|
+
:return: Tuple of (port_field, port_protocol, purpose)
|
|
1239
|
+
"""
|
|
1240
|
+
port_field = protocol.get(key, "")
|
|
1241
|
+
unwanted_substrings = ["(TCP)", "(UDP)", "(UDP/TCP)", "TCP", "UDP", "TCP/UDP", "//n", "/n", "\n", "/"]
|
|
1242
|
+
|
|
1243
|
+
# Use a loop to remove all unwanted substrings
|
|
1244
|
+
for substr in unwanted_substrings:
|
|
1245
|
+
port_field = port_field.replace(substr, "")
|
|
1246
|
+
|
|
1247
|
+
port_protocol = protocol.get("Protocols") or parse_port_or_protocol(key, protocol, str)
|
|
1248
|
+
purpose = protocol.get("Purpose", "Unknown") if protocol.get("Purpose") != "" else "Unknown"
|
|
1249
|
+
return port_field, port_protocol, purpose
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def parse_port_field(port_field: str) -> tuple:
|
|
1253
|
+
"""
|
|
1254
|
+
Parses the port field into start port and entries.
|
|
1255
|
+
|
|
1256
|
+
:param str port_field: The port field value
|
|
1257
|
+
:return: Tuple of (port_entries, start)
|
|
1258
|
+
"""
|
|
1259
|
+
if "-" in port_field:
|
|
1260
|
+
start, end = int(port_field.split("-")[0]), port_field.split("-")[1]
|
|
1261
|
+
port_entries = end.split(",")
|
|
1262
|
+
else:
|
|
1263
|
+
start = None
|
|
1264
|
+
port_entries = port_field.split(",")
|
|
1265
|
+
return port_entries, start
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def create_ports_protocol_records(
|
|
1269
|
+
protocol: dict, port_entries: List[str], start: int, ssp_id: int, port_protocol: str, purpose: str, app_config: Dict
|
|
1270
|
+
) -> None:
|
|
1271
|
+
"""
|
|
1272
|
+
Creates and updates PortsProtocol records for each port entry.
|
|
1273
|
+
|
|
1274
|
+
:param dict protocol: Protocol data
|
|
1275
|
+
:param List[str] port_entries: List of port entries
|
|
1276
|
+
:param int start: Start port
|
|
1277
|
+
:param int ssp_id: SSP ID
|
|
1278
|
+
:param str port_protocol: Protocol type
|
|
1279
|
+
:param str purpose: Purpose of the protocol
|
|
1280
|
+
:param dict app_config: Application configuration
|
|
1281
|
+
:rtype: None
|
|
1282
|
+
"""
|
|
1283
|
+
for port_entry in port_entries:
|
|
1284
|
+
port_entry = port_entry.strip()
|
|
1285
|
+
if start:
|
|
1286
|
+
create_ports_protocol(protocol, start, port_entry, ssp_id, port_protocol, purpose, app_config)
|
|
1287
|
+
else:
|
|
1288
|
+
single_port = int(port_entry)
|
|
1289
|
+
create_ports_protocol(protocol, single_port, single_port, ssp_id, port_protocol, purpose, app_config)
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
def create_single_port_record(
|
|
1293
|
+
protocol: dict, single_port: int, ssp_id: int, port_protocol: str, purpose: str, app_config: Dict
|
|
1294
|
+
) -> None:
|
|
1295
|
+
"""
|
|
1296
|
+
Creates a PortsProtocol record for a single port.
|
|
1297
|
+
|
|
1298
|
+
:param dict protocol: Protocol data
|
|
1299
|
+
:param int single_port: Single port value
|
|
1300
|
+
:param int ssp_id: SSP ID
|
|
1301
|
+
:param str port_protocol: Protocol type
|
|
1302
|
+
:param str purpose: Purpose of the protocol
|
|
1303
|
+
:param dict app_config: Application configuration
|
|
1304
|
+
:rtype: None
|
|
1305
|
+
"""
|
|
1306
|
+
create_ports_protocol(protocol, single_port, single_port, ssp_id, port_protocol, purpose, app_config)
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
def create_ports_protocol(
|
|
1310
|
+
protocol: dict, start_port: int, end_port: int, ssp_id: int, port_protocol: str, purpose: str, app_config: Dict
|
|
1311
|
+
) -> None:
|
|
1312
|
+
"""
|
|
1313
|
+
Creates or updates a PortsProtocol object.
|
|
1314
|
+
|
|
1315
|
+
:param dict protocol: Protocol data
|
|
1316
|
+
:param int start_port: Start port
|
|
1317
|
+
:param int end_port: End port
|
|
1318
|
+
:param int ssp_id: SSP ID
|
|
1319
|
+
:param str port_protocol: Protocol type
|
|
1320
|
+
:param str purpose: Purpose of the protocol
|
|
1321
|
+
:param dict app_config: Application configuration
|
|
1322
|
+
:rtype: None
|
|
1323
|
+
"""
|
|
1324
|
+
PortsProtocol(
|
|
1325
|
+
service=protocol.get("Services", "Unknown"),
|
|
1326
|
+
usedBy=protocol.get("Used By", "Unknown"),
|
|
1327
|
+
parentId=ssp_id,
|
|
1328
|
+
purpose=purpose,
|
|
1329
|
+
startPort=start_port,
|
|
1330
|
+
endPort=end_port,
|
|
1331
|
+
protocol=port_protocol.strip().replace("(", "").replace(")", ""),
|
|
1332
|
+
parentModule="securityplans",
|
|
1333
|
+
lastUpdatedById=app_config.get("userId"),
|
|
1334
|
+
createdById=app_config.get("userId"),
|
|
1335
|
+
).create_or_update()
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def update_start_port(start: str, port_entries: List[str]) -> tuple[str, list[str]]:
|
|
1339
|
+
"""
|
|
1340
|
+
Update the start port if necessary
|
|
1341
|
+
:param start:
|
|
1342
|
+
:param port_entries:
|
|
1343
|
+
:return: Updated start, port entries
|
|
1344
|
+
:rtype: tuple[str, list[str]]
|
|
1345
|
+
"""
|
|
1346
|
+
# Convert start to an integer for comparison
|
|
1347
|
+
start = int(start)
|
|
1348
|
+
|
|
1349
|
+
# Convert port_entries to integers
|
|
1350
|
+
port_entries_int = [int(port) for port in port_entries]
|
|
1351
|
+
|
|
1352
|
+
# Check if there's a smaller number in port_entries
|
|
1353
|
+
if port_entries_int and min(port_entries_int) < start:
|
|
1354
|
+
# Get the smallest number from port_entries
|
|
1355
|
+
new_start = min(port_entries_int)
|
|
1356
|
+
|
|
1357
|
+
# Log the change
|
|
1358
|
+
if new_start != start:
|
|
1359
|
+
logger.info(f"Start port changed from {start} to {new_start}")
|
|
1360
|
+
|
|
1361
|
+
# Remove the new start from port_entries and append the old start
|
|
1362
|
+
port_entries_int.remove(new_start)
|
|
1363
|
+
port_entries_int.append(start)
|
|
1364
|
+
|
|
1365
|
+
# Update start
|
|
1366
|
+
start = new_start
|
|
1367
|
+
|
|
1368
|
+
# Return updated start and updated port_entries
|
|
1369
|
+
return str(start), [str(port) for port in port_entries_int]
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
def parse_port_or_protocol(key: str, protocol: dict, return_type: Union[Type[str], Type[int]]) -> Union[str, int]:
|
|
1373
|
+
"""
|
|
1374
|
+
Parse port number from protocol
|
|
1375
|
+
|
|
1376
|
+
:param str key: Key to parse
|
|
1377
|
+
:param dict protocol: Protocol dictionary
|
|
1378
|
+
:param Union[Type[str], Type[int]] return_type: Data type to return
|
|
1379
|
+
:return: Port number or protocol
|
|
1380
|
+
:rtype: Union[str, int]
|
|
1381
|
+
"""
|
|
1382
|
+
try:
|
|
1383
|
+
if return_type == int:
|
|
1384
|
+
port_to_be = protocol.get(key, [])
|
|
1385
|
+
if isinstance(port_to_be, str) == str:
|
|
1386
|
+
port_to_be.replace(" ", "").replace("/n", "").replace("//n", "").replace("\n", "")
|
|
1387
|
+
if port := "".join(c for c in protocol.get(key, []) if c.isdigit()):
|
|
1388
|
+
return int(port)
|
|
1389
|
+
if port := "".join(c for c in protocol.get("Protocols", []) if c.isdigit()):
|
|
1390
|
+
return int(port)
|
|
1391
|
+
elif return_type == str:
|
|
1392
|
+
return "".join(c for c in protocol[key] if not c.isdigit()) or "".join(
|
|
1393
|
+
c for c in protocol["Protocols"] if c.isdigit()
|
|
1394
|
+
)
|
|
1395
|
+
except ValueError:
|
|
1396
|
+
return 0
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
def get_current_implementations(app: Application, regscale_id: int) -> list[dict]:
|
|
1400
|
+
"""Pull current implementations for a given regscale id
|
|
1401
|
+
|
|
1402
|
+
:param Application app: Application instance
|
|
1403
|
+
:param int regscale_id: RegScale ID
|
|
1404
|
+
:return: List of dictionaries
|
|
1405
|
+
:rtype: list[dict]
|
|
1406
|
+
"""
|
|
1407
|
+
current_imps = []
|
|
1408
|
+
api = Api()
|
|
1409
|
+
try:
|
|
1410
|
+
current_imps_response = api.get(
|
|
1411
|
+
url=app.config["domain"] + f"/api/controlImplementation/getAllByPlan/{regscale_id}",
|
|
1412
|
+
params=("skip_check", True),
|
|
1413
|
+
)
|
|
1414
|
+
if not current_imps_response.raise_for_status():
|
|
1415
|
+
current_imps = current_imps_response.json()
|
|
1416
|
+
except requests.HTTPError: # This endpoint returns 404 when empty.
|
|
1417
|
+
current_imps = []
|
|
1418
|
+
return current_imps
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
def get_friendly_control_id(control_number: str) -> str:
|
|
1422
|
+
"""Get friendly control id from control number
|
|
1423
|
+
|
|
1424
|
+
:param str control_number: Control number
|
|
1425
|
+
:return: Friendly control id
|
|
1426
|
+
:rtype: str
|
|
1427
|
+
"""
|
|
1428
|
+
# exp = r"^.*?\([^\d]*(\d+)[^\d]*\).*$"
|
|
1429
|
+
# the above regex allows for a denial of service attack
|
|
1430
|
+
# the below regex should mitigate that
|
|
1431
|
+
exp = r"\((\d+)\)"
|
|
1432
|
+
return (
|
|
1433
|
+
f"{control_number[:match.regs[1][0] - 1].strip()}.{match.groups()[0]}".lower()
|
|
1434
|
+
if (match := re.search(exp, control_number))
|
|
1435
|
+
else control_number.lower()
|
|
1436
|
+
)
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
def post_implementations(
|
|
1440
|
+
app: Application,
|
|
1441
|
+
ssp_obj: SSP,
|
|
1442
|
+
regscale_ssp: dict,
|
|
1443
|
+
mapping: List[ProfileMapping],
|
|
1444
|
+
ctrl_roles: dict,
|
|
1445
|
+
save_data: bool = False,
|
|
1446
|
+
load_missing: bool = False,
|
|
1447
|
+
) -> List:
|
|
1448
|
+
"""
|
|
1449
|
+
Post implementations to RegScale
|
|
1450
|
+
|
|
1451
|
+
:param Application app: Application object
|
|
1452
|
+
:param SSP ssp_obj: SecurityPlan object (python-docx)
|
|
1453
|
+
:param dict regscale_ssp: RegScale ssp
|
|
1454
|
+
:param List[ProfileMapping] mapping: mapping
|
|
1455
|
+
:param dict ctrl_roles: Control roles
|
|
1456
|
+
:param bool save_data: Whether to save data to a file
|
|
1457
|
+
:param bool load_missing: Whether to load missing controls
|
|
1458
|
+
:return: List of new implementations
|
|
1459
|
+
:rtype: List
|
|
1460
|
+
"""
|
|
1461
|
+
api = Api()
|
|
1462
|
+
current_imps = ControlImplementation.get_all_by_parent(
|
|
1463
|
+
parent_id=regscale_ssp.get("id"), parent_module="securityplans"
|
|
1464
|
+
) # get_current_implementations(app, regscale_ssp["id"])
|
|
1465
|
+
for imp in current_imps:
|
|
1466
|
+
imp.control = SecurityControl.get_object(object_id=imp.controlID)
|
|
1467
|
+
for map in mapping:
|
|
1468
|
+
map.control = SecurityControl.get_object(object_id=map.controlID)
|
|
1469
|
+
|
|
1470
|
+
missing_controls = check_control_list_length(ssp_obj, mapping)
|
|
1471
|
+
log_controls_info(ssp_obj, current_imps)
|
|
1472
|
+
(
|
|
1473
|
+
mapped_controls_log,
|
|
1474
|
+
unmapped_controls_log,
|
|
1475
|
+
implemented_controls_log,
|
|
1476
|
+
new_implementations,
|
|
1477
|
+
_,
|
|
1478
|
+
) = process_controls(
|
|
1479
|
+
app,
|
|
1480
|
+
api,
|
|
1481
|
+
ssp_obj,
|
|
1482
|
+
regscale_ssp,
|
|
1483
|
+
mapping,
|
|
1484
|
+
ctrl_roles,
|
|
1485
|
+
current_imps,
|
|
1486
|
+
missing_controls,
|
|
1487
|
+
)
|
|
1488
|
+
|
|
1489
|
+
if load_missing:
|
|
1490
|
+
new_imps = load_non_matched_profile_controls(app, regscale_ssp=regscale_ssp, mapping=mapping)
|
|
1491
|
+
new_implementations.extend(new_imps)
|
|
1492
|
+
|
|
1493
|
+
if save_data:
|
|
1494
|
+
save_control_logs(mapped_controls_log, unmapped_controls_log, implemented_controls_log)
|
|
1495
|
+
return new_implementations
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
def get_responsibility_and_status(fedramp_control: Any) -> Tuple[str, str]:
|
|
1499
|
+
"""
|
|
1500
|
+
Get responsibility and implementation status
|
|
1501
|
+
|
|
1502
|
+
:param Any fedramp_control: FedrampControl object
|
|
1503
|
+
:return: Tuple of responsibility and implementation status
|
|
1504
|
+
:rtype: Tuple[str, str]
|
|
1505
|
+
"""
|
|
1506
|
+
responsibility = None
|
|
1507
|
+
if fedramp_control.control_origination:
|
|
1508
|
+
if "Shared".lower() in fedramp_control.control_origination[0].lower():
|
|
1509
|
+
responsibility = "Shared"
|
|
1510
|
+
elif "Customer".lower() in fedramp_control.control_origination[0].lower():
|
|
1511
|
+
responsibility = "Customer"
|
|
1512
|
+
elif "Provider".lower() in fedramp_control.control_origination[0].lower():
|
|
1513
|
+
responsibility = "Provider"
|
|
1514
|
+
else:
|
|
1515
|
+
responsibility = fedramp_control.control_origination[0]
|
|
1516
|
+
|
|
1517
|
+
if fedramp_control.implementation_status and fedramp_control.implementation_status[0] in [
|
|
1518
|
+
"Alternative Implementation",
|
|
1519
|
+
"Implemented",
|
|
1520
|
+
]:
|
|
1521
|
+
implementation_status = ControlImplementationStatus.FullyImplemented
|
|
1522
|
+
elif ControlImplementationStatus.PartiallyImplemented in fedramp_control.implementation_status:
|
|
1523
|
+
implementation_status = ControlImplementationStatus.PartiallyImplemented
|
|
1524
|
+
else:
|
|
1525
|
+
implementation_status = (
|
|
1526
|
+
fedramp_control.implementation_status[0] if fedramp_control.implementation_status else "Not Implemented"
|
|
1527
|
+
)
|
|
1528
|
+
|
|
1529
|
+
return responsibility, implementation_status
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
def find_control_in_mapping(mapping: List[ProfileMapping], friendly_control_id: str) -> str:
|
|
1533
|
+
"""
|
|
1534
|
+
Find control id in mapping
|
|
1535
|
+
|
|
1536
|
+
:param List[ProfileMapping] mapping: Mapping
|
|
1537
|
+
:param str friendly_control_id: Friendly control id
|
|
1538
|
+
:return: ControlId
|
|
1539
|
+
:rtype: str
|
|
1540
|
+
"""
|
|
1541
|
+
control_id = None
|
|
1542
|
+
if control := [control for control in mapping if control.control.controlId.lower() == friendly_control_id]:
|
|
1543
|
+
control_id = control[0].control.controlId
|
|
1544
|
+
return control_id
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
def get_implementation_text(fedramp_control: Any) -> str:
|
|
1548
|
+
"""
|
|
1549
|
+
Get implementation text
|
|
1550
|
+
|
|
1551
|
+
:param Any fedramp_control: FedrampControl object
|
|
1552
|
+
:return: Implementation text
|
|
1553
|
+
:rtype: str
|
|
1554
|
+
"""
|
|
1555
|
+
if len(fedramp_control.parts) > 1:
|
|
1556
|
+
return "<br>".join(fedramp_control.part(x).text for x in fedramp_control.parts)
|
|
1557
|
+
else:
|
|
1558
|
+
try:
|
|
1559
|
+
return fedramp_control.part(None).text
|
|
1560
|
+
except IndexError:
|
|
1561
|
+
return ""
|
|
1562
|
+
|
|
1563
|
+
|
|
1564
|
+
def handle_control_implementation(
|
|
1565
|
+
app: Application,
|
|
1566
|
+
regscale_ssp: Dict,
|
|
1567
|
+
control_id: str,
|
|
1568
|
+
responsibility: str,
|
|
1569
|
+
implementation_status: str,
|
|
1570
|
+
implementation_text: str,
|
|
1571
|
+
ctrl_roles: Dict,
|
|
1572
|
+
friendly_control_id: str,
|
|
1573
|
+
new_implementations: list,
|
|
1574
|
+
update_implmentations: list,
|
|
1575
|
+
current_implementations_dict: Dict[str, ControlImplementation],
|
|
1576
|
+
mapping: Dict[str, ProfileMapping],
|
|
1577
|
+
) -> ControlImplementation:
|
|
1578
|
+
"""
|
|
1579
|
+
Handle control implementation
|
|
1580
|
+
|
|
1581
|
+
:param Application app: Application instance
|
|
1582
|
+
:param Dict regscale_ssp: RegScale ssp
|
|
1583
|
+
:param str control_id: Control id
|
|
1584
|
+
:param str responsibility: Responsibility
|
|
1585
|
+
:param str implementation_status: Implementation status
|
|
1586
|
+
:param str implementation_text: Implementation text
|
|
1587
|
+
:param Dict ctrl_roles: Control roles
|
|
1588
|
+
:param str friendly_control_id: Friendly control id
|
|
1589
|
+
:param list new_implementations: List of new implementations
|
|
1590
|
+
:param list update_implmentations: List of updated implementations
|
|
1591
|
+
:param Dict[str, ControlImplementation] current_implementations_dict: Dictionary of current implementations
|
|
1592
|
+
:param List[ProfileMapping] mapping: Mapping
|
|
1593
|
+
:return: Response object
|
|
1594
|
+
:rtype: Optional[Response]
|
|
1595
|
+
"""
|
|
1596
|
+
implementation = None
|
|
1597
|
+
if control_id in current_implementations_dict.keys():
|
|
1598
|
+
logger.info(
|
|
1599
|
+
f"Updating Implementation: {control_id}",
|
|
1600
|
+
"control-implementation",
|
|
1601
|
+
"control",
|
|
1602
|
+
)
|
|
1603
|
+
implementation = current_implementations_dict.get(control_id)
|
|
1604
|
+
implementation.status = implementation_status
|
|
1605
|
+
implementation.responsibility = responsibility
|
|
1606
|
+
implementation.implementation = implementation_text
|
|
1607
|
+
implementation.lastUpdatedById = app.config.get("userId")
|
|
1608
|
+
implementation.systemRoleId = (
|
|
1609
|
+
ctrl_roles.get(friendly_control_id)[0]
|
|
1610
|
+
if isinstance(ctrl_roles, dict)
|
|
1611
|
+
and friendly_control_id in ctrl_roles.keys()
|
|
1612
|
+
and ctrl_roles.get(friendly_control_id)[0]
|
|
1613
|
+
else None
|
|
1614
|
+
)
|
|
1615
|
+
update_implmentations.append(implementation)
|
|
1616
|
+
else:
|
|
1617
|
+
mapping_control = mapping.get(control_id)
|
|
1618
|
+
logger.info(
|
|
1619
|
+
f"Creating Implementation: {control_id}",
|
|
1620
|
+
record_type="control",
|
|
1621
|
+
model_layer="control-implementation",
|
|
1622
|
+
)
|
|
1623
|
+
implementation = ControlImplementation(
|
|
1624
|
+
parentId=regscale_ssp["id"],
|
|
1625
|
+
parentModule="securityplans",
|
|
1626
|
+
controlOwnerId=app.config["userId"],
|
|
1627
|
+
status=implementation_status,
|
|
1628
|
+
controlID=mapping_control.controlID,
|
|
1629
|
+
responsibility=responsibility,
|
|
1630
|
+
implementation=implementation_text,
|
|
1631
|
+
systemRoleId=(
|
|
1632
|
+
ctrl_roles.get(friendly_control_id)[0]
|
|
1633
|
+
if isinstance(ctrl_roles, dict)
|
|
1634
|
+
and friendly_control_id in ctrl_roles.keys()
|
|
1635
|
+
and ctrl_roles.get(friendly_control_id)[0]
|
|
1636
|
+
else None
|
|
1637
|
+
),
|
|
1638
|
+
)
|
|
1639
|
+
implementation = implementation.create()
|
|
1640
|
+
new_implementations.append(implementation)
|
|
1641
|
+
return implementation
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
def handle_requirements(
|
|
1645
|
+
app: Application,
|
|
1646
|
+
api: Api,
|
|
1647
|
+
fedramp_control: Any,
|
|
1648
|
+
mapping: List[ProfileMapping],
|
|
1649
|
+
friendly_control_id: str,
|
|
1650
|
+
implementation_status: str,
|
|
1651
|
+
regscale_ssp: Dict,
|
|
1652
|
+
) -> None:
|
|
1653
|
+
"""
|
|
1654
|
+
Handle requirements
|
|
1655
|
+
|
|
1656
|
+
:param Application app: Application instance
|
|
1657
|
+
:param Api api: API instance
|
|
1658
|
+
:param Any fedramp_control: FedrampControl object
|
|
1659
|
+
:param List[ProfileMapping] mapping: Mapping
|
|
1660
|
+
:param str friendly_control_id: Friendly control id
|
|
1661
|
+
:param str implementation_status: Implementation status
|
|
1662
|
+
:param Dict regscale_ssp: RegScale ssp
|
|
1663
|
+
:rtype: None
|
|
1664
|
+
"""
|
|
1665
|
+
parent_security_control_id = [
|
|
1666
|
+
control["controlID"] for control in mapping if control["controlId"] == friendly_control_id.split()[0]
|
|
1667
|
+
][0]
|
|
1668
|
+
current_imps = get_current_implementations(app=app, regscale_id=regscale_ssp["id"])
|
|
1669
|
+
parent_security_control = [imp for imp in current_imps if imp["controlID"] == parent_security_control_id][0]
|
|
1670
|
+
|
|
1671
|
+
for part in fedramp_control.parts:
|
|
1672
|
+
implementation_text = fedramp_control.part(part).text
|
|
1673
|
+
title = f"{friendly_control_id.split()[0]} - Req. {part}"
|
|
1674
|
+
requirement = Requirement(
|
|
1675
|
+
id=0,
|
|
1676
|
+
description=implementation_text.split("\n")[0],
|
|
1677
|
+
implementation=implementation_text,
|
|
1678
|
+
title=title,
|
|
1679
|
+
lastUpdatedById=app.config["userId"],
|
|
1680
|
+
status=implementation_status,
|
|
1681
|
+
controlID=parent_security_control_id,
|
|
1682
|
+
parentId=parent_security_control["id"],
|
|
1683
|
+
parentModule="controls",
|
|
1684
|
+
requirementOwnerId=app.config["userId"],
|
|
1685
|
+
createdById=app.config["userId"],
|
|
1686
|
+
)
|
|
1687
|
+
|
|
1688
|
+
existing_requirement = api.get(
|
|
1689
|
+
url=app.config["domain"] + f"/api/requirements/getByParent/{parent_security_control['id']}/controls"
|
|
1690
|
+
).json()
|
|
1691
|
+
|
|
1692
|
+
if title not in {req["title"] for req in existing_requirement}:
|
|
1693
|
+
logger.info("Posting Requirement: %s", title)
|
|
1694
|
+
post_regscale_object(
|
|
1695
|
+
api=api,
|
|
1696
|
+
config=app.config,
|
|
1697
|
+
obj=requirement,
|
|
1698
|
+
endpoint="requirements",
|
|
1699
|
+
)
|
|
1700
|
+
else:
|
|
1701
|
+
logger.info("Requirement %s already exists, skipping...", title)
|
|
1702
|
+
|
|
1703
|
+
|
|
1704
|
+
def format_parameter_name(fedramp_control: str, param_number: int) -> str:
|
|
1705
|
+
"""
|
|
1706
|
+
Forma parameter anem
|
|
1707
|
+
|
|
1708
|
+
:param str fedramp_control: root control name from catalog
|
|
1709
|
+
:param int param_number: number of parameter (rev4)
|
|
1710
|
+
:return: formatted parameter name
|
|
1711
|
+
:rtype: str
|
|
1712
|
+
"""
|
|
1713
|
+
pname = str(fedramp_control)
|
|
1714
|
+
pname = pname + "_prm_"
|
|
1715
|
+
pname = pname + str(param_number)
|
|
1716
|
+
pname = pname.replace("(", ".")
|
|
1717
|
+
pname = pname.replace(")", "")
|
|
1718
|
+
pname = pname.replace(" ", "")
|
|
1719
|
+
pname = pname.lower()
|
|
1720
|
+
return pname
|
|
1721
|
+
|
|
1722
|
+
|
|
1723
|
+
def handle_parameters(fedramp_control: Any, control_imp: ControlImplementation) -> None:
|
|
1724
|
+
"""
|
|
1725
|
+
Handle parameters
|
|
1726
|
+
|
|
1727
|
+
:param Any fedramp_control: FedrampControl object
|
|
1728
|
+
:param ControlImplementation control_imp: ControlImplementation object
|
|
1729
|
+
:rtype: None
|
|
1730
|
+
"""
|
|
1731
|
+
pnum = 0
|
|
1732
|
+
existing_params = Parameter.get_all_by_parent(parent_id=control_imp.id)
|
|
1733
|
+
existing_param_names_dict = {param.name: param for param in existing_params}
|
|
1734
|
+
base_control_params = ControlParameter.get_by_control(control_id=control_imp.controlID)
|
|
1735
|
+
base_control_params_dict = {param.parameterId: param for param in base_control_params}
|
|
1736
|
+
|
|
1737
|
+
for parameter in fedramp_control.parameters:
|
|
1738
|
+
pnum = pnum + 1
|
|
1739
|
+
try:
|
|
1740
|
+
param_dict = get_parameter_value(parameter)
|
|
1741
|
+
pname = format_parameter_name(str(fedramp_control), pnum)
|
|
1742
|
+
control_param_name = pname
|
|
1743
|
+
base_control_param = base_control_params_dict.get(control_param_name)
|
|
1744
|
+
if base_control_param:
|
|
1745
|
+
if not existing_params or control_param_name not in existing_param_names_dict:
|
|
1746
|
+
Parameter(
|
|
1747
|
+
controlImplementationId=control_imp.id,
|
|
1748
|
+
name=control_param_name,
|
|
1749
|
+
value=param_dict.get("value"),
|
|
1750
|
+
parentParameterId=base_control_param.id,
|
|
1751
|
+
).create()
|
|
1752
|
+
else:
|
|
1753
|
+
existing_param = existing_param_names_dict.get(control_param_name)
|
|
1754
|
+
if existing_param.name == control_param_name:
|
|
1755
|
+
existing_param.value = param_dict.get("value")
|
|
1756
|
+
existing_param.parentParameterId = base_control_param.id
|
|
1757
|
+
existing_param.save()
|
|
1758
|
+
except Exception as e:
|
|
1759
|
+
logger.warning("Unable to map parameter %s to RegScale: %s", parameter, e)
|
|
1760
|
+
|
|
1761
|
+
|
|
1762
|
+
def process_controls(
|
|
1763
|
+
app: Application,
|
|
1764
|
+
api: Api,
|
|
1765
|
+
ssp_obj: dict,
|
|
1766
|
+
regscale_ssp: dict,
|
|
1767
|
+
mapping: List[ProfileMapping],
|
|
1768
|
+
ctrl_roles: dict,
|
|
1769
|
+
current_imps: List[ControlImplementation],
|
|
1770
|
+
missing_controls: Optional[list],
|
|
1771
|
+
) -> Tuple[List[str], List[str], List[str], List, List]:
|
|
1772
|
+
"""
|
|
1773
|
+
Process controls
|
|
1774
|
+
|
|
1775
|
+
:param Application app: Application instance
|
|
1776
|
+
:param Api api: API instance
|
|
1777
|
+
:param dict ssp_obj: SSP object
|
|
1778
|
+
:param dict regscale_ssp: RegScale ssp
|
|
1779
|
+
:param dict mapping: Mapping
|
|
1780
|
+
:param dict ctrl_roles: Control roles
|
|
1781
|
+
:param list current_imps: List of current implementations
|
|
1782
|
+
:param Optional[list] missing_controls: List of missing controls in the selected profile
|
|
1783
|
+
:return Tuple[List[str], List[str], List[str], List]:
|
|
1784
|
+
List of mapped controls
|
|
1785
|
+
List of unmapped controls
|
|
1786
|
+
List of implemented controls
|
|
1787
|
+
List new implementations
|
|
1788
|
+
List of updated implementations
|
|
1789
|
+
"""
|
|
1790
|
+
mapped_controls_log = []
|
|
1791
|
+
unmapped_controls_log = []
|
|
1792
|
+
implemented_controls_log = []
|
|
1793
|
+
new_implementations = []
|
|
1794
|
+
update_implmentations = []
|
|
1795
|
+
has_requirements = False
|
|
1796
|
+
if not missing_controls:
|
|
1797
|
+
missing_controls = []
|
|
1798
|
+
|
|
1799
|
+
logger.info(f"Processing {len(ssp_obj.control_list)} controls..")
|
|
1800
|
+
mapping_dict = {control.control.controlId: control for control in mapping}
|
|
1801
|
+
for fedramp_control in ssp_obj.control_list:
|
|
1802
|
+
responsibility, implementation_status = get_responsibility_and_status(fedramp_control)
|
|
1803
|
+
friendly_control_id = get_friendly_control_id(fedramp_control.number)
|
|
1804
|
+
control_id = find_control_in_mapping(mapping, friendly_control_id)
|
|
1805
|
+
logger.info(f"Processing Control: {friendly_control_id.upper()}")
|
|
1806
|
+
if not control_id or friendly_control_id in missing_controls:
|
|
1807
|
+
unmapped_controls_log.append(friendly_control_id.upper())
|
|
1808
|
+
continue
|
|
1809
|
+
|
|
1810
|
+
implementation_text = get_implementation_text(fedramp_control)
|
|
1811
|
+
current_implementations_dict = {c.control.controlId: c for c in current_imps}
|
|
1812
|
+
control_imp = handle_control_implementation(
|
|
1813
|
+
app,
|
|
1814
|
+
regscale_ssp,
|
|
1815
|
+
control_id,
|
|
1816
|
+
responsibility,
|
|
1817
|
+
implementation_status,
|
|
1818
|
+
implementation_text,
|
|
1819
|
+
ctrl_roles,
|
|
1820
|
+
friendly_control_id,
|
|
1821
|
+
new_implementations,
|
|
1822
|
+
update_implmentations,
|
|
1823
|
+
current_implementations_dict,
|
|
1824
|
+
mapping_dict,
|
|
1825
|
+
)
|
|
1826
|
+
|
|
1827
|
+
if "Req" in fedramp_control.number:
|
|
1828
|
+
handle_requirements(
|
|
1829
|
+
app,
|
|
1830
|
+
api,
|
|
1831
|
+
fedramp_control,
|
|
1832
|
+
mapping,
|
|
1833
|
+
friendly_control_id,
|
|
1834
|
+
implementation_status,
|
|
1835
|
+
regscale_ssp,
|
|
1836
|
+
)
|
|
1837
|
+
has_requirements = True
|
|
1838
|
+
|
|
1839
|
+
if control_imp:
|
|
1840
|
+
handle_parameters(fedramp_control, control_imp)
|
|
1841
|
+
|
|
1842
|
+
if has_requirements:
|
|
1843
|
+
has_requirements = False # Reset for the next control
|
|
1844
|
+
|
|
1845
|
+
ControlImplementation.batch_update(items=update_implmentations)
|
|
1846
|
+
return (
|
|
1847
|
+
mapped_controls_log,
|
|
1848
|
+
unmapped_controls_log,
|
|
1849
|
+
implemented_controls_log,
|
|
1850
|
+
new_implementations,
|
|
1851
|
+
update_implmentations,
|
|
1852
|
+
)
|
|
1853
|
+
|
|
1854
|
+
|
|
1855
|
+
def check_control_list_length(ssp_obj: SSP, mapping: dict) -> Optional[list]:
|
|
1856
|
+
"""
|
|
1857
|
+
Check control list length
|
|
1858
|
+
|
|
1859
|
+
:param SSP ssp_obj: SSP object
|
|
1860
|
+
:param dict mapping: Mapping
|
|
1861
|
+
:return: List of missing controls, if found
|
|
1862
|
+
:rtype: Optional[list]
|
|
1863
|
+
"""
|
|
1864
|
+
profile_control_ids = [get_friendly_control_id(item.control.controlId) for item in mapping]
|
|
1865
|
+
parsed_control_ids = [get_friendly_control_id(control.number) for control in ssp_obj.control_list]
|
|
1866
|
+
if len(ssp_obj.control_list) > len(mapping):
|
|
1867
|
+
missing_controls = set(profile_control_ids) - set(parsed_control_ids)
|
|
1868
|
+
logger.error(
|
|
1869
|
+
f"There are more controls in the source document ({len(ssp_obj.control_list)}) than in the base profile ({len(mapping)})!",
|
|
1870
|
+
record_type="implementations",
|
|
1871
|
+
model_layer="implementations",
|
|
1872
|
+
)
|
|
1873
|
+
logger.error(
|
|
1874
|
+
f"Extra controls found in source document and missing from base profile: {', '.join(missing_controls)}",
|
|
1875
|
+
record_type="implementations",
|
|
1876
|
+
model_layer="implementations",
|
|
1877
|
+
)
|
|
1878
|
+
return missing_controls
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
def log_controls_info(ssp_obj: dict, current_imps: List[dict]) -> None:
|
|
1882
|
+
"""
|
|
1883
|
+
Log controls info
|
|
1884
|
+
|
|
1885
|
+
:param dict ssp_obj: SSP object
|
|
1886
|
+
:param List[dict] current_imps: List of current implementations
|
|
1887
|
+
:return None:
|
|
1888
|
+
:rtype None:
|
|
1889
|
+
"""
|
|
1890
|
+
logger.info(
|
|
1891
|
+
f"Attempting to post {len(ssp_obj.control_list)} controls from this FedRAMP SSP Document to RegScale!",
|
|
1892
|
+
record_type="control",
|
|
1893
|
+
model_layer="control-implementation",
|
|
1894
|
+
)
|
|
1895
|
+
if len(current_imps) > 0:
|
|
1896
|
+
logger.info(
|
|
1897
|
+
f"This RegScale Security plan already has {len(current_imps)} implementations..",
|
|
1898
|
+
record_type="control",
|
|
1899
|
+
model_layer="control-implementation",
|
|
1900
|
+
)
|
|
1901
|
+
|
|
1902
|
+
|
|
1903
|
+
def save_control_logs(
|
|
1904
|
+
mapped_controls_log: List[str],
|
|
1905
|
+
unmapped_controls_log: List[str],
|
|
1906
|
+
implemented_controls_log: List[str],
|
|
1907
|
+
) -> None:
|
|
1908
|
+
"""
|
|
1909
|
+
Save control logs
|
|
1910
|
+
|
|
1911
|
+
:param List[str] mapped_controls_log: List of mapped controls
|
|
1912
|
+
:param List[str] unmapped_controls_log: List of unmapped controls
|
|
1913
|
+
:param List[str] implemented_controls_log: List of implemented controls
|
|
1914
|
+
:return None:
|
|
1915
|
+
:rtype None:
|
|
1916
|
+
"""
|
|
1917
|
+
check_file_path("./artifacts", output=False)
|
|
1918
|
+
with open("./artifacts/control_implementation.log", "w") as f:
|
|
1919
|
+
f.write("|*** Unmapped Controls ***|\n")
|
|
1920
|
+
f.write(NEW_LINE_OUTPUT)
|
|
1921
|
+
f.write("\n".join(unmapped_controls_log))
|
|
1922
|
+
f.write(NEW_LINE_OUTPUT)
|
|
1923
|
+
f.write("|*** Mapped Controls ***|\n")
|
|
1924
|
+
f.write(NEW_LINE_OUTPUT)
|
|
1925
|
+
f.write("\n".join(mapped_controls_log))
|
|
1926
|
+
f.write(NEW_LINE_OUTPUT)
|
|
1927
|
+
f.write("|*** Already Implemented Controls ***|\n")
|
|
1928
|
+
f.write(NEW_LINE_OUTPUT)
|
|
1929
|
+
f.write("\n".join(implemented_controls_log))
|
|
1930
|
+
f.write(NEW_LINE_OUTPUT)
|
|
1931
|
+
|
|
1932
|
+
|
|
1933
|
+
def get_parameter_value(param: str) -> Dict:
|
|
1934
|
+
"""
|
|
1935
|
+
Get the value of a Parameter
|
|
1936
|
+
|
|
1937
|
+
:param str param: Parameter as a string
|
|
1938
|
+
:return: Dictionary of parameter name and value
|
|
1939
|
+
:rtype: Dict
|
|
1940
|
+
"""
|
|
1941
|
+
param_dict = dict()
|
|
1942
|
+
if ":" in param:
|
|
1943
|
+
param_dict["name"] = param.split(":")[0]
|
|
1944
|
+
param_dict["value"] = param.split(":")[1] if len(param.split(":")) > 1 else param
|
|
1945
|
+
else:
|
|
1946
|
+
param_dict["name"] = param
|
|
1947
|
+
param_dict["value"] = param
|
|
1948
|
+
return param_dict
|
|
1949
|
+
|
|
1950
|
+
|
|
1951
|
+
def load_non_matched_profile_controls(app: Application, regscale_ssp: dict, mapping: List[ProfileMapping]) -> List:
|
|
1952
|
+
"""Load controls from a given profile mapping that are not matched by the document
|
|
1953
|
+
|
|
1954
|
+
:param Application app: Application instance
|
|
1955
|
+
:param dict regscale_ssp: RegScale SSP as a dictionary
|
|
1956
|
+
:param List[ProfileMapping] mapping: Profile mapping
|
|
1957
|
+
:return: List of newly created implementations
|
|
1958
|
+
:rtype: List
|
|
1959
|
+
"""
|
|
1960
|
+
api = Api()
|
|
1961
|
+
current_imps = get_current_implementations(app, regscale_ssp["id"])
|
|
1962
|
+
if ssp := [
|
|
1963
|
+
ssp
|
|
1964
|
+
for ssp in api.get(url=urljoin(app.config["domain"], SSP_URL_SUFFIX)).json()
|
|
1965
|
+
if ssp["title"] == regscale_ssp["systemName"]
|
|
1966
|
+
]:
|
|
1967
|
+
created_imps = []
|
|
1968
|
+
ssp_id = ssp[0]["id"]
|
|
1969
|
+
existing_controls = {imp["controlID"] for imp in current_imps}
|
|
1970
|
+
controls_to_add = [control for control in mapping if control.controlID not in existing_controls]
|
|
1971
|
+
logger.info(
|
|
1972
|
+
f"Adding {len(controls_to_add)} additional controls from profile",
|
|
1973
|
+
record_type="control",
|
|
1974
|
+
model_layer="control-implementation",
|
|
1975
|
+
)
|
|
1976
|
+
existing_control_ids = {imp["controlID"] for imp in current_imps}
|
|
1977
|
+
for control in controls_to_add:
|
|
1978
|
+
if isinstance(control, dict):
|
|
1979
|
+
control_id = control["controlID"]
|
|
1980
|
+
elif isinstance(control, ProfileMapping):
|
|
1981
|
+
control_id = control.controlID
|
|
1982
|
+
else:
|
|
1983
|
+
continue
|
|
1984
|
+
if control_id not in existing_control_ids:
|
|
1985
|
+
implementation = ControlImplementation(
|
|
1986
|
+
parentId=ssp_id,
|
|
1987
|
+
parentModule="securityplans",
|
|
1988
|
+
controlOwnerId=app.config["userId"],
|
|
1989
|
+
status="Not Implemented",
|
|
1990
|
+
controlID=control_id,
|
|
1991
|
+
responsibility=None,
|
|
1992
|
+
implementation=None,
|
|
1993
|
+
).dict()
|
|
1994
|
+
logger.info(
|
|
1995
|
+
f"Posting implementation: {control_id}.",
|
|
1996
|
+
record_type="control",
|
|
1997
|
+
model_layer="control-implementation",
|
|
1998
|
+
)
|
|
1999
|
+
created_imps.append(post_regscale_object(api, app.config, implementation))
|
|
2000
|
+
return created_imps
|
|
2001
|
+
|
|
2002
|
+
|
|
2003
|
+
def post_attachments(api: Api, link: str, regscale_ssp: dict) -> None:
|
|
2004
|
+
"""
|
|
2005
|
+
Download and post Attachments to RegScale
|
|
2006
|
+
|
|
2007
|
+
:param Api api: API object
|
|
2008
|
+
:param str link: link to download file onary of RegScale SSP
|
|
2009
|
+
:param dict regscale_ssp: RegScale SSP
|
|
2010
|
+
:rtype: None
|
|
2011
|
+
"""
|
|
2012
|
+
try:
|
|
2013
|
+
dl_path = download_file(link["link"])
|
|
2014
|
+
logger.info(
|
|
2015
|
+
f"Posting linked image to RegScale.. {link}",
|
|
2016
|
+
record_type="attachments",
|
|
2017
|
+
model_layer="attachments",
|
|
2018
|
+
)
|
|
2019
|
+
File.upload_file_to_regscale(
|
|
2020
|
+
file_name=(dl_path.absolute()),
|
|
2021
|
+
parent_id=regscale_ssp["id"],
|
|
2022
|
+
parent_module="securityplans",
|
|
2023
|
+
api=api,
|
|
2024
|
+
)
|
|
2025
|
+
|
|
2026
|
+
except Exception as ex:
|
|
2027
|
+
logger.warning(
|
|
2028
|
+
f"Unable to download file: {link}\n{ex}",
|
|
2029
|
+
record_type="attachments",
|
|
2030
|
+
model_layer="attachments",
|
|
2031
|
+
)
|
|
2032
|
+
|
|
2033
|
+
|
|
2034
|
+
def posted_embedded_attachments(api: Api, file_path: Path, regscale_ssp: dict) -> None:
|
|
2035
|
+
"""
|
|
2036
|
+
Find and post embedded picture files to RegScale
|
|
2037
|
+
|
|
2038
|
+
:param Api api: API object
|
|
2039
|
+
:param Path file_path: file_path
|
|
2040
|
+
:param dict regscale_ssp: RegScale SSP
|
|
2041
|
+
:return None:
|
|
2042
|
+
"""
|
|
2043
|
+
filename = file_path
|
|
2044
|
+
with zipfile.ZipFile(filename, mode="r") as archive:
|
|
2045
|
+
file_dump_path = gettempdir() + os.sep + "imagedump"
|
|
2046
|
+
for file in archive.filelist:
|
|
2047
|
+
if file.filename.startswith("word/media/") and file.file_size > 200000: # 200KB filter
|
|
2048
|
+
archive.extract(file, path=file_dump_path)
|
|
2049
|
+
# Create directories in case they do not exist.
|
|
2050
|
+
media_path = file_dump_path + os.sep + "word" + os.sep + "media"
|
|
2051
|
+
if not os.path.exists(media_path):
|
|
2052
|
+
os.makedirs(media_path)
|
|
2053
|
+
for filename in os.listdir(file_dump_path + os.sep + "word" + os.sep + "media"):
|
|
2054
|
+
full_file_path = os.path.join(file_dump_path + os.sep + "word" + os.sep + "media", filename)
|
|
2055
|
+
if os.path.isfile(full_file_path):
|
|
2056
|
+
logger.info(
|
|
2057
|
+
f"Posting embedded image to RegScale... {full_file_path}",
|
|
2058
|
+
record_type="attachments",
|
|
2059
|
+
model_layer="attachments",
|
|
2060
|
+
)
|
|
2061
|
+
try:
|
|
2062
|
+
File.upload_file_to_regscale(
|
|
2063
|
+
file_name=full_file_path,
|
|
2064
|
+
parent_id=regscale_ssp["id"],
|
|
2065
|
+
parent_module="securityplans",
|
|
2066
|
+
api=api,
|
|
2067
|
+
)
|
|
2068
|
+
except Exception as e:
|
|
2069
|
+
logger.warning(
|
|
2070
|
+
f"Unable to upload image -- continuing {e}",
|
|
2071
|
+
record_type="attachements",
|
|
2072
|
+
model_layer="attachments",
|
|
2073
|
+
)
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
def post_links(
|
|
2077
|
+
config: Dict, api: Api, document: Document, file_path: Path, regscale_ssp: Dict, post_embeds: bool = True
|
|
2078
|
+
) -> None:
|
|
2079
|
+
"""
|
|
2080
|
+
Use XPath to pull data from XML tables and post links to RegScale.
|
|
2081
|
+
|
|
2082
|
+
:param dict config: Application config
|
|
2083
|
+
:param Api api: Api object
|
|
2084
|
+
:param Document document: Word Document
|
|
2085
|
+
:param Path file_path: File path
|
|
2086
|
+
:param dict regscale_ssp: RegScale SSP
|
|
2087
|
+
:param bool post_embeds: Whether to post embedded items to RegScale
|
|
2088
|
+
:rtype: None
|
|
2089
|
+
"""
|
|
2090
|
+
# Post embedded attachments if needed
|
|
2091
|
+
if post_embeds:
|
|
2092
|
+
posted_embedded_attachments(api, file_path, regscale_ssp)
|
|
2093
|
+
|
|
2094
|
+
# Extract and post attachments from the document tables
|
|
2095
|
+
attachments = extract_attachments_from_document(document)
|
|
2096
|
+
|
|
2097
|
+
# Fetch existing links from the API
|
|
2098
|
+
existing_links = api.get(f"{config['domain']}/api/links/getAllByParent/{regscale_ssp['id']}/securityplans").json()
|
|
2099
|
+
logger.info(f"Found {len(existing_links)} existing links", record_type="links", model_layer="links")
|
|
2100
|
+
|
|
2101
|
+
# Post new links to RegScale
|
|
2102
|
+
post_new_links(api, config, regscale_ssp, attachments, existing_links)
|
|
2103
|
+
|
|
2104
|
+
|
|
2105
|
+
def extract_attachments_from_document(document: Document) -> List[Dict[str, str]]:
|
|
2106
|
+
"""
|
|
2107
|
+
Extracts attachments (links) from a document by parsing its tables.
|
|
2108
|
+
|
|
2109
|
+
:param Document document: Word Document to parse
|
|
2110
|
+
:return: List of dictionaries with attachment info
|
|
2111
|
+
"""
|
|
2112
|
+
attachments = []
|
|
2113
|
+
titles = []
|
|
2114
|
+
|
|
2115
|
+
for table in document.tables:
|
|
2116
|
+
if table._cells and "Identification Number" in table.cell(0, 0).text.strip():
|
|
2117
|
+
titles = extract_titles_from_table(table)
|
|
2118
|
+
|
|
2119
|
+
for link in table._element.xpath(".//w:hyperlink"):
|
|
2120
|
+
inner_run = link.xpath("w:r", namespaces=link.nsmap)[0]
|
|
2121
|
+
title = titles.pop()["title"] if titles else inner_run.text
|
|
2122
|
+
|
|
2123
|
+
# Extract the relationship ID and target URL
|
|
2124
|
+
r_id = link.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id")
|
|
2125
|
+
link_url = document._part.rels[r_id]._target
|
|
2126
|
+
attachments.append({"title": title, "link": link_url})
|
|
2127
|
+
|
|
2128
|
+
return attachments
|
|
2129
|
+
|
|
2130
|
+
|
|
2131
|
+
def extract_titles_from_table(table) -> List[Dict[str, str]]:
|
|
2132
|
+
"""
|
|
2133
|
+
Extracts titles from a table by parsing each row's text.
|
|
2134
|
+
|
|
2135
|
+
:param table: The table from the Word document to parse.
|
|
2136
|
+
:return: List of dictionaries with title and link data
|
|
2137
|
+
"""
|
|
2138
|
+
titles = []
|
|
2139
|
+
previous_text = None
|
|
2140
|
+
current_data = {}
|
|
2141
|
+
|
|
2142
|
+
for _, element in enumerate(table._element.xpath(".//w:r/w:t")):
|
|
2143
|
+
current_text = element.text.strip()
|
|
2144
|
+
|
|
2145
|
+
if current_text:
|
|
2146
|
+
if previous_text and previous_text.lower() == "link":
|
|
2147
|
+
current_data["id"] = current_text
|
|
2148
|
+
elif validate_date_str(current_text):
|
|
2149
|
+
current_data["title"] = f"{current_data.get('title', '')} {previous_text}".strip()
|
|
2150
|
+
current_data["date"] = current_text
|
|
2151
|
+
elif "date" in current_data:
|
|
2152
|
+
current_data["link"] = current_text
|
|
2153
|
+
titles.append(current_data.copy())
|
|
2154
|
+
current_data = {}
|
|
2155
|
+
|
|
2156
|
+
previous_text = current_text if len(current_data) != 4 else "link"
|
|
2157
|
+
|
|
2158
|
+
titles.reverse()
|
|
2159
|
+
return titles
|
|
2160
|
+
|
|
2161
|
+
|
|
2162
|
+
def post_new_links(
|
|
2163
|
+
api: Api, config: Dict, regscale_ssp: Dict, attachments: List[Dict[str, str]], existing_links: List[Dict]
|
|
2164
|
+
) -> None:
|
|
2165
|
+
"""
|
|
2166
|
+
Posts new links to RegScale if they do not already exist.
|
|
2167
|
+
|
|
2168
|
+
:param Api api: Api object
|
|
2169
|
+
:param dict config: Application config
|
|
2170
|
+
:param dict regscale_ssp: RegScale SSP
|
|
2171
|
+
:param List[Dict[str, str]] attachments: List of extracted links
|
|
2172
|
+
:param List[Dict] existing_links: List of existing links from RegScale
|
|
2173
|
+
:rtype: None
|
|
2174
|
+
"""
|
|
2175
|
+
existing_link_urls = {link["url"] for link in existing_links}
|
|
2176
|
+
|
|
2177
|
+
for attachment in attachments:
|
|
2178
|
+
link_data = {
|
|
2179
|
+
"id": 0,
|
|
2180
|
+
"url": attachment["link"],
|
|
2181
|
+
"title": attachment["title"],
|
|
2182
|
+
"parentID": regscale_ssp["id"],
|
|
2183
|
+
"parentModule": "securityplans",
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
if attachment["link"] not in existing_link_urls:
|
|
2187
|
+
post_regscale_object(api, config, link_data, endpoint="links")
|
|
2188
|
+
post_attachments(api=api, link=attachment, regscale_ssp=regscale_ssp)
|
|
2189
|
+
else:
|
|
2190
|
+
logger.info(
|
|
2191
|
+
f"{attachment['link']} already exists in Security Plan, skipping...",
|
|
2192
|
+
record_type="links",
|
|
2193
|
+
model_layer="links",
|
|
2194
|
+
)
|
|
2195
|
+
|
|
2196
|
+
|
|
2197
|
+
def validate_date_str(date_text: str, fmt: str = "%m/%d/%Y") -> bool:
|
|
2198
|
+
"""
|
|
2199
|
+
Validate provided text is a date in mm/dd/yyyy format
|
|
2200
|
+
|
|
2201
|
+
:param str date_text: Date as a string
|
|
2202
|
+
:param str fmt: date format of the date_text, defaults to %m/%d/%Y
|
|
2203
|
+
:return: Whether provided text can be converted as a date
|
|
2204
|
+
:rtype: bool
|
|
2205
|
+
"""
|
|
2206
|
+
try:
|
|
2207
|
+
datetime.strptime(date_text, fmt)
|
|
2208
|
+
except ValueError:
|
|
2209
|
+
return False
|
|
2210
|
+
return True
|
|
2211
|
+
|
|
2212
|
+
|
|
2213
|
+
def get_text_between_headers(full_text: list, start_header: str, end_header: str) -> str:
|
|
2214
|
+
"""
|
|
2215
|
+
Parses text from a document
|
|
2216
|
+
|
|
2217
|
+
:param list full_text: Full text of the document
|
|
2218
|
+
:param str start_header: Starting header
|
|
2219
|
+
:param str end_header: Where the text ends
|
|
2220
|
+
:return: String parsed from document
|
|
2221
|
+
:rtype: str
|
|
2222
|
+
"""
|
|
2223
|
+
try:
|
|
2224
|
+
start_index = next(i for i, j in enumerate(full_text) if j.lower().strip() == start_header.lower())
|
|
2225
|
+
end_index = next(i for i, j in enumerate(full_text) if j.lower().strip() == end_header.lower())
|
|
2226
|
+
logger.debug(f"{start_index=}, {end_index=}")
|
|
2227
|
+
found_text = " ".join(full_text[start_index + 1 : end_index])
|
|
2228
|
+
logger.debug(f"{found_text=}")
|
|
2229
|
+
return found_text
|
|
2230
|
+
except StopIteration:
|
|
2231
|
+
# If the start header is not found, return an empty string or raise an error
|
|
2232
|
+
return ""
|
|
2233
|
+
|
|
2234
|
+
|
|
2235
|
+
def replace_content_control(element, namespaces=None):
|
|
2236
|
+
"""Replaces xml w:sdt tags with the elements found under w:sdtContent
|
|
2237
|
+
:param element: lxml.etree.Element
|
|
2238
|
+
:param namespaces: dict
|
|
2239
|
+
"""
|
|
2240
|
+
|
|
2241
|
+
kwargs = {} if not namespaces else {"namespaces": namespaces} # xpath args can vary for docx objects vs lxml.etree
|
|
2242
|
+
sdt_elements = element.xpath(".//w:sdt", **kwargs) # Get all w:sdt elements
|
|
2243
|
+
for e in sdt_elements:
|
|
2244
|
+
inner_elements = e.xpath(".//w:sdt", namespaces=e.nsmap)
|
|
2245
|
+
for inner_element in inner_elements:
|
|
2246
|
+
replace_content_control(inner_element, namespaces=e.nsmap)
|
|
2247
|
+
content_elements = e.xpath(".//w:sdtContent/*", namespaces=e.nsmap) # Get all elements under sdtContent
|
|
2248
|
+
for c in content_elements:
|
|
2249
|
+
e.addprevious(c) # Add content outside of sdt tag
|
|
2250
|
+
parent = e.getparent()
|
|
2251
|
+
if parent is not None:
|
|
2252
|
+
parent.remove(e) # Remove sdt tag
|
|
2253
|
+
|
|
2254
|
+
|
|
2255
|
+
def gather_stakeholders(tables: list, regscale_ssp: dict, document: Document):
|
|
2256
|
+
"""Gather Stakeholders
|
|
2257
|
+
|
|
2258
|
+
:param list tables: A list of tables from the XML document.
|
|
2259
|
+
:param dict regscale_ssp: A dict of RegScale SSP data.
|
|
2260
|
+
:param SSP ssp: Object of docx SSP data.
|
|
2261
|
+
"""
|
|
2262
|
+
app = Application()
|
|
2263
|
+
api = Api()
|
|
2264
|
+
pocs = collect_points_of_contact(tables, document)
|
|
2265
|
+
existing_stakeholders = fetch_existing_stakeholders(api, app.config, regscale_ssp)
|
|
2266
|
+
logger.info(
|
|
2267
|
+
f"Found {len(existing_stakeholders)} existing stakeholders",
|
|
2268
|
+
record_type="stakeholders",
|
|
2269
|
+
model_layer="stakeholders",
|
|
2270
|
+
)
|
|
2271
|
+
filtered_pocs = filter_valid_pocs(pocs)
|
|
2272
|
+
|
|
2273
|
+
insert_new_stakeholders(api, app.config, filtered_pocs, existing_stakeholders, regscale_ssp)
|
|
2274
|
+
|
|
2275
|
+
|
|
2276
|
+
def collect_points_of_contact(tables: list, document: Document) -> List:
|
|
2277
|
+
"""Collect various points of contact
|
|
2278
|
+
|
|
2279
|
+
:param list tables: A list of tables from the XML document.
|
|
2280
|
+
:param SSP ssp: SSP Object of docx SSP data.
|
|
2281
|
+
:return: A list of points of contact
|
|
2282
|
+
:rtype: List
|
|
2283
|
+
"""
|
|
2284
|
+
pocs = list()
|
|
2285
|
+
pocs.append(extract_management_poc(tables, pocs))
|
|
2286
|
+
pocs.extend(extract_information_poc(tables, document))
|
|
2287
|
+
pocs.append(extract_csp_poc(tables, pocs))
|
|
2288
|
+
return pocs
|
|
2289
|
+
|
|
2290
|
+
|
|
2291
|
+
def fetch_existing_stakeholders(api: Api, config: dict, regscale_ssp: dict) -> list:
|
|
2292
|
+
"""Fetch existing stakeholders from the API
|
|
2293
|
+
|
|
2294
|
+
:param Api api: An API instance
|
|
2295
|
+
:param dict config: A configuration dictionary
|
|
2296
|
+
:param dict regscale_ssp: A dict of RegScale SSP data.
|
|
2297
|
+
:return: A list of existing stakeholders
|
|
2298
|
+
:rtype: list
|
|
2299
|
+
"""
|
|
2300
|
+
url = urljoin(
|
|
2301
|
+
config.get("domain"),
|
|
2302
|
+
f"/api/stakeholders/getAllByParent/{str(regscale_ssp.get('id'))}/securityplans",
|
|
2303
|
+
)
|
|
2304
|
+
response = api.get(url=url, headers={"Content-Type": "application/json"})
|
|
2305
|
+
if response.ok:
|
|
2306
|
+
logger.info(
|
|
2307
|
+
f"Found {len(response.json())} existing stakeholders",
|
|
2308
|
+
record_type="stackholders",
|
|
2309
|
+
model_layer="stackholders",
|
|
2310
|
+
)
|
|
2311
|
+
return response.json()
|
|
2312
|
+
return []
|
|
2313
|
+
|
|
2314
|
+
|
|
2315
|
+
def filter_valid_pocs(pocs: list) -> list:
|
|
2316
|
+
"""Filter out invalid (non-dict) points of contact
|
|
2317
|
+
|
|
2318
|
+
:param list pocs: A list of points of contact
|
|
2319
|
+
:return: A list of valid points of contact
|
|
2320
|
+
:rtype: list
|
|
2321
|
+
"""
|
|
2322
|
+
return [poc for poc in pocs if isinstance(poc, dict)]
|
|
2323
|
+
|
|
2324
|
+
|
|
2325
|
+
def insert_new_stakeholders(
|
|
2326
|
+
api: Api, config: dict, pocs: list, existing_stakeholders: list, regscale_ssp: dict
|
|
2327
|
+
) -> None:
|
|
2328
|
+
"""Insert new stakeholders into the system
|
|
2329
|
+
|
|
2330
|
+
:param Api api: An API instance
|
|
2331
|
+
:param dict config: A configuration dictionary
|
|
2332
|
+
:param list pocs: A list of points of contact
|
|
2333
|
+
:param list existing_stakeholders: A list of existing stakeholders
|
|
2334
|
+
:param dict regscale_ssp: A dict of RegScale SSP data.
|
|
2335
|
+
:rtype: None
|
|
2336
|
+
"""
|
|
2337
|
+
pocs_inserted = []
|
|
2338
|
+
for poc in pocs:
|
|
2339
|
+
stakeholder = create_stakeholder_dict(poc, regscale_ssp)
|
|
2340
|
+
if should_insert_stakeholder(poc, pocs_inserted, existing_stakeholders):
|
|
2341
|
+
post_stakeholder(api, config, stakeholder)
|
|
2342
|
+
pocs_inserted.append(stakeholder.get("name").strip())
|
|
2343
|
+
|
|
2344
|
+
|
|
2345
|
+
def extract_management_poc(tables: list, pocs: list) -> Dict:
|
|
2346
|
+
"""Extract Management POC
|
|
2347
|
+
|
|
2348
|
+
:param list tables: A list of tables from the XML document.
|
|
2349
|
+
:param list pocs: A dict of points of contact
|
|
2350
|
+
:return: A dict of the Management POC
|
|
2351
|
+
:rtype: Dict
|
|
2352
|
+
"""
|
|
2353
|
+
return _extracted_from_gather_stakeholders(
|
|
2354
|
+
tables,
|
|
2355
|
+
"Owner Information",
|
|
2356
|
+
pocs,
|
|
2357
|
+
"Information System Management Point of Contact",
|
|
2358
|
+
)
|
|
2359
|
+
|
|
2360
|
+
|
|
2361
|
+
def extract_information_poc(tables: list, document: Document) -> List:
|
|
2362
|
+
"""Extract Information POC
|
|
2363
|
+
|
|
2364
|
+
:param list tables: A list of tables from the XML document.
|
|
2365
|
+
:param SSP ssp: SSP Object of docx SSP data.
|
|
2366
|
+
:return: A List of the Information POC
|
|
2367
|
+
:rtype: List
|
|
2368
|
+
"""
|
|
2369
|
+
return [
|
|
2370
|
+
get_contact_info(
|
|
2371
|
+
tables,
|
|
2372
|
+
key="Information System Technical Point of Contact",
|
|
2373
|
+
xpath=TABLE_TAG,
|
|
2374
|
+
),
|
|
2375
|
+
get_base_contact(document),
|
|
2376
|
+
]
|
|
2377
|
+
|
|
2378
|
+
|
|
2379
|
+
def extract_csp_poc(tables: List, pocs: List) -> Dict:
|
|
2380
|
+
"""Extract CSP POC
|
|
2381
|
+
|
|
2382
|
+
:param List tables: A list of tables from the XML document.
|
|
2383
|
+
:param List pocs: A list of points of contact
|
|
2384
|
+
:return: A dict of the CSP POC
|
|
2385
|
+
:rtype: Dict
|
|
2386
|
+
"""
|
|
2387
|
+
return _extracted_from_gather_stakeholders(
|
|
2388
|
+
tables,
|
|
2389
|
+
"AO Point of Contact",
|
|
2390
|
+
pocs,
|
|
2391
|
+
"CSP Name Internal ISSO (or Equivalent) Point of Contact",
|
|
2392
|
+
)
|
|
2393
|
+
|
|
2394
|
+
|
|
2395
|
+
def create_stakeholder_dict(poc: dict, regscale_ssp: dict) -> dict:
|
|
2396
|
+
"""Create a stakeholder dictionary
|
|
2397
|
+
|
|
2398
|
+
:param dict poc: A point of contact
|
|
2399
|
+
:param dict regscale_ssp: A dict of RegScale SSP data.
|
|
2400
|
+
:return: A stakeholder dictionary
|
|
2401
|
+
:rtype: dict
|
|
2402
|
+
"""
|
|
2403
|
+
poc = {k.lower(): v for k, v in poc.items()} # Make this case-insensitive.
|
|
2404
|
+
email = name = title = phone = ""
|
|
2405
|
+
if "email" in (keys := [key.lower() for key in poc]):
|
|
2406
|
+
email = poc[(_ := _check_if_string_in_list_of_string("email", keys)).lower()]
|
|
2407
|
+
if "name" in (keys := [key.lower() for key in poc]):
|
|
2408
|
+
name = poc[(_ := _check_if_string_in_list_of_string("name", keys)).lower()]
|
|
2409
|
+
if "title" in (keys := [key.lower() for key in poc]):
|
|
2410
|
+
name = poc[(_ := _check_if_string_in_list_of_string("title", keys)).lower()]
|
|
2411
|
+
if "phone" in (keys := [key.lower() for key in poc]):
|
|
2412
|
+
name = poc[(_ := _check_if_string_in_list_of_string("phone", keys)).lower()]
|
|
2413
|
+
|
|
2414
|
+
return {
|
|
2415
|
+
"name": name,
|
|
2416
|
+
"title": title,
|
|
2417
|
+
"phone": phone,
|
|
2418
|
+
"email": email,
|
|
2419
|
+
"address": poc.get("address", "") if "address" in poc else "",
|
|
2420
|
+
"parentId": regscale_ssp["id"],
|
|
2421
|
+
"parentModule": "securityplans",
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
|
|
2425
|
+
def should_insert_stakeholder(poc: Dict, pocs_inserted: List, existing_stakeholders: List) -> bool:
|
|
2426
|
+
"""Check if the stakeholder should be inserted
|
|
2427
|
+
|
|
2428
|
+
:param Dict poc: A point of contact
|
|
2429
|
+
:param List pocs_inserted: A list of pocs already inserted
|
|
2430
|
+
:param List existing_stakeholders: A list of existing stakeholders
|
|
2431
|
+
:return: True if the stakeholder should be inserted
|
|
2432
|
+
:rtype: bool
|
|
2433
|
+
"""
|
|
2434
|
+
return "name" in poc.keys() and (
|
|
2435
|
+
poc["name"].strip() not in pocs_inserted
|
|
2436
|
+
and poc["name"].strip() not in {guy["name"] for guy in existing_stakeholders if "name" in guy.keys()}
|
|
2437
|
+
)
|
|
2438
|
+
|
|
2439
|
+
|
|
2440
|
+
def _check_if_string_in_list_of_string(string: str, list_of_strings: list) -> str:
|
|
2441
|
+
"""
|
|
2442
|
+
Check if a string is in a list of strings
|
|
2443
|
+
|
|
2444
|
+
:param str string: The string to check
|
|
2445
|
+
:param list list_of_strings: The list of strings to check
|
|
2446
|
+
:return: the found string
|
|
2447
|
+
:rtype: str
|
|
2448
|
+
"""
|
|
2449
|
+
dat = any(string in s for s in list_of_strings)
|
|
2450
|
+
return {s for s in list_of_strings if string in s}.pop() if dat else ""
|
|
2451
|
+
|
|
2452
|
+
|
|
2453
|
+
def _extracted_from_gather_stakeholders(tables: Any, key: str, pocs: list, key2: str) -> dict:
|
|
2454
|
+
"""
|
|
2455
|
+
Extract system owner and add to pocs
|
|
2456
|
+
|
|
2457
|
+
:param Any tables: Tables from the XML document
|
|
2458
|
+
:param str key: Key used to find system owner
|
|
2459
|
+
:param list pocs: List of points of contacts from a SSP
|
|
2460
|
+
:param str key2: Second key to find system owner details
|
|
2461
|
+
:return: Dictionary of system owner details
|
|
2462
|
+
:rtype: dict
|
|
2463
|
+
"""
|
|
2464
|
+
system_owner = get_contact_info(tables, key=key, xpath=TABLE_TAG)
|
|
2465
|
+
pocs.append(system_owner)
|
|
2466
|
+
return get_contact_info(tables, key=key2, xpath=TABLE_TAG)
|
|
2467
|
+
|
|
2468
|
+
|
|
2469
|
+
def post_leveraged_authorizations(table_data: list, ssp_id: int) -> None:
|
|
2470
|
+
"""
|
|
2471
|
+
Function to post leveraged authorizations
|
|
2472
|
+
|
|
2473
|
+
:param list table_data: Data used to post to RegScale
|
|
2474
|
+
:param int ssp_id: SSP ID # in RegScale
|
|
2475
|
+
:rtype: None
|
|
2476
|
+
"""
|
|
2477
|
+
date_key = "Date Granted"
|
|
2478
|
+
app = Application()
|
|
2479
|
+
key = "Leveraged Information System Name"
|
|
2480
|
+
data = [table for table in table_data if key in table.keys()]
|
|
2481
|
+
for la in data:
|
|
2482
|
+
if not la.get(key, None):
|
|
2483
|
+
continue
|
|
2484
|
+
if la.get(date_key) is None or not validate_date_str(la.get(date_key)):
|
|
2485
|
+
logger.warning(
|
|
2486
|
+
f"Using today's date because of bad Date Granted for {la.get(key)}: {la.get(date_key)}",
|
|
2487
|
+
record_type="leveraged-authorizations",
|
|
2488
|
+
model_layer="leveraged-authorizations",
|
|
2489
|
+
)
|
|
2490
|
+
la[date_key] = get_current_datetime()
|
|
2491
|
+
try:
|
|
2492
|
+
LeveragedAuthorization(
|
|
2493
|
+
title=la.get(key, " "),
|
|
2494
|
+
servicesUsed=la.get("Leveraged Service Provider Owner"),
|
|
2495
|
+
fedrampId=la.get("FedRAMP ID", "Fxxxxxxxxxx"),
|
|
2496
|
+
authorizationType=la.get("Authorization Type", "Joint Authorization Board (JAB)"),
|
|
2497
|
+
dateAuthorized=la.get("Date Granted"),
|
|
2498
|
+
natureOfAgreement=la.get("Nature of Agreement", "Other"),
|
|
2499
|
+
dataTypes=la.get("Data Types", TBD),
|
|
2500
|
+
authenticationType=la.get("Authentication Type", TBD),
|
|
2501
|
+
authorizedUserTypes=la.get("Authorizad User Types", TBD),
|
|
2502
|
+
impactLevel=la.get(IMPACT_LEVEL, "High"),
|
|
2503
|
+
createdById=app.config.get("userId"),
|
|
2504
|
+
securityPlanId=ssp_id,
|
|
2505
|
+
ownerId=app.config.get("userId"),
|
|
2506
|
+
lastUpdatedById=app.config.get("userId"),
|
|
2507
|
+
).create()
|
|
2508
|
+
logger.info(
|
|
2509
|
+
f"Leveraged Authorizations for {la.get(key)} created in RegScale.",
|
|
2510
|
+
record_type="leveraged-authorizations",
|
|
2511
|
+
model_layer="leveraged-authorizations",
|
|
2512
|
+
)
|
|
2513
|
+
except Exception as e:
|
|
2514
|
+
logger.error(
|
|
2515
|
+
f"Error creating leveraged authorizations: {e}",
|
|
2516
|
+
record_type="leveraged-authorizations",
|
|
2517
|
+
model_layer="leveraged-authorizations",
|
|
2518
|
+
)
|
|
2519
|
+
|
|
2520
|
+
|
|
2521
|
+
def find_profile_by_name(profile_name: str) -> Dict:
|
|
2522
|
+
"""Find profile by name
|
|
2523
|
+
|
|
2524
|
+
:param str profile_name: Name of the profile
|
|
2525
|
+
:raises ValueError: If the profile is not found
|
|
2526
|
+
:return: Dictionary of the profile
|
|
2527
|
+
:rtype: Dict
|
|
2528
|
+
"""
|
|
2529
|
+
app = Application()
|
|
2530
|
+
api = Api()
|
|
2531
|
+
logger.info(
|
|
2532
|
+
f"Using the {profile_name} profile to import controls.",
|
|
2533
|
+
record_type="profile",
|
|
2534
|
+
model_layer="profile",
|
|
2535
|
+
)
|
|
2536
|
+
profile_response = api.get(url=urljoin(app.config["domain"], "/api/profiles/getList"))
|
|
2537
|
+
|
|
2538
|
+
if profile_response.ok:
|
|
2539
|
+
profiles = profile_response.json()
|
|
2540
|
+
logger.info(
|
|
2541
|
+
f"Found {len(profiles)} profiles in RegScale.",
|
|
2542
|
+
record_type="profile",
|
|
2543
|
+
model_layer="profile",
|
|
2544
|
+
)
|
|
2545
|
+
else:
|
|
2546
|
+
profiles = []
|
|
2547
|
+
logger.error(
|
|
2548
|
+
"Unable to get profiles from RegScale.",
|
|
2549
|
+
record_type="profile",
|
|
2550
|
+
model_layer="profile",
|
|
2551
|
+
)
|
|
2552
|
+
profile_response.raise_for_status()
|
|
2553
|
+
profile = None
|
|
2554
|
+
try:
|
|
2555
|
+
for profile_obj in profiles:
|
|
2556
|
+
if profile_obj["name"] == profile_name:
|
|
2557
|
+
profile = profile_obj
|
|
2558
|
+
if profile is None:
|
|
2559
|
+
raise ValueError(f"Unable to find profile: {profile_name}")
|
|
2560
|
+
except Exception as ex:
|
|
2561
|
+
logger.error(
|
|
2562
|
+
f"Unable to continue, {profile_name} is not found!\n{ex}",
|
|
2563
|
+
record_type="profile",
|
|
2564
|
+
model_layer="profile",
|
|
2565
|
+
)
|
|
2566
|
+
return profile
|
|
2567
|
+
|
|
2568
|
+
|
|
2569
|
+
def get_profile_info_by_id(profile_id: int) -> Dict:
|
|
2570
|
+
"""
|
|
2571
|
+
Get a profile by the profile_id from the Regscale Api
|
|
2572
|
+
|
|
2573
|
+
:param int profile_id: The profile_id to get
|
|
2574
|
+
:return: Profile
|
|
2575
|
+
:rtype: Dict
|
|
2576
|
+
"""
|
|
2577
|
+
profile = None
|
|
2578
|
+
try:
|
|
2579
|
+
app = Application()
|
|
2580
|
+
api = Api()
|
|
2581
|
+
profile_response = api.get(urljoin(app.config["domain"], f"/api/profiles/{profile_id}"))
|
|
2582
|
+
if profile_response.ok:
|
|
2583
|
+
profile = profile_response.json()
|
|
2584
|
+
else:
|
|
2585
|
+
logger.error(
|
|
2586
|
+
"Unable to get profile from RegScale.",
|
|
2587
|
+
record_type="profile",
|
|
2588
|
+
model_layer="profile",
|
|
2589
|
+
)
|
|
2590
|
+
except (IndexError, AttributeError) as ex:
|
|
2591
|
+
logger.error(
|
|
2592
|
+
f"Error Profile, {profile_id} is not found!\n{ex}",
|
|
2593
|
+
record_type="profile",
|
|
2594
|
+
model_layer="profile",
|
|
2595
|
+
)
|
|
2596
|
+
return profile
|
|
2597
|
+
|
|
2598
|
+
|
|
2599
|
+
def process_fedramp_docx_by_profile_id(
|
|
2600
|
+
file_path: Union[click.Path, str],
|
|
2601
|
+
profile_id: int,
|
|
2602
|
+
save_data: bool = False,
|
|
2603
|
+
load_missing: bool = False,
|
|
2604
|
+
) -> Any:
|
|
2605
|
+
"""
|
|
2606
|
+
Process a FedRAMP docx by the profile_id from the Regscale Api
|
|
2607
|
+
|
|
2608
|
+
:param Union[click.Path, str] file_path: The file path to the FedRAMP docx
|
|
2609
|
+
:param int profile_id: The profile_id to process
|
|
2610
|
+
:param bool save_data: Whether to save the data
|
|
2611
|
+
:param bool load_missing: Whether to load missing controls
|
|
2612
|
+
:return: RegScale SSP
|
|
2613
|
+
:rtype: Any
|
|
2614
|
+
"""
|
|
2615
|
+
profile = get_profile_info_by_id(profile_id)
|
|
2616
|
+
new_implementations, regscale_ssp = process_fedramp_docx(
|
|
2617
|
+
fedramp_file_path=file_path,
|
|
2618
|
+
base_fedramp_profile=profile["name"],
|
|
2619
|
+
base_fedramp_profile_id=profile["id"],
|
|
2620
|
+
save_data=save_data,
|
|
2621
|
+
add_missing=load_missing,
|
|
2622
|
+
profile=profile,
|
|
2623
|
+
)
|
|
2624
|
+
# implementation_results
|
|
2625
|
+
logger.write_events()
|
|
2626
|
+
return (
|
|
2627
|
+
"artifacts/import-results.csv",
|
|
2628
|
+
{
|
|
2629
|
+
"ssp_title": regscale_ssp.get("systemName", "New SSP Default Name"),
|
|
2630
|
+
"ssp_id": regscale_ssp.get("id"),
|
|
2631
|
+
},
|
|
2632
|
+
new_implementations,
|
|
2633
|
+
)
|
|
2634
|
+
|
|
2635
|
+
|
|
2636
|
+
def get_profile_mapping(profile_id: int) -> Optional[list]:
|
|
2637
|
+
"""
|
|
2638
|
+
Get a profile mapping by the profile_id from the Regscale Api
|
|
2639
|
+
|
|
2640
|
+
:param int profile_id: The profile_id to get
|
|
2641
|
+
:return: Profile Mapping, if found
|
|
2642
|
+
:rtype: Optional[list]
|
|
2643
|
+
|
|
2644
|
+
"""
|
|
2645
|
+
app = Application()
|
|
2646
|
+
api = Api()
|
|
2647
|
+
profile_mapping = None
|
|
2648
|
+
try:
|
|
2649
|
+
profile_mapping_resp = api.get(
|
|
2650
|
+
urljoin(
|
|
2651
|
+
app.config["domain"],
|
|
2652
|
+
f"/api/profileMapping/getByProfile/{profile_id}",
|
|
2653
|
+
)
|
|
2654
|
+
)
|
|
2655
|
+
if profile_mapping_resp.ok:
|
|
2656
|
+
profile_mapping = profile_mapping_resp.json()
|
|
2657
|
+
else:
|
|
2658
|
+
logger.error(
|
|
2659
|
+
"Unable to get profile mapping from RegScale.",
|
|
2660
|
+
record_type="profile-mapping",
|
|
2661
|
+
model_layer="profile-mapping",
|
|
2662
|
+
)
|
|
2663
|
+
except Exception as e:
|
|
2664
|
+
logger.error(
|
|
2665
|
+
f"Failed to get profile-mappings by profile id with error: {str(e)}",
|
|
2666
|
+
record_type="profile-mapping",
|
|
2667
|
+
model_layer="profile-mapping",
|
|
2668
|
+
)
|
|
2669
|
+
return profile_mapping
|
|
2670
|
+
|
|
2671
|
+
|
|
2672
|
+
def parse_ssp_docx_tables(tables: Any) -> Tuple[str, str, str, str, str, str, list]:
|
|
2673
|
+
"""
|
|
2674
|
+
Parse the SSP docx tables
|
|
2675
|
+
|
|
2676
|
+
:param tables: List of tables from the SSP docx
|
|
2677
|
+
:return Tuple[str, str, str, str, str, list]: System Status, System Type, Title, Cloud Model, Cloud Sevice, Version, Table Data
|
|
2678
|
+
:rtype: Tuple[str, str, str, list]
|
|
2679
|
+
"""
|
|
2680
|
+
count = 0
|
|
2681
|
+
title = None
|
|
2682
|
+
version = None
|
|
2683
|
+
system_status = "Other"
|
|
2684
|
+
system_type = SYSTEM_TYPE
|
|
2685
|
+
cloud_model = None
|
|
2686
|
+
cloud_service = None
|
|
2687
|
+
table_data = []
|
|
2688
|
+
for table in tables:
|
|
2689
|
+
for i, row in enumerate(table.rows):
|
|
2690
|
+
checked = False
|
|
2691
|
+
rem = row._element
|
|
2692
|
+
check_boxes = rem.xpath(".//w14:checked")
|
|
2693
|
+
text = (cell.text.strip() for cell in row.cells)
|
|
2694
|
+
if check_boxes:
|
|
2695
|
+
for checks in check_boxes:
|
|
2696
|
+
if checks.items()[0][1] == "1":
|
|
2697
|
+
count = count + 1
|
|
2698
|
+
checked = True
|
|
2699
|
+
# Establish the mapping based on the first row
|
|
2700
|
+
# headers; these will become the keys of our dictionary
|
|
2701
|
+
if i == 0:
|
|
2702
|
+
keys = tuple(text)
|
|
2703
|
+
if not title:
|
|
2704
|
+
dat = [x.strip() for x in keys if x].pop().split("\n")
|
|
2705
|
+
title = dat[1] if len(dat) > 1 else dat[0]
|
|
2706
|
+
if len(dat) > 1:
|
|
2707
|
+
version = dat[2] if len(dat) > 1 else dat[1]
|
|
2708
|
+
if version:
|
|
2709
|
+
version = version.replace("Version", "")
|
|
2710
|
+
else:
|
|
2711
|
+
version = "1.0"
|
|
2712
|
+
continue
|
|
2713
|
+
row_data = dict(zip(keys, text))
|
|
2714
|
+
if checked:
|
|
2715
|
+
if SYSTEM_STATUS in row_data:
|
|
2716
|
+
system_status = row_data[SYSTEM_STATUS]
|
|
2717
|
+
if SERVICE_ARCHS in row_data:
|
|
2718
|
+
cloud_service = system_type = row_data[SERVICE_ARCHS]
|
|
2719
|
+
if DEPLOY_MODEL in row_data:
|
|
2720
|
+
cloud_model = row_data[DEPLOY_MODEL]
|
|
2721
|
+
row_data["checked"] = checked
|
|
2722
|
+
row_data["element"] = rem
|
|
2723
|
+
table_data.append(row_data)
|
|
2724
|
+
return (
|
|
2725
|
+
system_status,
|
|
2726
|
+
system_type,
|
|
2727
|
+
title,
|
|
2728
|
+
cloud_model,
|
|
2729
|
+
cloud_service,
|
|
2730
|
+
version,
|
|
2731
|
+
table_data,
|
|
2732
|
+
)
|
|
2733
|
+
|
|
2734
|
+
|
|
2735
|
+
def process_fedramp_docx( # noqa: C901
|
|
2736
|
+
fedramp_file_path: click.Path,
|
|
2737
|
+
base_fedramp_profile: str,
|
|
2738
|
+
base_fedramp_profile_id: int,
|
|
2739
|
+
save_data: bool = False,
|
|
2740
|
+
add_missing: bool = False,
|
|
2741
|
+
profile: Dict = None,
|
|
2742
|
+
) -> Tuple[List, Document]:
|
|
2743
|
+
"""
|
|
2744
|
+
Convert a FedRAMP file to a RegScale SSP
|
|
2745
|
+
|
|
2746
|
+
:param click.Path fedramp_file_path: The click file path object
|
|
2747
|
+
:param str base_fedramp_profile: base fedramp profile
|
|
2748
|
+
:param int base_fedramp_profile_id: base fedramp profile id
|
|
2749
|
+
:param bool save_data: Whether to save the data
|
|
2750
|
+
:param bool add_missing: Whether to add missing controls
|
|
2751
|
+
:param Dict profile: The profile to use
|
|
2752
|
+
:return: Tuple of new implementations count and RegScale SSP
|
|
2753
|
+
:rtype: Tuple[List, SSP]
|
|
2754
|
+
"""
|
|
2755
|
+
# If list of controls is more than profile mapping, make sure i get them from somewhere? Get base catalog from profile.
|
|
2756
|
+
load_missing = add_missing
|
|
2757
|
+
app = Application()
|
|
2758
|
+
api = Api()
|
|
2759
|
+
ssp = SSP(fedramp_file_path)
|
|
2760
|
+
document = Document(fedramp_file_path)
|
|
2761
|
+
for p in document.paragraphs:
|
|
2762
|
+
replace_content_control(p._element)
|
|
2763
|
+
|
|
2764
|
+
full_text = [para.text for para in document.paragraphs]
|
|
2765
|
+
system_text_lookup = "System Function or Purpose"
|
|
2766
|
+
description_lookup_str = "General System Description"
|
|
2767
|
+
|
|
2768
|
+
description = get_text_between_headers(
|
|
2769
|
+
full_text,
|
|
2770
|
+
start_header=description_lookup_str,
|
|
2771
|
+
end_header=system_text_lookup,
|
|
2772
|
+
)
|
|
2773
|
+
|
|
2774
|
+
environment = get_text_between_headers(
|
|
2775
|
+
full_text,
|
|
2776
|
+
start_header="SYSTEM ENVIRONMENT AND INVENTORY",
|
|
2777
|
+
end_header="Data Flow",
|
|
2778
|
+
)
|
|
2779
|
+
|
|
2780
|
+
purpose = get_text_between_headers(
|
|
2781
|
+
full_text,
|
|
2782
|
+
start_header=system_text_lookup,
|
|
2783
|
+
end_header="System Description:",
|
|
2784
|
+
)
|
|
2785
|
+
if not purpose or purpose == "":
|
|
2786
|
+
purpose = get_text_between_headers(
|
|
2787
|
+
full_text,
|
|
2788
|
+
start_header=system_text_lookup,
|
|
2789
|
+
end_header="Information System Components and Boundaries",
|
|
2790
|
+
)
|
|
2791
|
+
|
|
2792
|
+
confidentiality = "Low"
|
|
2793
|
+
integrity = "Low"
|
|
2794
|
+
availability = "Low"
|
|
2795
|
+
tables = get_tables(document)
|
|
2796
|
+
security_objective = get_xpath_data_detailed(
|
|
2797
|
+
tables,
|
|
2798
|
+
key="Security Objective",
|
|
2799
|
+
ident="Confidentiality",
|
|
2800
|
+
xpath=TABLE_TAG,
|
|
2801
|
+
count_array=[2, 4, 6],
|
|
2802
|
+
)
|
|
2803
|
+
|
|
2804
|
+
availability = (
|
|
2805
|
+
security_objective["availability"].split(" ")[0] if "availability" in security_objective else availability
|
|
2806
|
+
)
|
|
2807
|
+
confidentiality = (
|
|
2808
|
+
security_objective["confidentiality"].split(" ")[0]
|
|
2809
|
+
if "confidentiality" in security_objective
|
|
2810
|
+
else confidentiality
|
|
2811
|
+
)
|
|
2812
|
+
integrity = security_objective["integrity"].split(" ")[0] if "integrity" in security_objective else integrity
|
|
2813
|
+
|
|
2814
|
+
(
|
|
2815
|
+
system_status,
|
|
2816
|
+
system_type,
|
|
2817
|
+
title,
|
|
2818
|
+
cloud_model,
|
|
2819
|
+
cloud_service,
|
|
2820
|
+
version,
|
|
2821
|
+
table_data,
|
|
2822
|
+
) = parse_ssp_docx_tables(document.tables)
|
|
2823
|
+
|
|
2824
|
+
mdeploypublic = True if "multiple organizations " in cloud_model else False
|
|
2825
|
+
mdeploypriv = True if "specific organization/agency" in cloud_model else False
|
|
2826
|
+
mdeploygov = True if "organizations/agencies" in cloud_model else False
|
|
2827
|
+
mdeployhybrid = True if "shared across all clients/agencies" in cloud_model else False
|
|
2828
|
+
|
|
2829
|
+
msaas = True if SYSTEM_TYPE in cloud_service else False
|
|
2830
|
+
mpaas = True if SYSTEM_TYPE in cloud_service and not msaas else False
|
|
2831
|
+
miaas = True if "General Support System" in cloud_service else False
|
|
2832
|
+
mother = True if "Explain:" in cloud_service else False
|
|
2833
|
+
|
|
2834
|
+
privacydata = get_xpath_privacy_detailed(
|
|
2835
|
+
tables,
|
|
2836
|
+
key="Does the ISA collect, maintain, or share PII in any identifiable form?",
|
|
2837
|
+
xpath=TABLE_TAG,
|
|
2838
|
+
count_array=[0, 2, 4, 6],
|
|
2839
|
+
)
|
|
2840
|
+
|
|
2841
|
+
sysinfo = get_xpath_sysinfo_detailed(
|
|
2842
|
+
tables,
|
|
2843
|
+
key="Unique Identifier",
|
|
2844
|
+
xpath=TABLE_TAG,
|
|
2845
|
+
count_array=[3, 5],
|
|
2846
|
+
)
|
|
2847
|
+
if sysinfo["systemname"]:
|
|
2848
|
+
title = sysinfo["systemname"].strip() if "systemname" in sysinfo else title
|
|
2849
|
+
if sysinfo["uniqueidentifier"]:
|
|
2850
|
+
uniqueidentifier = sysinfo["uniqueidentifier"].strip() if "uniqueidentifier" in sysinfo else None
|
|
2851
|
+
else:
|
|
2852
|
+
uniqueidentifier = None
|
|
2853
|
+
|
|
2854
|
+
prepdata = get_xpath_prepdata_detailed(
|
|
2855
|
+
tables,
|
|
2856
|
+
key="Identification of Organization that Prepared this Document",
|
|
2857
|
+
ident=ORGANIZATION_TAG,
|
|
2858
|
+
xpath=TABLE_TAG,
|
|
2859
|
+
)
|
|
2860
|
+
preporgname = prepdata["orgname"] if "orgname" in prepdata else None
|
|
2861
|
+
prepaddress = prepdata["street"] if "street" in prepdata else None
|
|
2862
|
+
prepoffice = prepdata["office"] if "office" in prepdata else None
|
|
2863
|
+
prepcitystate = prepdata["citystate"] if "citystate" in prepdata else None
|
|
2864
|
+
cspdata = get_xpath_prepdata_detailed(
|
|
2865
|
+
tables,
|
|
2866
|
+
key="Identification of Cloud Service Provider",
|
|
2867
|
+
ident=ORGANIZATION_TAG,
|
|
2868
|
+
xpath=TABLE_TAG,
|
|
2869
|
+
)
|
|
2870
|
+
csporgname = cspdata["orgname"] if "orgname" in cspdata else None
|
|
2871
|
+
cspaddress = cspdata["street"] if "street" in cspdata else None
|
|
2872
|
+
cspoffice = cspdata["office"] if "office" in cspdata else None
|
|
2873
|
+
cspcitystate = cspdata["citystate"] if "citystate" in cspdata else None
|
|
2874
|
+
status = "Operational" if "in production" in system_status else "Other"
|
|
2875
|
+
# Links are posted to links mapped to ssp
|
|
2876
|
+
# post_links(app, table_data, ssp_id)
|
|
2877
|
+
# Parts will go in implementation fields.
|
|
2878
|
+
if base_fedramp_profile_id:
|
|
2879
|
+
profile = get_profile_info_by_id(profile_id=base_fedramp_profile_id)
|
|
2880
|
+
if not profile:
|
|
2881
|
+
profile = find_profile_by_name(base_fedramp_profile) or {}
|
|
2882
|
+
profile_mapping = ProfileMapping.get_by_profile(profile_id=profile.get("id"))
|
|
2883
|
+
if len(profile_mapping) == 0:
|
|
2884
|
+
error_and_exit(f"Unable to continue, please load {base_fedramp_profile} with controls!")
|
|
2885
|
+
|
|
2886
|
+
logger.info(
|
|
2887
|
+
f"Utilizing profile: {profile.get('name')}",
|
|
2888
|
+
record_type="profile",
|
|
2889
|
+
model_layer="profile",
|
|
2890
|
+
)
|
|
2891
|
+
args = {
|
|
2892
|
+
"profile": profile,
|
|
2893
|
+
"title": title,
|
|
2894
|
+
"otheridentifier": uniqueidentifier,
|
|
2895
|
+
# get_profile_mapping(profile["id"]))
|
|
2896
|
+
"version": version,
|
|
2897
|
+
"confidentiality": confidentiality,
|
|
2898
|
+
"integrity": integrity,
|
|
2899
|
+
"availability": availability,
|
|
2900
|
+
"status": status,
|
|
2901
|
+
"system_type": system_type,
|
|
2902
|
+
# "ssp": ssp,
|
|
2903
|
+
"revision": revision(document),
|
|
2904
|
+
"description": description,
|
|
2905
|
+
"environment": environment,
|
|
2906
|
+
"purpose": purpose,
|
|
2907
|
+
"modeiaas": miaas,
|
|
2908
|
+
"modepaas": mpaas,
|
|
2909
|
+
"modeother": mother,
|
|
2910
|
+
"modesaas": msaas,
|
|
2911
|
+
"deploypubic": mdeploypublic,
|
|
2912
|
+
"deployprivate": mdeploypriv,
|
|
2913
|
+
"deploygov": mdeploygov,
|
|
2914
|
+
"deployhybrid": mdeployhybrid,
|
|
2915
|
+
"preporgname": preporgname,
|
|
2916
|
+
"prepaddress": prepaddress,
|
|
2917
|
+
"prepoffice": prepoffice,
|
|
2918
|
+
"prepcitystate": prepcitystate,
|
|
2919
|
+
"csporgname": csporgname,
|
|
2920
|
+
"cspaddress": cspaddress,
|
|
2921
|
+
"cspoffice": cspoffice,
|
|
2922
|
+
"cspcitystate": cspcitystate,
|
|
2923
|
+
}
|
|
2924
|
+
regscale_ssp = create_initial_ssp(args)
|
|
2925
|
+
|
|
2926
|
+
try:
|
|
2927
|
+
logger.info("Parsing and creating Privacy Data", record_type="privacy", model_layer="privacy")
|
|
2928
|
+
create_privacy_data(app=app, privacy_data=privacydata, ssp_id=regscale_ssp.get("id"))
|
|
2929
|
+
logger.info(
|
|
2930
|
+
"Successfully Created Privacy data.",
|
|
2931
|
+
record_type="privacy",
|
|
2932
|
+
model_layer="privacy",
|
|
2933
|
+
)
|
|
2934
|
+
except Exception as e:
|
|
2935
|
+
logger.error(
|
|
2936
|
+
f"Unable to create privacy record: {e}",
|
|
2937
|
+
record_type="privacy",
|
|
2938
|
+
model_layer="privacy",
|
|
2939
|
+
)
|
|
2940
|
+
|
|
2941
|
+
try:
|
|
2942
|
+
logger.info(
|
|
2943
|
+
"Parsing and creating System Information", record_type="responsible-roles", model_layer="responsible-roles"
|
|
2944
|
+
)
|
|
2945
|
+
create_responsible_roles(app, table_data, ssp_id=regscale_ssp["id"])
|
|
2946
|
+
ctrl_roles = post_responsible_roles(app, table_data, ssp_id=regscale_ssp["id"])
|
|
2947
|
+
except Exception as e:
|
|
2948
|
+
logger.error(
|
|
2949
|
+
f"Unable to gather responsible roles: {e}",
|
|
2950
|
+
record_type="responsible-roles",
|
|
2951
|
+
model_layer="responsible-roles",
|
|
2952
|
+
)
|
|
2953
|
+
|
|
2954
|
+
try:
|
|
2955
|
+
logger.info("Parsing and creating Stakeholders", record_type="stakeholder", model_layer="stakeholder")
|
|
2956
|
+
gather_stakeholders(tables, regscale_ssp, document)
|
|
2957
|
+
except Exception as e:
|
|
2958
|
+
logger.error(
|
|
2959
|
+
f"Unable to gather stakeholders: {e}",
|
|
2960
|
+
record_type="stakeholder",
|
|
2961
|
+
model_layer="stakeholder",
|
|
2962
|
+
)
|
|
2963
|
+
try:
|
|
2964
|
+
logger.info("Parsing and creating Interconnects", record_type="interconnect", model_layer="interconnects")
|
|
2965
|
+
post_interconnects(app, table_data, regscale_ssp)
|
|
2966
|
+
except Exception as e:
|
|
2967
|
+
logger.error(
|
|
2968
|
+
f"Unable to gather interconnects: {e}",
|
|
2969
|
+
record_type="interconnect",
|
|
2970
|
+
model_layer="interconnects",
|
|
2971
|
+
)
|
|
2972
|
+
try:
|
|
2973
|
+
logger.info(
|
|
2974
|
+
"Parsing and creating Ports and Protocols", record_type="ports-protocols", model_layer="ports-protocols"
|
|
2975
|
+
)
|
|
2976
|
+
tables_dict = tables_to_dict(document)
|
|
2977
|
+
ports_table_data = [row for t in tables_dict for row in t if "Ports (TCP/UDP)*" in row]
|
|
2978
|
+
post_ports(app, ports_table_data, ssp_id=regscale_ssp["id"])
|
|
2979
|
+
except Exception as e:
|
|
2980
|
+
logger.error(
|
|
2981
|
+
f"Unable to gather ports: {e}",
|
|
2982
|
+
record_type="ports-protocols",
|
|
2983
|
+
model_layer="ports-protocols",
|
|
2984
|
+
)
|
|
2985
|
+
try:
|
|
2986
|
+
logger.info("Parsing and creating Links", record_type="links", model_layer="links")
|
|
2987
|
+
post_links(
|
|
2988
|
+
config=app.config, api=api, document=document, file_path=fedramp_file_path, regscale_ssp=regscale_ssp
|
|
2989
|
+
)
|
|
2990
|
+
except Exception as e:
|
|
2991
|
+
logger.error(
|
|
2992
|
+
f"Unable to gather links: {e}",
|
|
2993
|
+
record_type="links",
|
|
2994
|
+
model_layer="links",
|
|
2995
|
+
)
|
|
2996
|
+
try:
|
|
2997
|
+
logger.info(
|
|
2998
|
+
"Parsing and creating implementations", record_type="implementations", model_layer="implementations"
|
|
2999
|
+
)
|
|
3000
|
+
new_implementations = post_implementations(
|
|
3001
|
+
app=app,
|
|
3002
|
+
ssp_obj=ssp,
|
|
3003
|
+
regscale_ssp=regscale_ssp,
|
|
3004
|
+
mapping=profile_mapping,
|
|
3005
|
+
ctrl_roles=ctrl_roles,
|
|
3006
|
+
save_data=save_data,
|
|
3007
|
+
load_missing=load_missing,
|
|
3008
|
+
)
|
|
3009
|
+
except Exception as e:
|
|
3010
|
+
logger.debug(e, exc_info=True)
|
|
3011
|
+
logger.error(
|
|
3012
|
+
f"Unable to gather implementations: {e}",
|
|
3013
|
+
record_type="implementations",
|
|
3014
|
+
model_layer="implementations",
|
|
3015
|
+
)
|
|
3016
|
+
new_implementations = []
|
|
3017
|
+
try:
|
|
3018
|
+
logger.info(
|
|
3019
|
+
"Parsing and creating Leveraged Authorizations",
|
|
3020
|
+
record_type="leveraged-authorizations",
|
|
3021
|
+
model_layer="leveraged-authorizations",
|
|
3022
|
+
)
|
|
3023
|
+
post_leveraged_authorizations(table_data, ssp_id=regscale_ssp.get("id"))
|
|
3024
|
+
except Exception as e:
|
|
3025
|
+
logger.error(
|
|
3026
|
+
f"Unable to gather leveraged authorizations: {e}",
|
|
3027
|
+
record_type="leveraged-authorizations",
|
|
3028
|
+
model_layer="leveraged-authorizations",
|
|
3029
|
+
)
|
|
3030
|
+
return new_implementations, regscale_ssp
|
|
3031
|
+
|
|
3032
|
+
|
|
3033
|
+
def tables_to_dict(document: Document) -> List[List[Dict]]:
|
|
3034
|
+
"""
|
|
3035
|
+
Convert tables in a document to a list of dictionaries
|
|
3036
|
+
:param Document document: document to convert
|
|
3037
|
+
:return: List of table and its rows as dictionaries
|
|
3038
|
+
:rtype: List[List[Dict]]
|
|
3039
|
+
"""
|
|
3040
|
+
tables_as_dicts = []
|
|
3041
|
+
|
|
3042
|
+
for table in document.tables:
|
|
3043
|
+
replace_content_control(table._element) # remove content controls
|
|
3044
|
+
# Assuming first row is the header
|
|
3045
|
+
keys = [cell.text.strip() for cell in table.rows[0].cells]
|
|
3046
|
+
table_dict = []
|
|
3047
|
+
|
|
3048
|
+
# Iterate over the rest of the rows
|
|
3049
|
+
for row in table.rows[1:]:
|
|
3050
|
+
values = [cell.text.strip() for cell in row.cells]
|
|
3051
|
+
# Create a dict by zipping keys and values together
|
|
3052
|
+
row_dict = dict(zip(keys, values))
|
|
3053
|
+
table_dict.append(row_dict)
|
|
3054
|
+
|
|
3055
|
+
tables_as_dicts.append(table_dict)
|
|
3056
|
+
|
|
3057
|
+
return tables_as_dicts
|
|
3058
|
+
|
|
3059
|
+
|
|
3060
|
+
def revision(doc: Document) -> str:
|
|
3061
|
+
"""The SSP revision."""
|
|
3062
|
+
result = None
|
|
3063
|
+
regex = r"Version\s#*([\d.]+)"
|
|
3064
|
+
try:
|
|
3065
|
+
fed_ramp_revision_string = doc.tables[0].cell(0, 0).text # First cell of the first table.
|
|
3066
|
+
result = re.search(regex, fed_ramp_revision_string).group(1)
|
|
3067
|
+
except (AttributeError, IndexError):
|
|
3068
|
+
print("Warning, unable to return revision information.")
|
|
3069
|
+
return result
|
|
3070
|
+
|
|
3071
|
+
|
|
3072
|
+
def create_initial_ssp(args: Dict) -> Any:
|
|
3073
|
+
"""
|
|
3074
|
+
Create an initial SSP
|
|
3075
|
+
|
|
3076
|
+
:param Dict args: Arguments to create the initial SSP
|
|
3077
|
+
:return: SSP
|
|
3078
|
+
:rtype: Any
|
|
3079
|
+
"""
|
|
3080
|
+
app = Application()
|
|
3081
|
+
api = Api()
|
|
3082
|
+
today_dt = date.today()
|
|
3083
|
+
expiration_date = date(today_dt.year + 3, today_dt.month, today_dt.day)
|
|
3084
|
+
default = "Moderate"
|
|
3085
|
+
profile = args.get("profile")
|
|
3086
|
+
title = args.get("title")
|
|
3087
|
+
version = args.get("version", "")
|
|
3088
|
+
otheridentifier = args.get("otheridentifier", "")
|
|
3089
|
+
confidentiality = capitalize_words(args.get("confidentiality", default))
|
|
3090
|
+
integrity = capitalize_words(args.get("integrity", default))
|
|
3091
|
+
availability = capitalize_words(args.get("availability", default))
|
|
3092
|
+
status = args.get("status", "Operational")
|
|
3093
|
+
system_type = args.get("system_type", SYSTEM_TYPE)
|
|
3094
|
+
revision = args.get("revision", "1.0")
|
|
3095
|
+
description = args.get("description", "Unable to determine System Description")
|
|
3096
|
+
environment = args.get("environment", "")
|
|
3097
|
+
purpose = args.get("purpose", "")
|
|
3098
|
+
modeliaas = args.get("modeiaas", False)
|
|
3099
|
+
modelother = args.get("modeother", False)
|
|
3100
|
+
modelpaas = args.get("modepaas", False)
|
|
3101
|
+
modelsaas = args.get("modesaas", False)
|
|
3102
|
+
deploygov = args.get("deploygov", False)
|
|
3103
|
+
deployhybrid = args.get("deployhybrid", False)
|
|
3104
|
+
deployprivate = args.get("deployprivate", False)
|
|
3105
|
+
deploypublic = args.get("deploypublic", False)
|
|
3106
|
+
deployother = args.get("deployother", False)
|
|
3107
|
+
preporgname = args.get("preporgname", "")
|
|
3108
|
+
prepaddress = args.get("prepaddress", "")
|
|
3109
|
+
prepoffice = args.get("prepoffice", "")
|
|
3110
|
+
prepcitystate = args.get("prepcitystate", "")
|
|
3111
|
+
csporgname = args.get("csporgname", "")
|
|
3112
|
+
cspaddress = args.get("cspaddress", "")
|
|
3113
|
+
cspoffice = args.get("cspoffice", "")
|
|
3114
|
+
cspcitystate = args.get("cspcitystate", "")
|
|
3115
|
+
|
|
3116
|
+
regscale_ssp = SecurityPlan(
|
|
3117
|
+
dateSubmitted=get_current_datetime(),
|
|
3118
|
+
expirationDate=expiration_date.strftime(DATE_FORMAT),
|
|
3119
|
+
approvalDate=expiration_date.strftime(DATE_FORMAT),
|
|
3120
|
+
parentId=profile["id"],
|
|
3121
|
+
parentModule="profiles",
|
|
3122
|
+
systemName=title or "Unable to determine System Name",
|
|
3123
|
+
otherIdentifier=otheridentifier,
|
|
3124
|
+
confidentiality=confidentiality,
|
|
3125
|
+
integrity=integrity,
|
|
3126
|
+
availability=availability,
|
|
3127
|
+
status=status,
|
|
3128
|
+
bDeployGov=deploygov,
|
|
3129
|
+
bDeployHybrid=deployhybrid,
|
|
3130
|
+
bDeployPrivate=deployprivate,
|
|
3131
|
+
bDeployPublic=deploypublic,
|
|
3132
|
+
bDeployOther=deployother,
|
|
3133
|
+
bModelIaaS=modeliaas,
|
|
3134
|
+
bModelOther=modelother,
|
|
3135
|
+
bModelPaaS=modelpaas,
|
|
3136
|
+
bModelSaaS=modelsaas,
|
|
3137
|
+
createdById=app.config["userId"],
|
|
3138
|
+
lastUpdatedById=app.config["userId"],
|
|
3139
|
+
systemOwnerId=app.config["userId"],
|
|
3140
|
+
planAuthorizingOfficialId=app.config["userId"],
|
|
3141
|
+
planInformationSystemSecurityOfficerId=app.config["userId"],
|
|
3142
|
+
systemType=system_type,
|
|
3143
|
+
overallCategorization="Moderate",
|
|
3144
|
+
description=description,
|
|
3145
|
+
purpose=purpose,
|
|
3146
|
+
environment=environment,
|
|
3147
|
+
executiveSummary=f"Revision: {revision}",
|
|
3148
|
+
version=version,
|
|
3149
|
+
prepOrgName=preporgname,
|
|
3150
|
+
prepAddress=prepaddress,
|
|
3151
|
+
prepOffice=prepoffice,
|
|
3152
|
+
prepCityState=prepcitystate,
|
|
3153
|
+
cspOrgName=csporgname,
|
|
3154
|
+
cspAddress=cspaddress,
|
|
3155
|
+
cspOffice=cspoffice,
|
|
3156
|
+
cspCityState=cspcitystate,
|
|
3157
|
+
)
|
|
3158
|
+
|
|
3159
|
+
if regscale_ssp.status != "Operational":
|
|
3160
|
+
regscale_ssp.explanationForNonOperational = "Unable to determine status from SSP during FedRAMP .docx import."
|
|
3161
|
+
existing_security_plans_reponse = api.get(
|
|
3162
|
+
url=urljoin(app.config["domain"], SSP_URL_SUFFIX),
|
|
3163
|
+
)
|
|
3164
|
+
existing_security_plans = []
|
|
3165
|
+
if not existing_security_plans_reponse.ok:
|
|
3166
|
+
logger.info("No Security Plans found")
|
|
3167
|
+
else:
|
|
3168
|
+
existing_security_plans = existing_security_plans_reponse.json()
|
|
3169
|
+
if len(existing_security_plans) >= 1:
|
|
3170
|
+
existing_ssps_dict = {
|
|
3171
|
+
ssp.get("title").lower(): ssp for ssp in existing_security_plans if ssp and "title" in ssp
|
|
3172
|
+
}
|
|
3173
|
+
if regscale_ssp.systemName.lower() not in existing_ssps_dict:
|
|
3174
|
+
regscale_ssp = regscale_ssp.create()
|
|
3175
|
+
else:
|
|
3176
|
+
existing_ssp = existing_ssps_dict.get(regscale_ssp.systemName.lower())
|
|
3177
|
+
regscale_ssp.id = existing_ssp.get("id")
|
|
3178
|
+
regscale_ssp = regscale_ssp.save()
|
|
3179
|
+
else:
|
|
3180
|
+
regscale_ssp = regscale_ssp.create()
|
|
3181
|
+
return regscale_ssp.dict()
|