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.

Files changed (481) hide show
  1. regscale/__init__.py +1 -0
  2. regscale/airflow/__init__.py +9 -0
  3. regscale/airflow/azure/__init__.py +9 -0
  4. regscale/airflow/azure/cli.py +89 -0
  5. regscale/airflow/azure/upload_dags.py +116 -0
  6. regscale/airflow/click_dags.py +127 -0
  7. regscale/airflow/click_mixins.py +82 -0
  8. regscale/airflow/config.py +25 -0
  9. regscale/airflow/factories/__init__.py +0 -0
  10. regscale/airflow/factories/connections.py +58 -0
  11. regscale/airflow/factories/workflows.py +78 -0
  12. regscale/airflow/hierarchy.py +88 -0
  13. regscale/airflow/operators/__init__.py +0 -0
  14. regscale/airflow/operators/click.py +36 -0
  15. regscale/airflow/sensors/__init__.py +0 -0
  16. regscale/airflow/sensors/sql.py +107 -0
  17. regscale/airflow/sessions/__init__.py +0 -0
  18. regscale/airflow/sessions/sql/__init__.py +3 -0
  19. regscale/airflow/sessions/sql/queries.py +64 -0
  20. regscale/airflow/sessions/sql/sql_server_queries.py +248 -0
  21. regscale/airflow/tasks/__init__.py +0 -0
  22. regscale/airflow/tasks/branches.py +22 -0
  23. regscale/airflow/tasks/cli.py +116 -0
  24. regscale/airflow/tasks/click.py +73 -0
  25. regscale/airflow/tasks/debugging.py +9 -0
  26. regscale/airflow/tasks/groups.py +116 -0
  27. regscale/airflow/tasks/init.py +60 -0
  28. regscale/airflow/tasks/states.py +47 -0
  29. regscale/airflow/tasks/workflows.py +36 -0
  30. regscale/ansible/__init__.py +9 -0
  31. regscale/core/__init__.py +0 -0
  32. regscale/core/app/__init__.py +3 -0
  33. regscale/core/app/api.py +571 -0
  34. regscale/core/app/application.py +665 -0
  35. regscale/core/app/internal/__init__.py +136 -0
  36. regscale/core/app/internal/admin_actions.py +230 -0
  37. regscale/core/app/internal/assessments_editor.py +873 -0
  38. regscale/core/app/internal/catalog.py +316 -0
  39. regscale/core/app/internal/comparison.py +459 -0
  40. regscale/core/app/internal/control_editor.py +571 -0
  41. regscale/core/app/internal/encrypt.py +79 -0
  42. regscale/core/app/internal/evidence.py +1240 -0
  43. regscale/core/app/internal/file_uploads.py +151 -0
  44. regscale/core/app/internal/healthcheck.py +66 -0
  45. regscale/core/app/internal/login.py +305 -0
  46. regscale/core/app/internal/migrations.py +240 -0
  47. regscale/core/app/internal/model_editor.py +1701 -0
  48. regscale/core/app/internal/poam_editor.py +632 -0
  49. regscale/core/app/internal/workflow.py +105 -0
  50. regscale/core/app/logz.py +74 -0
  51. regscale/core/app/utils/XMLIR.py +258 -0
  52. regscale/core/app/utils/__init__.py +0 -0
  53. regscale/core/app/utils/api_handler.py +358 -0
  54. regscale/core/app/utils/app_utils.py +1110 -0
  55. regscale/core/app/utils/catalog_utils/__init__.py +0 -0
  56. regscale/core/app/utils/catalog_utils/common.py +91 -0
  57. regscale/core/app/utils/catalog_utils/compare_catalog.py +193 -0
  58. regscale/core/app/utils/catalog_utils/diagnostic_catalog.py +97 -0
  59. regscale/core/app/utils/catalog_utils/download_catalog.py +103 -0
  60. regscale/core/app/utils/catalog_utils/update_catalog.py +718 -0
  61. regscale/core/app/utils/catalog_utils/update_catalog_v2.py +1378 -0
  62. regscale/core/app/utils/catalog_utils/update_catalog_v3.py +1272 -0
  63. regscale/core/app/utils/catalog_utils/update_plans.py +334 -0
  64. regscale/core/app/utils/file_utils.py +238 -0
  65. regscale/core/app/utils/parser_utils.py +81 -0
  66. regscale/core/app/utils/pickle_file_handler.py +57 -0
  67. regscale/core/app/utils/regscale_utils.py +319 -0
  68. regscale/core/app/utils/report_utils.py +119 -0
  69. regscale/core/app/utils/variables.py +226 -0
  70. regscale/core/decorators.py +31 -0
  71. regscale/core/lazy_group.py +65 -0
  72. regscale/core/login.py +63 -0
  73. regscale/core/server/__init__.py +0 -0
  74. regscale/core/server/flask_api.py +473 -0
  75. regscale/core/server/helpers.py +373 -0
  76. regscale/core/server/rest.py +64 -0
  77. regscale/core/server/static/css/bootstrap.css +6030 -0
  78. regscale/core/server/static/css/bootstrap.min.css +6 -0
  79. regscale/core/server/static/css/main.css +176 -0
  80. regscale/core/server/static/images/regscale-cli.svg +49 -0
  81. regscale/core/server/static/images/regscale.svg +38 -0
  82. regscale/core/server/templates/base.html +74 -0
  83. regscale/core/server/templates/index.html +43 -0
  84. regscale/core/server/templates/login.html +28 -0
  85. regscale/core/server/templates/make_base64.html +22 -0
  86. regscale/core/server/templates/upload_STIG.html +109 -0
  87. regscale/core/server/templates/upload_STIG_result.html +26 -0
  88. regscale/core/server/templates/upload_ssp.html +144 -0
  89. regscale/core/server/templates/upload_ssp_result.html +128 -0
  90. regscale/core/static/__init__.py +0 -0
  91. regscale/core/static/regex.py +14 -0
  92. regscale/core/utils/__init__.py +117 -0
  93. regscale/core/utils/click_utils.py +13 -0
  94. regscale/core/utils/date.py +238 -0
  95. regscale/core/utils/graphql.py +254 -0
  96. regscale/core/utils/urls.py +23 -0
  97. regscale/dev/__init__.py +6 -0
  98. regscale/dev/analysis.py +454 -0
  99. regscale/dev/cli.py +235 -0
  100. regscale/dev/code_gen.py +492 -0
  101. regscale/dev/dirs.py +69 -0
  102. regscale/dev/docs.py +384 -0
  103. regscale/dev/monitoring.py +26 -0
  104. regscale/dev/profiling.py +216 -0
  105. regscale/exceptions/__init__.py +4 -0
  106. regscale/exceptions/license_exception.py +7 -0
  107. regscale/exceptions/validation_exception.py +9 -0
  108. regscale/integrations/__init__.py +1 -0
  109. regscale/integrations/commercial/__init__.py +486 -0
  110. regscale/integrations/commercial/ad.py +433 -0
  111. regscale/integrations/commercial/amazon/__init__.py +0 -0
  112. regscale/integrations/commercial/amazon/common.py +106 -0
  113. regscale/integrations/commercial/aqua/__init__.py +0 -0
  114. regscale/integrations/commercial/aqua/aqua.py +91 -0
  115. regscale/integrations/commercial/aws/__init__.py +6 -0
  116. regscale/integrations/commercial/aws/cli.py +322 -0
  117. regscale/integrations/commercial/aws/inventory/__init__.py +110 -0
  118. regscale/integrations/commercial/aws/inventory/base.py +64 -0
  119. regscale/integrations/commercial/aws/inventory/resources/__init__.py +19 -0
  120. regscale/integrations/commercial/aws/inventory/resources/compute.py +234 -0
  121. regscale/integrations/commercial/aws/inventory/resources/containers.py +113 -0
  122. regscale/integrations/commercial/aws/inventory/resources/database.py +101 -0
  123. regscale/integrations/commercial/aws/inventory/resources/integration.py +237 -0
  124. regscale/integrations/commercial/aws/inventory/resources/networking.py +253 -0
  125. regscale/integrations/commercial/aws/inventory/resources/security.py +240 -0
  126. regscale/integrations/commercial/aws/inventory/resources/storage.py +91 -0
  127. regscale/integrations/commercial/aws/scanner.py +823 -0
  128. regscale/integrations/commercial/azure/__init__.py +0 -0
  129. regscale/integrations/commercial/azure/common.py +32 -0
  130. regscale/integrations/commercial/azure/intune.py +488 -0
  131. regscale/integrations/commercial/azure/scanner.py +49 -0
  132. regscale/integrations/commercial/burp.py +78 -0
  133. regscale/integrations/commercial/cpe.py +144 -0
  134. regscale/integrations/commercial/crowdstrike.py +1117 -0
  135. regscale/integrations/commercial/defender.py +1511 -0
  136. regscale/integrations/commercial/dependabot.py +210 -0
  137. regscale/integrations/commercial/durosuite/__init__.py +0 -0
  138. regscale/integrations/commercial/durosuite/api.py +1546 -0
  139. regscale/integrations/commercial/durosuite/process_devices.py +101 -0
  140. regscale/integrations/commercial/durosuite/scanner.py +637 -0
  141. regscale/integrations/commercial/durosuite/variables.py +21 -0
  142. regscale/integrations/commercial/ecr.py +90 -0
  143. regscale/integrations/commercial/gcp/__init__.py +237 -0
  144. regscale/integrations/commercial/gcp/auth.py +96 -0
  145. regscale/integrations/commercial/gcp/control_tests.py +238 -0
  146. regscale/integrations/commercial/gcp/variables.py +18 -0
  147. regscale/integrations/commercial/gitlab.py +332 -0
  148. regscale/integrations/commercial/grype.py +165 -0
  149. regscale/integrations/commercial/ibm.py +90 -0
  150. regscale/integrations/commercial/import_all/__init__.py +0 -0
  151. regscale/integrations/commercial/import_all/import_all_cmd.py +467 -0
  152. regscale/integrations/commercial/import_all/scan_file_fingerprints.json +27 -0
  153. regscale/integrations/commercial/jira.py +1046 -0
  154. regscale/integrations/commercial/mappings/__init__.py +0 -0
  155. regscale/integrations/commercial/mappings/csf_controls.json +713 -0
  156. regscale/integrations/commercial/mappings/nist_800_53_r5_controls.json +1516 -0
  157. regscale/integrations/commercial/nessus/__init__.py +0 -0
  158. regscale/integrations/commercial/nessus/nessus_utils.py +429 -0
  159. regscale/integrations/commercial/nessus/scanner.py +416 -0
  160. regscale/integrations/commercial/nexpose.py +90 -0
  161. regscale/integrations/commercial/okta.py +798 -0
  162. regscale/integrations/commercial/opentext/__init__.py +0 -0
  163. regscale/integrations/commercial/opentext/click.py +99 -0
  164. regscale/integrations/commercial/opentext/scanner.py +143 -0
  165. regscale/integrations/commercial/prisma.py +91 -0
  166. regscale/integrations/commercial/qualys.py +1462 -0
  167. regscale/integrations/commercial/salesforce.py +980 -0
  168. regscale/integrations/commercial/sap/__init__.py +0 -0
  169. regscale/integrations/commercial/sap/click.py +31 -0
  170. regscale/integrations/commercial/sap/sysdig/__init__.py +0 -0
  171. regscale/integrations/commercial/sap/sysdig/click.py +57 -0
  172. regscale/integrations/commercial/sap/sysdig/sysdig_scanner.py +190 -0
  173. regscale/integrations/commercial/sap/tenable/__init__.py +0 -0
  174. regscale/integrations/commercial/sap/tenable/click.py +49 -0
  175. regscale/integrations/commercial/sap/tenable/scanner.py +196 -0
  176. regscale/integrations/commercial/servicenow.py +1756 -0
  177. regscale/integrations/commercial/sicura/__init__.py +0 -0
  178. regscale/integrations/commercial/sicura/api.py +855 -0
  179. regscale/integrations/commercial/sicura/commands.py +73 -0
  180. regscale/integrations/commercial/sicura/scanner.py +481 -0
  181. regscale/integrations/commercial/sicura/variables.py +16 -0
  182. regscale/integrations/commercial/snyk.py +90 -0
  183. regscale/integrations/commercial/sonarcloud.py +260 -0
  184. regscale/integrations/commercial/sqlserver.py +369 -0
  185. regscale/integrations/commercial/stig_mapper_integration/__init__.py +0 -0
  186. regscale/integrations/commercial/stig_mapper_integration/click_commands.py +38 -0
  187. regscale/integrations/commercial/stig_mapper_integration/mapping_engine.py +353 -0
  188. regscale/integrations/commercial/stigv2/__init__.py +0 -0
  189. regscale/integrations/commercial/stigv2/ckl_parser.py +349 -0
  190. regscale/integrations/commercial/stigv2/click_commands.py +95 -0
  191. regscale/integrations/commercial/stigv2/stig_integration.py +202 -0
  192. regscale/integrations/commercial/synqly/__init__.py +0 -0
  193. regscale/integrations/commercial/synqly/assets.py +46 -0
  194. regscale/integrations/commercial/synqly/ticketing.py +132 -0
  195. regscale/integrations/commercial/synqly/vulnerabilities.py +223 -0
  196. regscale/integrations/commercial/synqly_jira.py +840 -0
  197. regscale/integrations/commercial/tenablev2/__init__.py +0 -0
  198. regscale/integrations/commercial/tenablev2/authenticate.py +31 -0
  199. regscale/integrations/commercial/tenablev2/click.py +1584 -0
  200. regscale/integrations/commercial/tenablev2/scanner.py +504 -0
  201. regscale/integrations/commercial/tenablev2/stig_parsers.py +140 -0
  202. regscale/integrations/commercial/tenablev2/utils.py +78 -0
  203. regscale/integrations/commercial/tenablev2/variables.py +17 -0
  204. regscale/integrations/commercial/trivy.py +162 -0
  205. regscale/integrations/commercial/veracode.py +96 -0
  206. regscale/integrations/commercial/wizv2/WizDataMixin.py +97 -0
  207. regscale/integrations/commercial/wizv2/__init__.py +0 -0
  208. regscale/integrations/commercial/wizv2/click.py +429 -0
  209. regscale/integrations/commercial/wizv2/constants.py +1001 -0
  210. regscale/integrations/commercial/wizv2/issue.py +361 -0
  211. regscale/integrations/commercial/wizv2/models.py +112 -0
  212. regscale/integrations/commercial/wizv2/parsers.py +339 -0
  213. regscale/integrations/commercial/wizv2/sbom.py +115 -0
  214. regscale/integrations/commercial/wizv2/scanner.py +416 -0
  215. regscale/integrations/commercial/wizv2/utils.py +796 -0
  216. regscale/integrations/commercial/wizv2/variables.py +39 -0
  217. regscale/integrations/commercial/wizv2/wiz_auth.py +159 -0
  218. regscale/integrations/commercial/xray.py +91 -0
  219. regscale/integrations/integration/__init__.py +2 -0
  220. regscale/integrations/integration/integration.py +26 -0
  221. regscale/integrations/integration/inventory.py +17 -0
  222. regscale/integrations/integration/issue.py +100 -0
  223. regscale/integrations/integration_override.py +149 -0
  224. regscale/integrations/public/__init__.py +103 -0
  225. regscale/integrations/public/cisa.py +641 -0
  226. regscale/integrations/public/criticality_updater.py +70 -0
  227. regscale/integrations/public/emass.py +411 -0
  228. regscale/integrations/public/emass_slcm_import.py +697 -0
  229. regscale/integrations/public/fedramp/__init__.py +0 -0
  230. regscale/integrations/public/fedramp/appendix_parser.py +548 -0
  231. regscale/integrations/public/fedramp/click.py +479 -0
  232. regscale/integrations/public/fedramp/components.py +714 -0
  233. regscale/integrations/public/fedramp/docx_parser.py +259 -0
  234. regscale/integrations/public/fedramp/fedramp_cis_crm.py +1124 -0
  235. regscale/integrations/public/fedramp/fedramp_common.py +3181 -0
  236. regscale/integrations/public/fedramp/fedramp_docx.py +388 -0
  237. regscale/integrations/public/fedramp/fedramp_five.py +2343 -0
  238. regscale/integrations/public/fedramp/fedramp_traversal.py +138 -0
  239. regscale/integrations/public/fedramp/import_fedramp_r4_ssp.py +279 -0
  240. regscale/integrations/public/fedramp/import_workbook.py +495 -0
  241. regscale/integrations/public/fedramp/inventory_items.py +244 -0
  242. regscale/integrations/public/fedramp/mappings/__init__.py +0 -0
  243. regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +7388 -0
  244. regscale/integrations/public/fedramp/mappings/fedramp_r5_params.json +8636 -0
  245. regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +9605 -0
  246. regscale/integrations/public/fedramp/mappings/system_roles.py +34 -0
  247. regscale/integrations/public/fedramp/mappings/user.py +175 -0
  248. regscale/integrations/public/fedramp/mappings/values.py +141 -0
  249. regscale/integrations/public/fedramp/markdown_parser.py +150 -0
  250. regscale/integrations/public/fedramp/metadata.py +689 -0
  251. regscale/integrations/public/fedramp/models/__init__.py +59 -0
  252. regscale/integrations/public/fedramp/models/leveraged_auth_new.py +168 -0
  253. regscale/integrations/public/fedramp/models/poam_importer.py +522 -0
  254. regscale/integrations/public/fedramp/parts_mapper.py +107 -0
  255. regscale/integrations/public/fedramp/poam/__init__.py +0 -0
  256. regscale/integrations/public/fedramp/poam/scanner.py +851 -0
  257. regscale/integrations/public/fedramp/properties.py +201 -0
  258. regscale/integrations/public/fedramp/reporting.py +84 -0
  259. regscale/integrations/public/fedramp/resources.py +496 -0
  260. regscale/integrations/public/fedramp/rosetta.py +110 -0
  261. regscale/integrations/public/fedramp/ssp_logger.py +87 -0
  262. regscale/integrations/public/fedramp/system_characteristics.py +922 -0
  263. regscale/integrations/public/fedramp/system_control_implementations.py +582 -0
  264. regscale/integrations/public/fedramp/system_implementation.py +190 -0
  265. regscale/integrations/public/fedramp/xml_utils.py +87 -0
  266. regscale/integrations/public/nist_catalog.py +275 -0
  267. regscale/integrations/public/oscal.py +1946 -0
  268. regscale/integrations/public/otx.py +169 -0
  269. regscale/integrations/scanner_integration.py +2692 -0
  270. regscale/integrations/variables.py +25 -0
  271. regscale/models/__init__.py +7 -0
  272. regscale/models/app_models/__init__.py +5 -0
  273. regscale/models/app_models/catalog_compare.py +213 -0
  274. regscale/models/app_models/click.py +252 -0
  275. regscale/models/app_models/datetime_encoder.py +21 -0
  276. regscale/models/app_models/import_validater.py +321 -0
  277. regscale/models/app_models/mapping.py +260 -0
  278. regscale/models/app_models/pipeline.py +37 -0
  279. regscale/models/click_models.py +413 -0
  280. regscale/models/config.py +154 -0
  281. regscale/models/email_style.css +67 -0
  282. regscale/models/hierarchy.py +8 -0
  283. regscale/models/inspect_models.py +79 -0
  284. regscale/models/integration_models/__init__.py +0 -0
  285. regscale/models/integration_models/amazon_models/__init__.py +0 -0
  286. regscale/models/integration_models/amazon_models/inspector.py +262 -0
  287. regscale/models/integration_models/amazon_models/inspector_scan.py +206 -0
  288. regscale/models/integration_models/aqua.py +247 -0
  289. regscale/models/integration_models/azure_alerts.py +255 -0
  290. regscale/models/integration_models/base64.py +23 -0
  291. regscale/models/integration_models/burp.py +433 -0
  292. regscale/models/integration_models/burp_models.py +128 -0
  293. regscale/models/integration_models/cisa_kev_data.json +19333 -0
  294. regscale/models/integration_models/defender_data.py +93 -0
  295. regscale/models/integration_models/defenderimport.py +143 -0
  296. regscale/models/integration_models/drf.py +443 -0
  297. regscale/models/integration_models/ecr_models/__init__.py +0 -0
  298. regscale/models/integration_models/ecr_models/data.py +69 -0
  299. regscale/models/integration_models/ecr_models/ecr.py +239 -0
  300. regscale/models/integration_models/flat_file_importer.py +1079 -0
  301. regscale/models/integration_models/grype_import.py +247 -0
  302. regscale/models/integration_models/ibm.py +126 -0
  303. regscale/models/integration_models/implementation_results.py +85 -0
  304. regscale/models/integration_models/nexpose.py +140 -0
  305. regscale/models/integration_models/prisma.py +202 -0
  306. regscale/models/integration_models/qualys.py +720 -0
  307. regscale/models/integration_models/qualys_scanner.py +160 -0
  308. regscale/models/integration_models/sbom/__init__.py +0 -0
  309. regscale/models/integration_models/sbom/cyclone_dx.py +139 -0
  310. regscale/models/integration_models/send_reminders.py +620 -0
  311. regscale/models/integration_models/snyk.py +155 -0
  312. regscale/models/integration_models/synqly_models/__init__.py +0 -0
  313. regscale/models/integration_models/synqly_models/capabilities.json +1 -0
  314. regscale/models/integration_models/synqly_models/connector_types.py +22 -0
  315. regscale/models/integration_models/synqly_models/connectors/__init__.py +7 -0
  316. regscale/models/integration_models/synqly_models/connectors/assets.py +97 -0
  317. regscale/models/integration_models/synqly_models/connectors/ticketing.py +583 -0
  318. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +169 -0
  319. regscale/models/integration_models/synqly_models/ocsf_mapper.py +331 -0
  320. regscale/models/integration_models/synqly_models/param.py +72 -0
  321. regscale/models/integration_models/synqly_models/synqly_model.py +733 -0
  322. regscale/models/integration_models/synqly_models/tenants.py +39 -0
  323. regscale/models/integration_models/tenable_models/__init__.py +0 -0
  324. regscale/models/integration_models/tenable_models/integration.py +187 -0
  325. regscale/models/integration_models/tenable_models/models.py +513 -0
  326. regscale/models/integration_models/trivy_import.py +231 -0
  327. regscale/models/integration_models/veracode.py +217 -0
  328. regscale/models/integration_models/xray.py +135 -0
  329. regscale/models/locking.py +100 -0
  330. regscale/models/platform.py +110 -0
  331. regscale/models/regscale_models/__init__.py +67 -0
  332. regscale/models/regscale_models/assessment.py +570 -0
  333. regscale/models/regscale_models/assessment_plan.py +52 -0
  334. regscale/models/regscale_models/asset.py +567 -0
  335. regscale/models/regscale_models/asset_mapping.py +190 -0
  336. regscale/models/regscale_models/case.py +42 -0
  337. regscale/models/regscale_models/catalog.py +261 -0
  338. regscale/models/regscale_models/cci.py +46 -0
  339. regscale/models/regscale_models/change.py +167 -0
  340. regscale/models/regscale_models/checklist.py +372 -0
  341. regscale/models/regscale_models/comment.py +49 -0
  342. regscale/models/regscale_models/compliance_settings.py +112 -0
  343. regscale/models/regscale_models/component.py +412 -0
  344. regscale/models/regscale_models/component_mapping.py +65 -0
  345. regscale/models/regscale_models/control.py +38 -0
  346. regscale/models/regscale_models/control_implementation.py +1128 -0
  347. regscale/models/regscale_models/control_objective.py +261 -0
  348. regscale/models/regscale_models/control_parameter.py +100 -0
  349. regscale/models/regscale_models/control_test.py +34 -0
  350. regscale/models/regscale_models/control_test_plan.py +75 -0
  351. regscale/models/regscale_models/control_test_result.py +52 -0
  352. regscale/models/regscale_models/custom_field.py +245 -0
  353. regscale/models/regscale_models/data.py +109 -0
  354. regscale/models/regscale_models/data_center.py +40 -0
  355. regscale/models/regscale_models/deviation.py +203 -0
  356. regscale/models/regscale_models/email.py +97 -0
  357. regscale/models/regscale_models/evidence.py +47 -0
  358. regscale/models/regscale_models/evidence_mapping.py +40 -0
  359. regscale/models/regscale_models/facility.py +59 -0
  360. regscale/models/regscale_models/file.py +382 -0
  361. regscale/models/regscale_models/filetag.py +37 -0
  362. regscale/models/regscale_models/form_field_value.py +94 -0
  363. regscale/models/regscale_models/group.py +169 -0
  364. regscale/models/regscale_models/implementation_objective.py +335 -0
  365. regscale/models/regscale_models/implementation_option.py +275 -0
  366. regscale/models/regscale_models/implementation_role.py +33 -0
  367. regscale/models/regscale_models/incident.py +177 -0
  368. regscale/models/regscale_models/interconnection.py +43 -0
  369. regscale/models/regscale_models/issue.py +1176 -0
  370. regscale/models/regscale_models/leveraged_authorization.py +125 -0
  371. regscale/models/regscale_models/line_of_inquiry.py +52 -0
  372. regscale/models/regscale_models/link.py +205 -0
  373. regscale/models/regscale_models/meta_data.py +64 -0
  374. regscale/models/regscale_models/mixins/__init__.py +0 -0
  375. regscale/models/regscale_models/mixins/parent_cache.py +124 -0
  376. regscale/models/regscale_models/module.py +224 -0
  377. regscale/models/regscale_models/modules.py +191 -0
  378. regscale/models/regscale_models/objective.py +14 -0
  379. regscale/models/regscale_models/parameter.py +87 -0
  380. regscale/models/regscale_models/ports_protocol.py +81 -0
  381. regscale/models/regscale_models/privacy.py +89 -0
  382. regscale/models/regscale_models/profile.py +50 -0
  383. regscale/models/regscale_models/profile_link.py +68 -0
  384. regscale/models/regscale_models/profile_mapping.py +124 -0
  385. regscale/models/regscale_models/project.py +63 -0
  386. regscale/models/regscale_models/property.py +278 -0
  387. regscale/models/regscale_models/question.py +85 -0
  388. regscale/models/regscale_models/questionnaire.py +87 -0
  389. regscale/models/regscale_models/questionnaire_instance.py +177 -0
  390. regscale/models/regscale_models/rbac.py +132 -0
  391. regscale/models/regscale_models/reference.py +86 -0
  392. regscale/models/regscale_models/regscale_model.py +1643 -0
  393. regscale/models/regscale_models/requirement.py +29 -0
  394. regscale/models/regscale_models/risk.py +274 -0
  395. regscale/models/regscale_models/sbom.py +54 -0
  396. regscale/models/regscale_models/scan_history.py +436 -0
  397. regscale/models/regscale_models/search.py +53 -0
  398. regscale/models/regscale_models/security_control.py +132 -0
  399. regscale/models/regscale_models/security_plan.py +204 -0
  400. regscale/models/regscale_models/software_inventory.py +159 -0
  401. regscale/models/regscale_models/stake_holder.py +64 -0
  402. regscale/models/regscale_models/stig.py +647 -0
  403. regscale/models/regscale_models/supply_chain.py +152 -0
  404. regscale/models/regscale_models/system_role.py +188 -0
  405. regscale/models/regscale_models/system_role_external_assignment.py +40 -0
  406. regscale/models/regscale_models/tag.py +37 -0
  407. regscale/models/regscale_models/tag_mapping.py +19 -0
  408. regscale/models/regscale_models/task.py +133 -0
  409. regscale/models/regscale_models/threat.py +196 -0
  410. regscale/models/regscale_models/user.py +175 -0
  411. regscale/models/regscale_models/user_group.py +55 -0
  412. regscale/models/regscale_models/vulnerability.py +242 -0
  413. regscale/models/regscale_models/vulnerability_mapping.py +162 -0
  414. regscale/models/regscale_models/workflow.py +55 -0
  415. regscale/models/regscale_models/workflow_action.py +34 -0
  416. regscale/models/regscale_models/workflow_instance.py +269 -0
  417. regscale/models/regscale_models/workflow_instance_step.py +114 -0
  418. regscale/models/regscale_models/workflow_template.py +58 -0
  419. regscale/models/regscale_models/workflow_template_step.py +45 -0
  420. regscale/regscale.py +815 -0
  421. regscale/utils/__init__.py +7 -0
  422. regscale/utils/b64conversion.py +14 -0
  423. regscale/utils/click_utils.py +118 -0
  424. regscale/utils/decorators.py +48 -0
  425. regscale/utils/dict_utils.py +59 -0
  426. regscale/utils/files.py +79 -0
  427. regscale/utils/fxns.py +30 -0
  428. regscale/utils/graphql_client.py +113 -0
  429. regscale/utils/lists.py +16 -0
  430. regscale/utils/numbers.py +12 -0
  431. regscale/utils/shell.py +148 -0
  432. regscale/utils/string.py +121 -0
  433. regscale/utils/synqly_utils.py +165 -0
  434. regscale/utils/threading/__init__.py +8 -0
  435. regscale/utils/threading/threadhandler.py +131 -0
  436. regscale/utils/threading/threadsafe_counter.py +47 -0
  437. regscale/utils/threading/threadsafe_dict.py +242 -0
  438. regscale/utils/threading/threadsafe_list.py +83 -0
  439. regscale/utils/version.py +104 -0
  440. regscale/validation/__init__.py +0 -0
  441. regscale/validation/address.py +37 -0
  442. regscale/validation/record.py +48 -0
  443. regscale/visualization/__init__.py +5 -0
  444. regscale/visualization/click.py +34 -0
  445. regscale_cli-6.16.0.0.dist-info/LICENSE +21 -0
  446. regscale_cli-6.16.0.0.dist-info/METADATA +659 -0
  447. regscale_cli-6.16.0.0.dist-info/RECORD +481 -0
  448. regscale_cli-6.16.0.0.dist-info/WHEEL +5 -0
  449. regscale_cli-6.16.0.0.dist-info/entry_points.txt +6 -0
  450. regscale_cli-6.16.0.0.dist-info/top_level.txt +2 -0
  451. tests/fixtures/__init__.py +2 -0
  452. tests/fixtures/api.py +87 -0
  453. tests/fixtures/models.py +91 -0
  454. tests/fixtures/test_fixture.py +144 -0
  455. tests/mocks/__init__.py +0 -0
  456. tests/mocks/objects.py +3 -0
  457. tests/mocks/response.py +32 -0
  458. tests/mocks/xml.py +13 -0
  459. tests/regscale/__init__.py +0 -0
  460. tests/regscale/core/__init__.py +0 -0
  461. tests/regscale/core/test_api.py +232 -0
  462. tests/regscale/core/test_app.py +406 -0
  463. tests/regscale/core/test_login.py +37 -0
  464. tests/regscale/core/test_logz.py +66 -0
  465. tests/regscale/core/test_sbom_generator.py +87 -0
  466. tests/regscale/core/test_validation_utils.py +163 -0
  467. tests/regscale/core/test_version.py +78 -0
  468. tests/regscale/models/__init__.py +0 -0
  469. tests/regscale/models/test_asset.py +71 -0
  470. tests/regscale/models/test_config.py +26 -0
  471. tests/regscale/models/test_control_implementation.py +27 -0
  472. tests/regscale/models/test_import.py +97 -0
  473. tests/regscale/models/test_issue.py +36 -0
  474. tests/regscale/models/test_mapping.py +52 -0
  475. tests/regscale/models/test_platform.py +31 -0
  476. tests/regscale/models/test_regscale_model.py +346 -0
  477. tests/regscale/models/test_report.py +32 -0
  478. tests/regscale/models/test_tenable_integrations.py +118 -0
  479. tests/regscale/models/test_user_model.py +121 -0
  480. tests/regscale/test_about.py +19 -0
  481. 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()