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,1946 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Integrates OSCAL into RegScale"""
4
+
5
+ # standard python imports
6
+ import json
7
+ import re
8
+ import tempfile
9
+ import uuid
10
+ from os import remove, sep
11
+ from typing import Tuple, Union, Optional
12
+
13
+ import click
14
+ import requests
15
+ import xmltodict
16
+ import yaml
17
+ from bs4 import BeautifulSoup
18
+ from packaging import version as package_version
19
+ from pathlib import Path
20
+
21
+ from regscale.core.app.api import Api
22
+ from regscale.core.app.application import Application
23
+ from regscale.core.app.logz import create_logger
24
+ from regscale.core.app.utils.app_utils import (
25
+ check_file_path,
26
+ create_progress_object,
27
+ error_and_exit,
28
+ find_keys,
29
+ get_file_name,
30
+ reformat_str_date,
31
+ save_data_to,
32
+ )
33
+ from regscale.models.regscale_models import Catalog, ControlImplementation, Component, SecurityControl
34
+ from regscale.utils.threading.threadhandler import create_threads, thread_assignment
35
+
36
+ # create global variables
37
+ job_progress = create_progress_object()
38
+ logger = create_logger()
39
+ OBJ_TO_CONTROLS = False
40
+
41
+ (
42
+ new_controls,
43
+ new_regscale_controls,
44
+ errors,
45
+ new_params,
46
+ new_tests,
47
+ new_objectives,
48
+ updates,
49
+ ) = ([], [], [], [], [], [], [])
50
+ SC_URL = "/api/securitycontrols/"
51
+ UL_CLOSE = "</ul>"
52
+ LI = "<li>"
53
+ LI_CLOSE = "</li>"
54
+
55
+
56
+ @click.group()
57
+ def oscal():
58
+ """Performs bulk processing of OSCAL files."""
59
+
60
+
61
+ # OSCAL Version Support
62
+ @oscal.command()
63
+ def version():
64
+ """Info on current OSCAL version supported by RegScale."""
65
+ logger.info("RegScale currently supports OSCAL Version 1.0.")
66
+
67
+
68
+ def convert_oscal_comp_to_regscale(j_data: dict) -> None:
69
+ """
70
+ Convert OSCAL component dict into a RegScale Component
71
+
72
+ :param dict j_data: OSCAL component
73
+ :rtype: None
74
+ """
75
+ app = Application()
76
+ config = app.config
77
+ api = Api()
78
+ component_id = None
79
+ regscale_components = []
80
+ components = []
81
+ existing_components = api.get(url=config["domain"] + "/api/components/getList").json()
82
+ controls_to_be_added = []
83
+
84
+ try:
85
+ components = j_data["component-definition"]["components"]
86
+ except KeyError as kex:
87
+ error_and_exit(f"Key Error! {kex}")
88
+
89
+ for comp in components:
90
+ control_implementations = comp["control-implementations"]
91
+
92
+ base_catalog = ""
93
+ title = ""
94
+
95
+ # alternative
96
+ if len(comp["control-implementations"]) > 0:
97
+ base_catalog = comp["control-implementations"][0]["source"]
98
+ else:
99
+ logger.info(f"Component {comp['title']} contains no control implementation statements")
100
+
101
+ component = Component(
102
+ title=comp.get("title", ""),
103
+ componentOwnerId=app.config["userId"],
104
+ componentType=comp.get("type", "").lower(),
105
+ description=comp.get("description", ""),
106
+ purpose=comp.get("purpose", ""),
107
+ )
108
+ regscale_components.append(component.dict())
109
+ for control_implements in control_implementations:
110
+ if "implemented-requirements" in control_implements:
111
+ for control_data in control_implements["implemented-requirements"]:
112
+ control_data = {
113
+ "component": comp["title"],
114
+ # "id": control_data["control-id"],
115
+ "title": control_data["control-id"],
116
+ "description": control_data["description"],
117
+ }
118
+ controls_to_be_added.append(control_data)
119
+ elif base_catalog and "implemented-requirements" not in control_implements:
120
+ # Get control data from base catalog
121
+ logger.debug("base_catalog: %s\ntitle: %s", base_catalog, title)
122
+
123
+ for reg in regscale_components:
124
+ check_component = [x for x in existing_components if x["title"] == reg["title"]]
125
+ if not check_component:
126
+ response = api.post(url=f'{config["domain"]}/api/components/', json=reg)
127
+ if not response.raise_for_status():
128
+ component_id = response.json()["id"]
129
+ logger.info("Successfully posted %s to RegScale.", reg["title"])
130
+ else:
131
+ for cmp in check_component:
132
+ # update the id for the reg object
133
+ reg["id"] = cmp["id"]
134
+ response = api.put(url=f'{config["domain"]}/api/components/{cmp["id"]}', json=reg)
135
+ if not response.raise_for_status():
136
+ component_id = cmp["id"]
137
+ logger.info("Successfully updated component %s in RegScale.", cmp["title"])
138
+ # Load controls to RegScale and associate with new component
139
+ load_controls(
140
+ controls_to_be_added=controls_to_be_added,
141
+ component_id=component_id,
142
+ base_catalog=base_catalog,
143
+ )
144
+
145
+
146
+ @oscal.command(name="component")
147
+ @click.option(
148
+ "--file_name",
149
+ type=click.Path(exists=True, dir_okay=False),
150
+ help="Enter the path to the file to parse OSCAL Complonents.",
151
+ required=True,
152
+ )
153
+ def upload_component(file_name: str) -> None:
154
+ """Upload OSCAL Component to RegScale."""
155
+ # Load Controls and assign to component
156
+ process_component(file_name=file_name)
157
+
158
+
159
+ def load_controls(controls_to_be_added: list[dict], component_id: int, base_catalog: str) -> None:
160
+ """
161
+ Load control implementations to RegScale
162
+
163
+ :param list[dict] controls_to_be_added: Controls to load from RegScale
164
+ :param int component_id: Component ID from RegScale
165
+ :param str base_catalog: base catalogue to filter components
166
+ :rtype: None
167
+ """
168
+ app = Application()
169
+ config = app.config
170
+ all_implementations = [
171
+ imp.dict()
172
+ for imp in ControlImplementation.get_all_by_parent(parent_id=component_id, parent_module="components")
173
+ ]
174
+
175
+ if cat := [cat.dict() for cat in Catalog.get_list() if cat.title == base_catalog]:
176
+ cat_id = cat[0]["id"]
177
+ controls = [control.dict() for control in SecurityControl.get_list_by_catalog(catalog_id=cat_id)]
178
+ for imp_control in controls_to_be_added:
179
+ logger.debug("imp_control: %s", imp_control)
180
+ try:
181
+ if reg_control := [
182
+ control for control in controls if control["controlId"].lower() == imp_control["title"].lower()
183
+ ]:
184
+ control_implementation = ControlImplementation(
185
+ parentId=component_id,
186
+ parentModule="components",
187
+ controlOwnerId=config["userId"],
188
+ status="Fully Implemented",
189
+ controlID=reg_control[0]["id"],
190
+ implementation=imp_control["description"],
191
+ )
192
+ control = SecurityControl.lookup_control(app=app, control_id=control_implementation.controlID)
193
+ create_or_update_control(
194
+ control=control,
195
+ control_implementation=control_implementation,
196
+ all_implementations=all_implementations,
197
+ )
198
+ except requests.RequestException as rex:
199
+ logger.error(rex)
200
+ else:
201
+ logger.debug(f"Catalog '{base_catalog}' not found")
202
+
203
+ # Insert or update new control implementations
204
+ ControlImplementation.bulk_save()
205
+
206
+
207
+ def create_or_update_control(
208
+ control: SecurityControl, control_implementation: ControlImplementation, all_implementations: list[dict]
209
+ ) -> None:
210
+ """
211
+ Create or update control implementation
212
+
213
+ :param SecurityControl control: Control object
214
+ :param ControlImplementation control_implementation: Control implementation object
215
+ :param list[dict] all_implementations: List of all control implementations
216
+ :rtype: None
217
+ """
218
+ try:
219
+ if control.controlId not in [
220
+ imp["controlName"] for imp in all_implementations if imp["parentId"] == control_implementation.parentId
221
+ ]:
222
+ control_implementation.create(bulk=True)
223
+ else:
224
+ dat = [
225
+ imp
226
+ for imp in all_implementations
227
+ if imp["controlName"] == control.controlId and imp["parentId"] == control_implementation.parentId
228
+ ][0]
229
+ dat["implementation"] = control_implementation.implementation
230
+ dat["status"] = control_implementation.status
231
+ ControlImplementation(**dat).save(bulk=True)
232
+ except IndexError as iex:
233
+ logger.error("Index Error: %s\n%s", dat, iex)
234
+
235
+
236
+ def process_component(file_name: str) -> None:
237
+ """
238
+ OSCAL Component to RegScale
239
+
240
+ :param str file_name: File Name
241
+ :rtype: None
242
+ """
243
+ output_name = tempfile.gettempdir() + sep + "component.json"
244
+ logger.debug(file_name)
245
+ file_convert_json(file_name, output_name)
246
+ try:
247
+ json_d = open(output_name, "r").read()
248
+ except FileNotFoundError:
249
+ error_and_exit(f"File not found!\n{file_name}")
250
+ convert_oscal_comp_to_regscale(j_data=json.loads(json_d))
251
+ remove(output_name)
252
+
253
+
254
+ def file_convert_json(input: str, output: str) -> None:
255
+ """
256
+ Convert file from YML/XML to JSON
257
+
258
+ :param str input: Path to the original file to convert
259
+ :param str output: Desired path of the converted file
260
+ :rtype: None
261
+ """
262
+ # Create object
263
+ with open(input, "r") as file_in, open(output, "w") as file_out:
264
+ if Path(input).suffix == ".json":
265
+ obj = json.load(file_in)
266
+ if Path(input).suffix == ".xml":
267
+ obj = xmltodict.parse((file_in.read()))
268
+ if Path(input).suffix in [".yaml", ".yml"]:
269
+ obj = yaml.safe_load(file_in.read())
270
+ json.dump(obj, file_out)
271
+
272
+
273
+ # OSCAL Profile Loader Support
274
+ @oscal.command()
275
+ @click.option(
276
+ "--title",
277
+ type=click.STRING,
278
+ help="RegScale will name the profile with the title provided.",
279
+ prompt="Enter the title for the OSCAL profile",
280
+ required=True,
281
+ )
282
+ @click.option(
283
+ "--categorization",
284
+ type=click.Choice(["Low", "Moderate", "High"], case_sensitive=False),
285
+ help="Choose from Low, Moderate, or High.",
286
+ prompt="Enter the FIPS categorization level",
287
+ required=True,
288
+ )
289
+ @click.option(
290
+ "--catalog",
291
+ type=click.INT,
292
+ help="Primary key (unique ID) of the RegScale catalogue.",
293
+ prompt="Enter the RegScale Catalogue ID to use",
294
+ required=True,
295
+ )
296
+ @click.option(
297
+ "--file_name",
298
+ type=click.Path(exists=True, dir_okay=False, file_okay=True),
299
+ help="RegScale will process and load the profile along with all specified controls.",
300
+ prompt="Enter the file name of the OSCAL profile to process",
301
+ required=True,
302
+ )
303
+ def profile(title: str, categorization: str, catalog: int, file_name: Path):
304
+ """OSCAL Profile Loader."""
305
+ upload_profile(title=title, categorization=categorization, catalog=catalog, file_name=file_name)
306
+
307
+
308
+ # flake8: noqa: C901
309
+ def upload_profile(title: str, categorization: str, catalog: int, file_name: Path) -> None:
310
+ """
311
+ OSCAL Profile Uploader
312
+
313
+ :param str title: Title
314
+ :param str categorization: Category information
315
+ :param int catalog: Catalogue Title
316
+ :param Path file_name: Desired file name
317
+ :rtype: None
318
+ """
319
+ app = Application()
320
+ config = app.config
321
+ api = Api()
322
+ # validation
323
+ if catalog <= 0:
324
+ error_and_exit("No catalogue provided or catalogue invalid.")
325
+ elif categorization.title() not in ["Low", "Moderate", "High"]:
326
+ error_and_exit("Categorization not provided or invalid.")
327
+
328
+ # load the catalog
329
+ try:
330
+ oscal = open(file_name, "r", encoding="utf-8-sig")
331
+ oscal_data = json.load(oscal)
332
+ except Exception as ex:
333
+ logger.debug(file_name)
334
+ error_and_exit(f"Unable to open the specified OSCAL file for processing.\n{ex}")
335
+ # load the config from YAML
336
+ try:
337
+ config = app.load_config()
338
+ except FileNotFoundError:
339
+ error_and_exit("Unable to open the init.yaml file.")
340
+
341
+ global schema
342
+
343
+ # set headers
344
+ str_user = config["userId"]
345
+ headers = {"Accept": "application/json", "Authorization": config["token"]}
346
+
347
+ # create a new profile
348
+ profile = {
349
+ "id": 0,
350
+ "uuid": "",
351
+ "name": title,
352
+ "confidentiality": "",
353
+ "integrity": "",
354
+ "availability": "",
355
+ "category": categorization,
356
+ "profileOwnerId": str_user,
357
+ "createdById": str_user,
358
+ "dateCreated": None,
359
+ "lastUpdatedById": str_user,
360
+ "dateLastUpdated": None,
361
+ "isPublic": True,
362
+ }
363
+
364
+ # create the profile
365
+ url_prof = f'{config["domain"]}/api/profiles/'
366
+ logger.info("RegScale creating a new profile...")
367
+ try:
368
+ prof_response = api.post(url=url_prof, headers=headers, json=profile)
369
+ prof_json_response = prof_response.json()
370
+ logger.info("\nProfile ID: " + str(prof_json_response["id"]))
371
+ # get the profile ID
372
+ int_profile = prof_json_response["id"]
373
+ except requests.exceptions.RequestException as ex:
374
+ error_and_exit(f"Unable to create profile in RegScale.\n{ex}")
375
+
376
+ # get the list of existing controls for the catalog
377
+ url_sc = f'{config["domain"]}/api/SecurityControls/getList/{catalog}'
378
+ try:
379
+ sc_response = api.get(url_sc, headers=headers)
380
+ sc_data = sc_response.json()
381
+ except requests.exceptions.RequestException as ex:
382
+ error_and_exit(
383
+ f"Unable to retrieve security controls for this catalogue in RegScale.\nError: \
384
+ {ex}\n{sc_response.text}"
385
+ )
386
+
387
+ # loop through each item in the OSCAL control set
388
+ mappings = []
389
+ for m in oscal_data["profile"]["imports"][0]["include-controls"][0]["with-ids"]:
390
+ b_match = False
391
+ for sc in sc_data:
392
+ if m == sc["controlId"]:
393
+ b_match = True
394
+ mapping = {
395
+ "id": 0,
396
+ "profileID": int_profile,
397
+ "controlID": int(sc["id"]),
398
+ }
399
+ mappings.append(mapping)
400
+ break
401
+ if b_match is False:
402
+ logger.error("Unable to locate control: %s.", m)
403
+
404
+ # upload the controls to the profile as mappings
405
+ url_maps = config["domain"] + "/api/profileMapping/batchCreate"
406
+ try:
407
+ api.post(url_maps, headers=headers, json=mappings)
408
+ logger.info(
409
+ "%s total mappings created in RegScale for this profile.",
410
+ str(len(mappings)),
411
+ )
412
+ except requests.exceptions.RequestException as ex:
413
+ error_and_exit(f"Unable to create mappings for this profile in RegScale.\n{ex}")
414
+
415
+
416
+ # Process catalog from OSCAL
417
+ @oscal.command()
418
+ @click.option(
419
+ "--file_name",
420
+ type=click.Path(exists=True, dir_okay=False, file_okay=True),
421
+ help="RegScale will process and load the catalogue along with all controls, statements, and \
422
+ parameters.",
423
+ prompt="Enter the file name of catalogue in NIST OSCAL to process",
424
+ required=True,
425
+ )
426
+ @click.option(
427
+ "--obj_to_controls",
428
+ type=click.BOOL,
429
+ default=0,
430
+ show_default=True,
431
+ help="Convert objectives to RegScale controls.",
432
+ )
433
+ @click.option(
434
+ "--fedramp",
435
+ type=click.BOOL,
436
+ default=0,
437
+ show_default=True,
438
+ required=False,
439
+ help="Specific processing for using the FedRAMP namespace to handle response points and assessment objectives differently",
440
+ )
441
+ @click.option(
442
+ "--new_catalog_name",
443
+ type=click.STRING,
444
+ help="RegScale will give the catalogue this new name.",
445
+ required=False,
446
+ default=None,
447
+ )
448
+ def catalog(
449
+ file_name: Path,
450
+ obj_to_controls: click.BOOL,
451
+ fedramp: click.BOOL,
452
+ new_catalog_name: str,
453
+ ):
454
+ """Process and load catalog to RegScale."""
455
+ upload_catalog(
456
+ file_name=file_name,
457
+ obj_to_controls=obj_to_controls,
458
+ fedramp=fedramp,
459
+ new_catalog_name=new_catalog_name,
460
+ )
461
+
462
+
463
+ # flake8: noqa: C901
464
+ def upload_catalog(
465
+ file_name: Path,
466
+ obj_to_controls: click.BOOL = 0,
467
+ fedramp: click.BOOL = 0,
468
+ new_catalog_name: str = None,
469
+ ) -> None:
470
+ """
471
+ Process and load catalogue to RegScale
472
+
473
+ :param Path file_name: Path to the catalogue to upload
474
+ :param click.BOOL obj_to_controls: Flag to indicate converting objectives to controls
475
+ :param click.BOOL fedramp: Flag to indicate it is a FedRAMP catalog so it processes differently
476
+ :param str new_catalog_name: New name to give the catalogue when uploaded, defaults to None
477
+ :rtype: None
478
+ """
479
+ import pandas as pd # Optimize import performance
480
+
481
+ app = Application()
482
+ config = app.config
483
+ api = Api()
484
+ # Create directory if not exists
485
+ check_file_path("processing")
486
+
487
+ # load the catalog
488
+ try:
489
+ with open(file_name, "r", encoding="utf-8-sig") as input:
490
+ oscal_data = json.load(input)
491
+ except requests.exceptions.RequestException as ex:
492
+ error_and_exit(f"Unable to open the specified OSCAL file for processing.\n{ex}")
493
+ # load the config from YAML
494
+ try:
495
+ config = app.load_config()
496
+ except Exception:
497
+ error_and_exit("Unable to open the init file.")
498
+ # debug flag to pause upload when testing and debugging (always true for production CLI use)
499
+ upload_flag = params_flag = tests_flag = objects_flag = deep_links_flag = True
500
+
501
+ # set headers
502
+ str_user = config["userId"]
503
+
504
+ # parse the OSCAL JSON to get related data (used to enrich base spreadsheet)
505
+ catalog_arr = (
506
+ oscal_data["catalog"]
507
+ if "catalog" in oscal_data
508
+ else error_and_exit("catalogue key not found in dataset, exiting..")
509
+ )
510
+ str_uuid = catalog_arr["uuid"]
511
+ metadata = catalog_arr["metadata"]
512
+ global schema
513
+ schema = oscal_version(metadata)
514
+ resource_guid = ""
515
+ resource_title = ""
516
+ citation = ""
517
+ links = ""
518
+
519
+ # process resources for lookup
520
+ resources = []
521
+ if "back-matter" in catalog_arr:
522
+ back_matter_arr = catalog_arr["back-matter"]
523
+ for i in back_matter_arr["resources"]:
524
+ # make sure values exist
525
+ if "title" in i:
526
+ resource_title = i["title"]
527
+ if "uuid" in i:
528
+ resource_guid = i["uuid"]
529
+ if "citation" in i:
530
+ citation = i["citation"]
531
+ if "text" in citation:
532
+ citation = citation["text"]
533
+ links = ""
534
+ if "rlinks" in i:
535
+ links = i["rlinks"]
536
+ if len(links) > 1:
537
+ for x in links:
538
+ if "href" in x:
539
+ links += x["href"] + "<br/>"
540
+ elif isinstance(links[0], dict) and len(links) == 1:
541
+ links = links[0].get("href")
542
+ # add parsed/flattened resource to the array
543
+ res = {
544
+ "uuid": resource_guid,
545
+ "short": resource_title,
546
+ "title": citation,
547
+ "links": links,
548
+ }
549
+ resources.append(res)
550
+
551
+ # Write to file to visualize the output
552
+ save_data_to(
553
+ file=Path("./processing/resources.json"),
554
+ data=resources,
555
+ output_log=False,
556
+ )
557
+ # convert data to pandas dataframe
558
+ raw_data = pd.DataFrame(resources)
559
+
560
+ # copy the columns of data that we want while renaming them to a specific case
561
+ resource_data = (
562
+ raw_data[["uuid", "title", "links"]].copy().rename(columns={"uuid": "UUID", "title": "Title", "links": "Links"})
563
+ )
564
+
565
+ # convert the data table to an HTML formatted table
566
+ str_resources = resource_data.to_html(index=False, justify="left")
567
+
568
+ # determine the date to use
569
+ date_format = "%Y-%m-%d %H:%M:%S.%f %z"
570
+ if "fedramp" in catalog_arr["metadata"]["version"].lower():
571
+ # format the dates into strings for RegScale
572
+ date_published = reformat_str_date(catalog_arr["metadata"]["published"], dt_format=date_format)
573
+ last_modified = reformat_str_date(
574
+ catalog_arr["metadata"]["last-modified"],
575
+ dt_format=date_format,
576
+ )
577
+ else:
578
+ # published date is required in RegScale but not found in NIST OSCAL catalog. Defaulting to last-modified date which is required in NIST OSCAL catalog
579
+ date_published = reformat_str_date(
580
+ catalog_arr["metadata"]["last-modified"],
581
+ dt_format=date_format,
582
+ )
583
+ last_modified = reformat_str_date(
584
+ catalog_arr["metadata"]["last-modified"],
585
+ dt_format=date_format,
586
+ )
587
+
588
+ # setup catalog data
589
+ cat = Catalog(
590
+ **{
591
+ "title": (
592
+ catalog_arr["metadata"]["title"]
593
+ if (new_catalog_name is None or new_catalog_name == "")
594
+ else new_catalog_name
595
+ ),
596
+ "description": "This publication provides a catalog of security and privacy controls for information systems and organizations to protect organizational operations and assets, individuals, other organizations, and the Nation from a diverse set of threats and risks, including hostile attacks, human errors, natural disasters, structural failures, foreign intelligence entities, and privacy risks. <br/><br/><strong>Resources</strong><br/><br/>"
597
+ + str_resources,
598
+ "datePublished": date_published,
599
+ "uuid": str_uuid,
600
+ "lastRevisionDate": last_modified,
601
+ "url": "https://csrc.nist.gov/",
602
+ "abstract": "This publication provides a catalog of security and privacy controls for federal information systems and organizations and a process for selecting controls to protect organizational operations (including mission, functions, image, and reputation), organizational assets, individuals, other organizations, and the Nation from a diverse set of threats including hostile cyber attacks, natural disasters, structural failures, and human errors (both intentional and unintentional). The security and privacy controls are customizable and implemented as part of an organization-wide process that manages information security and privacy risk. The controls address a diverse set of security and privacy requirements across the federal government and critical infrastructure, derived from legislation, Executive Orders, policies, directives, regulations, standards, and/or mission/business needs. The publication also describes how to develop specialized sets of controls, or overlays, tailored for specific types of missions/business functions, technologies, or environments of operation. Finally, the catalog of security controls addresses security from both a functionality perspective (the strength of security functions and mechanisms provided) and an assurance perspective (the measures of confidence in the implemented security capability). Addressing both security functionality and assurance helps to ensure that information technology component products and the information systems built from those products using sound system and security engineering principles are sufficiently trustworthy.",
603
+ "keywords": "FIPS Publication 200; FISMA; Privacy Act; Risk Management Framework; security controls; FIPS Publication 199; security requirements; computer security; assurance;",
604
+ "createdById": str_user,
605
+ "lastUpdatedById": str_user,
606
+ }
607
+ )
608
+
609
+ # create the catalog and print success result
610
+ if upload_flag is True:
611
+ logger.debug("Creating new catalogue in RegScale.")
612
+ # update the timeout
613
+ if api.timeout < 120:
614
+ api.timeout = 120
615
+ new_cat = cat.create()
616
+ catalogue_id = new_cat.id
617
+ logger.info("Created Catalogue ID: %s in RegScale", catalogue_id)
618
+ # get the catalog ID
619
+ else:
620
+ # don't set ID in debug mode
621
+ catalogue_id = 0
622
+
623
+ # process NIST families of controls
624
+ families = []
625
+ oscal_controls = []
626
+ parameters = []
627
+ parts = []
628
+ assessments = []
629
+ objectives = []
630
+
631
+ # process groups of controls
632
+ groups = catalog_arr["groups"]
633
+
634
+ for i in groups:
635
+ str_family = i["title"]
636
+ f = {
637
+ "id": (i["id"] if package_version.parse(schema) <= package_version.parse("1.0.2") else None),
638
+ "title": i["title"],
639
+ }
640
+ # add parsed item to the family array
641
+ families.append(f)
642
+
643
+ controls = (
644
+ list(find_keys(i, "controls"))
645
+ if package_version.parse(schema) > package_version.parse("1.0.2")
646
+ else i["controls"]
647
+ )
648
+
649
+ # loop through controls
650
+ for ctrl in controls:
651
+ # process the control
652
+ if isinstance(ctrl, dict):
653
+ oscal_controls = append_controls(
654
+ oscal_controls,
655
+ ctrl,
656
+ resources,
657
+ str_family,
658
+ parameters,
659
+ parts,
660
+ assessments,
661
+ objectives,
662
+ fedramp,
663
+ )
664
+
665
+ if isinstance(ctrl, list):
666
+ for cnt in ctrl:
667
+ oscal_controls = append_controls(
668
+ oscal_controls,
669
+ cnt,
670
+ resources,
671
+ str_family,
672
+ parameters,
673
+ parts,
674
+ assessments,
675
+ objectives,
676
+ fedramp,
677
+ )
678
+
679
+ # check for child controls/enhancements
680
+ if "controls" in ctrl:
681
+ child_ctrls = ctrl["controls"]
682
+ for child_ctrl in child_ctrls:
683
+ oscal_controls = append_controls(
684
+ oscal_controls,
685
+ child_ctrl,
686
+ resources,
687
+ str_family,
688
+ parameters,
689
+ parts,
690
+ assessments,
691
+ objectives,
692
+ fedramp,
693
+ )
694
+
695
+ # more unique processing for FedRAMP
696
+ if fedramp:
697
+ # arrays to hold processed fields
698
+ assessments = []
699
+ processed_objectives = []
700
+ # loop over each objective
701
+ for obj in objectives:
702
+ # replace response point language
703
+ if obj["description"].find("You must fill in this response point. ") > 0:
704
+ obj["description"] = obj["description"].replace("You must fill in this response point. ", "")
705
+ obj["description"] += " (REQUIRED)"
706
+ # put in the right bucket
707
+ if obj["objectiveType"] == "objective":
708
+ processed_objectives.append(obj)
709
+ else:
710
+ new_test = {
711
+ "id": 0,
712
+ "name": obj["name"],
713
+ "testType": "TEST",
714
+ "description": obj["description"],
715
+ "parentControl": obj["parentControl"],
716
+ }
717
+ assessments.append(new_test)
718
+ objectives = processed_objectives
719
+
720
+ # Write to file to visualize the output
721
+ save_data_to(
722
+ file=Path("./processing/families.json"),
723
+ data=families,
724
+ output_log=False,
725
+ )
726
+
727
+ # Write to file to visualize the output
728
+ save_data_to(
729
+ file=Path("./processing/controls.json"),
730
+ data=oscal_controls,
731
+ output_log=False,
732
+ )
733
+
734
+ # Write to file to visualize the output
735
+ save_data_to(
736
+ file=Path("./processing/parameters.json"),
737
+ data=parameters,
738
+ output_log=False,
739
+ )
740
+
741
+ # Write to file to visualize the output
742
+ save_data_to(
743
+ file=Path("./processing/parts.json"),
744
+ data=parts,
745
+ output_log=False,
746
+ )
747
+
748
+ # Write to file to visualize the output
749
+ save_data_to(
750
+ file=Path("./processing/tests.json"),
751
+ data=assessments,
752
+ output_log=False,
753
+ )
754
+
755
+ # Write to file to visualize the output
756
+ save_data_to(
757
+ file=Path("./processing/objectives.json"),
758
+ data=objectives,
759
+ output_log=False,
760
+ )
761
+
762
+ # output the items processed from the provided OSCAL catalog
763
+ logger.info(
764
+ "%s familys, %s controls, %s parameters, %s objectives %s parts & %s assessments processed from %s.",
765
+ len(families),
766
+ len(oscal_controls),
767
+ len(parameters),
768
+ len(objectives),
769
+ len(parts),
770
+ len(assessments),
771
+ get_file_name(file_name),
772
+ )
773
+
774
+ # use the progress object for the threaded process
775
+ with job_progress:
776
+ global new_regscale_controls
777
+ if oscal_controls:
778
+ # log the information
779
+ logger.debug("Posting %s OSCAL controls to RegScale.", len(oscal_controls))
780
+ # create task for job progress object
781
+ creating_controls = job_progress.add_task(
782
+ f"[#f8b737]Creating {len(oscal_controls)} controls in RegScale...",
783
+ total=len(oscal_controls),
784
+ )
785
+ # create threads to create controls
786
+ create_threads(
787
+ process=post_controls,
788
+ args=(
789
+ oscal_controls,
790
+ catalogue_id,
791
+ upload_flag,
792
+ creating_controls,
793
+ api,
794
+ ),
795
+ thread_count=len(oscal_controls),
796
+ )
797
+ # log the outcome
798
+ logger.info(
799
+ "%s/%s OSCAL controls created in RegScale.",
800
+ len(new_regscale_controls),
801
+ len(oscal_controls),
802
+ )
803
+ # Write to file to visualize the output
804
+ save_data_to(
805
+ file=Path("./processing/mappedControls.json"),
806
+ data=new_controls,
807
+ output_log=False,
808
+ )
809
+ # Write to file to visualize the output
810
+ if upload_flag:
811
+ save_data_to(
812
+ file=Path("./processing/newControls.json"),
813
+ data=new_regscale_controls,
814
+ output_log=False,
815
+ )
816
+ else:
817
+ with open(f"processing{sep}newControls.json", "r", encoding="utf-8-sig") as infile:
818
+ new_regscale_controls = json.load(infile)
819
+ # only process if the controls exists to map to
820
+ if len(new_regscale_controls) > 0:
821
+ if parameters:
822
+ # log the information
823
+ logger.debug("Posting %s parameters to RegScale.", len(parameters))
824
+ # create task for analyzing child controls
825
+ posting_child_controls = job_progress.add_task(
826
+ f"[#ef5d23]Creating {len(parameters)} parameters in RegScale...",
827
+ total=len(parameters),
828
+ )
829
+ # create threads to post child controls
830
+ create_threads(
831
+ process=post_child_controls,
832
+ args=(
833
+ parameters,
834
+ new_regscale_controls,
835
+ params_flag,
836
+ posting_child_controls,
837
+ api,
838
+ ),
839
+ thread_count=len(parameters),
840
+ )
841
+ # log the outcome
842
+ logger.info(
843
+ "%s/%s OSCAL parameters created in RegScale.",
844
+ len(new_params),
845
+ len(parameters),
846
+ )
847
+
848
+ # output the result
849
+ save_data_to(
850
+ file=Path("processing/newParameters.json"),
851
+ data=new_params,
852
+ output_log=False,
853
+ )
854
+ if assessments:
855
+ # log the information
856
+ logger.debug("Posting %s assessments to RegScale.", len(assessments))
857
+ # create task for creating assessments
858
+ assigning_tests = job_progress.add_task(
859
+ f"[#21a5bb]Creating {len(assessments)} assessments in RegScale...",
860
+ total=len(assessments),
861
+ )
862
+ # create threads to create tests
863
+ create_threads(
864
+ process=assign_control_tests,
865
+ args=(
866
+ assessments,
867
+ new_regscale_controls,
868
+ tests_flag,
869
+ assigning_tests,
870
+ api,
871
+ ),
872
+ thread_count=len(assessments),
873
+ )
874
+ # output the result
875
+ save_data_to(
876
+ file=Path("./processing/newTests.json"),
877
+ data=new_tests,
878
+ output_log=False,
879
+ )
880
+ # log the outcome
881
+ logger.info(
882
+ "%s/%s assessments created in RegScale.",
883
+ len(new_tests),
884
+ len(assessments),
885
+ )
886
+ # process objectives based on FedRAMP flag
887
+ if not fedramp:
888
+ obj_parts = [
889
+ part
890
+ for part in parts
891
+ if part["objectiveType"] in ["objective", "assessment-objective"]
892
+ and part["description"] not in ["Determine if the organization:", "Determine if:"]
893
+ ]
894
+ if obj_parts:
895
+ # log the information
896
+ logger.debug("Analyzing %s objectives for posting to RegScale.", len(parts))
897
+ # create task for creating objectives
898
+ creating_objectives = job_progress.add_task(
899
+ f"[#0866b4]Analyzing {len(parts)} OSCAL objectives for creation in RegScale...",
900
+ total=len(parts),
901
+ )
902
+ # create threads to loop through controls
903
+ create_threads(
904
+ process=create_objectives,
905
+ args=(
906
+ obj_parts,
907
+ new_regscale_controls,
908
+ objects_flag,
909
+ creating_objectives,
910
+ api,
911
+ ),
912
+ thread_count=len(obj_parts),
913
+ )
914
+ # complete the task for creating objectives
915
+ job_progress.advance(creating_objectives, len(parts))
916
+
917
+ # log the outcome
918
+ logger.info("%s objectives created in RegScale.", len(new_objectives))
919
+ # output the result
920
+ save_data_to(
921
+ file=Path("./processing/newObjectives.json"),
922
+ data=new_objectives,
923
+ output_log=False,
924
+ )
925
+ else:
926
+ # log the information
927
+ logger.debug("Analyzing %s objectives for posting to RegScale.", len(objectives))
928
+ # create task for creating objectives
929
+ creating_objectives = job_progress.add_task(
930
+ f"[#0866b4]Creating {len(objectives)} objectives in RegScale...",
931
+ total=len(objectives),
932
+ )
933
+ # create threads to loop through controls
934
+ create_threads(
935
+ process=create_objectives,
936
+ args=(
937
+ objectives,
938
+ new_regscale_controls,
939
+ objects_flag,
940
+ creating_objectives,
941
+ api,
942
+ ),
943
+ thread_count=len(objectives),
944
+ )
945
+ # complete the task for creating objectives
946
+ job_progress.advance(creating_objectives, len(objectives))
947
+ if deep_links_flag:
948
+ # process deep links
949
+ try:
950
+ logger.info(
951
+ "Retrieving all objectives for this catalogue # %i from RegScale (this might take a minute)...",
952
+ catalogue_id,
953
+ )
954
+ # extend the api timeout for this api call
955
+ api.timeout = 60
956
+ url_deep = f'{config["domain"]}/api/controlObjectives/getByCatalogue/{catalogue_id}'
957
+ obj_list_response = api.get(url_deep, headers={"Authorization": config["token"]})
958
+ obj_list = obj_list_response.json()
959
+ logger.info(
960
+ "%i total objectives now retrieved from RegScale for processing.",
961
+ len(obj_list),
962
+ )
963
+ except Exception:
964
+ error_and_exit("Unable to retrieve control objective information from RegScale.")
965
+ # log the information
966
+ logger.debug(
967
+ "Analyzing %s objectives for potential updating in RegScale.",
968
+ len(assessments),
969
+ )
970
+ # create task for creating objectives
971
+ updating_objectives = job_progress.add_task(
972
+ f"[#05d1b7]Analyzing {len(obj_list)} objectives in RegScale...",
973
+ total=len(obj_list),
974
+ )
975
+ # create threads to loop through controls
976
+ create_threads(
977
+ process=update_objectives,
978
+ args=(obj_list, parts, updating_objectives, api),
979
+ thread_count=len(obj_list),
980
+ )
981
+ # log the outcome
982
+ logger.info(
983
+ "%s objectives analyzed & %s objectives updated in RegScale.",
984
+ len(obj_list),
985
+ len(updates),
986
+ )
987
+ if errors:
988
+ # output the errors
989
+ save_data_to(
990
+ file=Path("./processing/errors.json"),
991
+ data=new_objectives,
992
+ )
993
+ if obj_to_controls:
994
+ # create task for creating objectives
995
+ inserting_controls_from_objectives = job_progress.add_task(
996
+ f"[#c42843]Creating {len(new_objectives)} control(s) from objectives..."
997
+ )
998
+ # Post Objectives as Controls
999
+ create_threads(
1000
+ process=post_alternative_controls,
1001
+ args=(
1002
+ new_objectives,
1003
+ parts,
1004
+ inserting_controls_from_objectives,
1005
+ api,
1006
+ ),
1007
+ thread_count=len(new_objectives),
1008
+ )
1009
+
1010
+
1011
+ def post_controls(args: Tuple, thread: int) -> None:
1012
+ """
1013
+ Function to analyze controls from OSCAL catalog and post them to RegScale while using threads
1014
+
1015
+ :param Tuple args: Tuple of args to use during the process
1016
+ :param int thread: Thread number of current thread
1017
+ :rtype: None
1018
+ """
1019
+ # set up local variables from args passed
1020
+ controls, catalog_id, upload_flag, task, api = args
1021
+
1022
+ # set up RegScale URL
1023
+ url_sc = api.config["domain"] + SC_URL
1024
+
1025
+ # find which records should be executed by the current thread
1026
+ threads = thread_assignment(thread=thread, total_items=len(controls))
1027
+
1028
+ # iterate through the thread assignment items and process them
1029
+ for i in range(len(threads)):
1030
+ # set the recommendation for the thread for later use in the function
1031
+ control = controls[threads[i]]
1032
+ str_parts = strip_tag(
1033
+ control["parts"], "_obj"
1034
+ ) # Strip objectives from the tag, we still need to keep them in the original parts list.
1035
+ # create each security control
1036
+
1037
+ description = str_parts.replace(
1038
+ "<ul><li>{{" + control["id"] + "_smt}} - </li><ul>",
1039
+ "<ul><li>{{" + control["id"] + "_smt}} - Control:</li><ul>",
1040
+ )
1041
+
1042
+ security_control = {
1043
+ "title": control["id"] + " - " + control["title"],
1044
+ "controlType": "Stand-Alone",
1045
+ "controlId": control["id"],
1046
+ "sortId": control["sortId"],
1047
+ "description": description + control["guidance"],
1048
+ "references": control["links"],
1049
+ "relatedControls": "",
1050
+ "subControls": "",
1051
+ "enhancements": control["enhancements"],
1052
+ "family": control["family"],
1053
+ "mappings": control["parameters"],
1054
+ "assessmentPlan": control["assessment"],
1055
+ "weight": 0,
1056
+ "practiceLevel": "",
1057
+ "catalogueID": catalog_id,
1058
+ "createdById": api.config["userId"],
1059
+ "lastUpdatedById": api.config["userId"],
1060
+ }
1061
+ # attempt to create the security control
1062
+ if upload_flag:
1063
+ try:
1064
+ # upload to RegScale
1065
+
1066
+ response = api.post(url=url_sc, json=security_control)
1067
+ json_response = response.json()
1068
+ logger.debug("\n\nSuccess - %s", security_control["title"])
1069
+
1070
+ # update id to the new control id
1071
+ security_control["id"] = json_response["id"]
1072
+ new_controls.append(security_control)
1073
+
1074
+ # add the new controls
1075
+ new_regscale_controls.append(json_response)
1076
+ except requests.exceptions.RequestException:
1077
+ logger.error("Unable to create security control %s,", security_control["title"])
1078
+ errors.append(security_control)
1079
+ else:
1080
+ # append the result
1081
+ new_regscale_controls.append(security_control)
1082
+ job_progress.update(task, advance=1)
1083
+
1084
+
1085
+ def extract_text(html_string: str, elem: str) -> dict:
1086
+ """
1087
+ Function to return an object with a representation of a string formatted and cleaned
1088
+
1089
+ :param str html_string: A string of HTML
1090
+ :param str elem: An HTML tag type (ex. li)
1091
+ :return: A dictionary object with a representation of a string formatted and cleaned
1092
+ :rtype: dict
1093
+ """
1094
+ result = {
1095
+ "key": "",
1096
+ "original_text": html_string,
1097
+ "clean_text": "",
1098
+ "extract_text": "",
1099
+ }
1100
+ pattern = r"{([^{{]*?)}"
1101
+ soup = BeautifulSoup(html_string, "html.parser")
1102
+ for elem in soup.find_all(elem):
1103
+ txt = elem.text.strip()
1104
+ result["clean_text"] = txt
1105
+ match = re.search(pattern, txt)
1106
+ if match:
1107
+ key = match.group(1)
1108
+ result["key"] = key
1109
+ dat_ix = txt.find(" -")
1110
+ result["extract_text"] = txt[dat_ix:]
1111
+ return result
1112
+
1113
+
1114
+ def strip_tag(html_string: str, sub_str_to_find: str) -> str:
1115
+ """
1116
+ Function to strip an HTML tag from a string based on a substring
1117
+
1118
+ :param str html_string: A string of HTML
1119
+ :param str sub_str_to_find: A substring to search for
1120
+ :return: String with HTML tag removed
1121
+ :rtype: str
1122
+ """
1123
+ result = html_string
1124
+ try:
1125
+ soup = BeautifulSoup(html_string, "html.parser")
1126
+ for tag in soup.find_all(lambda tag: tag.name == "li" and sub_str_to_find in tag.text):
1127
+ tag.extract()
1128
+ # Strip empty HTML tags
1129
+ for tag in soup.find_all(lambda tag: not tag.contents or tag.contents == [""]):
1130
+ tag.extract()
1131
+ for datx in soup.find_all():
1132
+ if len(datx.get_text(strip=True)) == 0:
1133
+ # Remove empty tags
1134
+ datx.extract()
1135
+ result = str(soup)
1136
+ except ValueError as vex:
1137
+ logger.error(vex)
1138
+ return result
1139
+
1140
+
1141
+ def post_alternative_controls(args: Tuple, thread: int) -> None:
1142
+ """
1143
+ Function to post controls from objectives
1144
+
1145
+ :param Tuple args: Tuple of args to use during the process
1146
+ :param int thread: Thread number of current thread
1147
+ :rtype: None
1148
+ """
1149
+ new_objs, parts, task, api = args
1150
+
1151
+ # find which records should be executed by the current thread
1152
+ threads = thread_assignment(thread=thread, total_items=len(new_objs))
1153
+
1154
+ # iterate through the thread assignment items and process them
1155
+ for i in range(len(threads)):
1156
+ obj = new_objs[threads[i]]
1157
+ # Please don't automate my job ChatGPT
1158
+ title = reformat_title(obj["name"])
1159
+ label = [part["part_label"] for part in parts if part["name"] == obj["name"]][0]
1160
+ # Create a Child Control for this special case
1161
+ security_control_id = obj["securityControlId"]
1162
+ ctrl_lookup = api.get(url=api.config["domain"] + f"{SC_URL}{security_control_id}").json()
1163
+ security_control = {
1164
+ "title": (
1165
+ f"objective: {label} - {obj['description']}" if title else "objective"
1166
+ ), # f"{ctrl_lookup['controlId']} objective: {title}",
1167
+ "controlType": "Mapping", # Can also be Stand-Alone
1168
+ "controlId": ctrl_lookup["controlId"],
1169
+ "description": obj["description"],
1170
+ "references": ctrl_lookup["references"],
1171
+ "relatedControls": ctrl_lookup["id"],
1172
+ "subControls": "",
1173
+ "enhancements": "",
1174
+ "family": ctrl_lookup["family"],
1175
+ "mappings": "",
1176
+ "assessmentPlan": ctrl_lookup["assessmentPlan"],
1177
+ "weight": 0,
1178
+ "practiceLevel": "",
1179
+ "catalogueID": ctrl_lookup["catalogueID"],
1180
+ "createdById": api.config["userId"],
1181
+ "lastUpdatedById": api.config["userId"],
1182
+ }
1183
+ api.post(url=api.config["domain"] + SC_URL, json=security_control)
1184
+ job_progress.update(task, advance=1)
1185
+
1186
+
1187
+ def reformat_title(string: str) -> str:
1188
+ """
1189
+ Function to reformat the title of a string
1190
+
1191
+ :param str string: A string to reformat
1192
+ :return: Reformatted string
1193
+ :rtype: str
1194
+ """
1195
+ # strip everything before 'obj'
1196
+ new_string = None
1197
+ try:
1198
+ new_string = string.split("obj.")[1]
1199
+ except IndexError:
1200
+ new_string = string.split("obj")[1]
1201
+ new_string = new_string.replace("_", " ")
1202
+ new_string = (new_string.lstrip(".")).lstrip("-")
1203
+ return new_string
1204
+
1205
+
1206
+ def post_child_controls(args: Tuple, thread: int) -> None:
1207
+ """
1208
+ Function to analyze child controls from OSCAL catalog and posts them to RegScale while using \
1209
+ threads
1210
+
1211
+ :param Tuple args: Tuple of args to use during the process
1212
+ :param int thread: Thread number of current thread
1213
+ :rtype: None
1214
+ """
1215
+ # set up local variables from args passed
1216
+ parameters, regscale_controls, params_flag, task, api = args
1217
+
1218
+ # set up RegScale URL
1219
+ url_params = api.config["domain"] + "/api/controlParameters/"
1220
+
1221
+ # find which records should be executed by the current thread
1222
+ threads = thread_assignment(thread=thread, total_items=len(parameters))
1223
+
1224
+ # iterate through the thread assignment items and process them
1225
+ for i in range(len(threads)):
1226
+ # set the parameter for the thread for later use in the function
1227
+ parameter = parameters[threads[i]]
1228
+
1229
+ # find the parent control
1230
+ ctrl_lookup = next(
1231
+ (item for item in regscale_controls if (item["controlId"] == parameter["controlId"])),
1232
+ None,
1233
+ )
1234
+ if ctrl_lookup is None:
1235
+ logger.error(
1236
+ "Unable to locate %s for this parameter: %s.",
1237
+ parameter["controlId"],
1238
+ parameter["name"],
1239
+ )
1240
+ else:
1241
+ # create a new parameter to upload
1242
+ new_param = {
1243
+ "id": 0,
1244
+ "uuid": "",
1245
+ "text": parameter["value"],
1246
+ "dataType": "string",
1247
+ "parameterId": parameter["name"],
1248
+ "default": parameter["default"],
1249
+ "securityControlId": ctrl_lookup["id"],
1250
+ "archived": False,
1251
+ "createdById": api.config["userId"],
1252
+ "dateCreated": None,
1253
+ "lastUpdatedById": api.config["userId"],
1254
+ "dateLastUpdated": None,
1255
+ }
1256
+
1257
+ # attempt to create the parameter
1258
+ if params_flag:
1259
+ try:
1260
+ # upload to RegScale
1261
+ response = api.post(url_params, json=new_param)
1262
+ response_data = response.json()
1263
+ logger.debug(
1264
+ "\n\nSuccess - %s parameter uploaded successfully.",
1265
+ new_param["parameterId"],
1266
+ )
1267
+ # update the id to the new parameter id
1268
+ new_param["id"] = response_data["id"]
1269
+
1270
+ # add the control to the new array
1271
+ new_params.append(new_param)
1272
+ except requests.exceptions.RequestException:
1273
+ logger.error("Unable to create parameter: %s.", new_param["parameterId"])
1274
+ errors.append(new_param)
1275
+ else:
1276
+ # add the control to the new array
1277
+ new_params.append(new_param)
1278
+ job_progress.update(task, advance=1)
1279
+
1280
+
1281
+ def assign_control_tests(args: Tuple, thread: int) -> None:
1282
+ """
1283
+ Function to analyze match controls to assessments while using threads
1284
+
1285
+ :param Tuple args: Tuple of args to use during the process
1286
+ :param int thread: Thread number of current thread
1287
+ :rtype: None
1288
+ """
1289
+ # set up local variables from args passed
1290
+ assessments, regscale_controls, fedramp, tests_flag, task, api = args
1291
+
1292
+ # set up RegScale URL
1293
+ url_tests = api.config["domain"] + "/api/controlTestPlans/"
1294
+
1295
+ # find which records should be executed by the current thread
1296
+ threads = thread_assignment(thread=thread, total_items=len(assessments))
1297
+
1298
+ # iterate through the thread assignment items and process them
1299
+ for i in range(len(threads)):
1300
+ # set the recommendation for the thread for later use in the function
1301
+ assessment = assessments[threads[i]]
1302
+ # find the parent control
1303
+ ctrl_lookup = next(
1304
+ (item for item in regscale_controls if (item["controlId"] == assessment["parentControl"])),
1305
+ None,
1306
+ )
1307
+ if ctrl_lookup is None:
1308
+ logger.error("Unable to locate %s for this test.", assessment["parentControl"])
1309
+ else:
1310
+ # create a new test to upload
1311
+ new_test = {
1312
+ "id": 0,
1313
+ "uuid": "",
1314
+ "test": f'{assessment["testType"]} - {assessment["description"]}',
1315
+ "testId": assessment["name"] if fedramp else str(uuid.uuid4()),
1316
+ "securityControlId": ctrl_lookup["id"],
1317
+ "archived": False,
1318
+ "createdById": api.config["userId"],
1319
+ "dateCreated": None,
1320
+ "lastUpdatedById": api.config["userId"],
1321
+ "dateLastUpdated": None,
1322
+ }
1323
+ # attempt to create the test
1324
+ if tests_flag:
1325
+ try:
1326
+ # upload to RegScale
1327
+ response = api.post(url_tests, json=new_test)
1328
+ json_response = response.json()
1329
+ logger.debug(
1330
+ "\n\nSuccess - %s - test uploaded successfully.",
1331
+ new_test["test"],
1332
+ )
1333
+ # update the id to the new test id
1334
+ new_test["id"] = json_response["id"]
1335
+
1336
+ # add the test to the new array
1337
+ new_tests.append(new_test)
1338
+ except requests.exceptions.RequestException:
1339
+ logger.error("Unable to create test: %s.", new_test["test"])
1340
+ errors.append(new_test)
1341
+ else:
1342
+ # add the test to the new array
1343
+ new_tests.append(new_test)
1344
+ job_progress.update(task, advance=1)
1345
+
1346
+
1347
+ def create_objectives(args: Tuple, thread: int) -> None:
1348
+ """
1349
+ Function to create objectives from OSCAL catalog and post them to RegScale while using threads
1350
+
1351
+ :param Tuple args: Tuple of args to use during the process
1352
+ :param int thread: Thread number of current thread
1353
+ :rtype: None
1354
+ """
1355
+
1356
+ # set up local variables from args passed
1357
+ (
1358
+ objectives,
1359
+ regscale_controls,
1360
+ objects_flag,
1361
+ task,
1362
+ api,
1363
+ ) = args
1364
+
1365
+ # set up RegScale URL
1366
+ url_objs = api.config["domain"] + "/api/controlObjectives/"
1367
+
1368
+ # find which records should be executed by the current thread
1369
+
1370
+ obj_parts = list(objectives)
1371
+ threads = thread_assignment(thread=thread, total_items=len(obj_parts))
1372
+
1373
+ # iterate through the thread assignment items and process them
1374
+ for i in range(len(threads)):
1375
+ # set the recommendation for the thread for later use in the function
1376
+ objective: dict = obj_parts[threads[i]]
1377
+
1378
+ # find the parent control
1379
+ ctrl_lookup = next(
1380
+ (item for item in regscale_controls if (item["controlId"] == objective["parentControl"])),
1381
+ None,
1382
+ )
1383
+ if ctrl_lookup is None:
1384
+ logger.error(
1385
+ "Unable to locate %s for this objective/part: %s.",
1386
+ objective["parentControl"],
1387
+ objective["name"],
1388
+ )
1389
+ job_progress.update(task, advance=1)
1390
+ return
1391
+
1392
+ # create a new test to upload
1393
+ new_obj = {
1394
+ "id": 0,
1395
+ "uuid": "",
1396
+ "name": objective["name"],
1397
+ "description": objective["description"],
1398
+ "objectiveType": objective["objectiveType"],
1399
+ "otherId": "",
1400
+ "securityControlId": ctrl_lookup["id"],
1401
+ "parentObjectiveId": None,
1402
+ "archived": False,
1403
+ "createdById": api.config["userId"],
1404
+ "dateCreated": None,
1405
+ "lastUpdatedById": api.config["userId"],
1406
+ "dateLastUpdated": None,
1407
+ }
1408
+
1409
+ # attempt to create the objective
1410
+ if objects_flag:
1411
+ try:
1412
+ # upload to RegScale
1413
+ response = api.post(url_objs, json=new_obj)
1414
+ json_response = response.json()
1415
+ logger.debug(
1416
+ "\n\nSuccess - %s - objective uploaded successfully.",
1417
+ new_obj["name"],
1418
+ )
1419
+ # try to update the id to the new control id
1420
+ try:
1421
+ new_obj["id"] = json_response["id"]
1422
+ except KeyError:
1423
+ continue
1424
+
1425
+ # add the objective to the new array
1426
+ new_objectives.append(new_obj)
1427
+ except requests.exceptions.RequestException as rex:
1428
+ logger.error("Unable to create objective: %s.\n%s", new_obj["name"], rex)
1429
+ errors.append(new_obj)
1430
+ else:
1431
+ # add the part to the new array
1432
+ new_objectives.append(new_obj)
1433
+ job_progress.update(task, advance=1)
1434
+
1435
+
1436
+ def update_objectives(args: Tuple, thread: int) -> None:
1437
+ """
1438
+ Loop through each objective and see if it has a parent, if so,
1439
+ update parent ID and send update to RegScale while using threads
1440
+
1441
+ :param Tuple args: Tuple of args to use during the process
1442
+ :param int thread: Thread number of current thread
1443
+ :rtype: None
1444
+ """
1445
+ # set up local variables from args passed
1446
+ objectives, parts, task, api = args
1447
+
1448
+ # set up RegScale URL
1449
+ url_objs = api.config["domain"] + "/api/controlObjectives/"
1450
+
1451
+ # find which records should be executed by the current thread
1452
+ threads = thread_assignment(thread=thread, total_items=len(objectives))
1453
+
1454
+ # iterate through the thread assignment items and process them
1455
+ for i in range(len(threads)):
1456
+ # set the recommendation for the thread for later use in the function
1457
+ objective = objectives[threads[i]]
1458
+
1459
+ # find the part by name
1460
+ part_lookup = next((item for item in parts if (item["name"] == objective["name"])), None)
1461
+ if part_lookup is not None:
1462
+ # see if the part has a parent
1463
+ if part_lookup["parentObjective"] != "":
1464
+ # lookup the parent objective from RegScale
1465
+ parent_lookup = next(
1466
+ (item for item in objectives if (item["name"] == part_lookup["parentObjective"])),
1467
+ None,
1468
+ )
1469
+ if parent_lookup is not None:
1470
+ logger.debug("Found Parent: %s.", parent_lookup["name"])
1471
+ # update the parent
1472
+ update_parent = parent_lookup["objective"]
1473
+ update_parent["parentObjectiveId"] = parent_lookup["id"]
1474
+ try:
1475
+ # upload to RegScale
1476
+ update_response = api.put(
1477
+ f'{url_objs}{update_parent["id"]}',
1478
+ json=update_parent,
1479
+ )
1480
+ logger.debug(
1481
+ "Success - %s - objective parent updated successfully.",
1482
+ update_parent["name"],
1483
+ )
1484
+ updates.append(update_response)
1485
+ except requests.exceptions.RequestException:
1486
+ logger.error(
1487
+ "Unable to update parent objective: %s.",
1488
+ update_parent["name"],
1489
+ )
1490
+ errors.append(update_parent)
1491
+ job_progress.update(task, advance=1)
1492
+
1493
+
1494
+ def process_fedramp_objectives(part: dict, str_obj: str, objectives: list, ctrl, str_name: Optional[str] = "") -> str:
1495
+ """
1496
+ Function to handle processing of FedRAMP objectives
1497
+
1498
+ :param dict part: Part from OSCAL
1499
+ :param str str_obj: Concatenated objective string
1500
+ :param list objectives: A list of OSCAL objective strings
1501
+ :param ctrl: Parent security control
1502
+ :param str str_name: Name of the part
1503
+ :return: Processed FedRAMP objective object
1504
+ :rtype: str
1505
+ """
1506
+ str_prose = part.get("prose", "")
1507
+
1508
+ str_name = part.get("name", "") if not str_name else str_name
1509
+ if str_name == "statement":
1510
+ str_obj += f"{str_prose} "
1511
+ elif str_name == "item" or str_name == "objective":
1512
+ if "props" in part and str_name != "objective":
1513
+ str_obj += f"{part['props'][0]['value']} "
1514
+ str_obj += f"{str_prose} "
1515
+ str_type = "assessment" if str_name == "objective" else "objective"
1516
+
1517
+ if sub_parts := part.get("parts", []):
1518
+ for sub_part in sub_parts:
1519
+ str_obj2 = str_obj
1520
+ _ = process_fedramp_objectives(sub_part, str_obj2, objectives, ctrl, str_name)
1521
+ else:
1522
+ if str_obj.rstrip() != "":
1523
+ new_obj = {
1524
+ "id": 0,
1525
+ "name": part["id"],
1526
+ "part_id": ctrl["id"],
1527
+ "objectiveType": str_type,
1528
+ "description": str_obj,
1529
+ "parentControl": ctrl["id"],
1530
+ "parentObjective": "",
1531
+ }
1532
+ objectives.append(new_obj)
1533
+ return str_obj.rstrip()
1534
+
1535
+
1536
+ def process_objectives(objs: list, parts: list, ctrl: Union[list, dict], parent_id: int, fedramp: bool) -> str:
1537
+ """
1538
+ Function for recursively working through objectives and formatting it as HTML description
1539
+
1540
+ :param list objs: List of RegScale object
1541
+ :param list parts: Parts of the object
1542
+ :param Union[list, dict] ctrl: Controls for the object
1543
+ :param int parent_id: Parent id of the Object
1544
+ :param bool fedramp: Flag if catalog is a FedRAMP catalog
1545
+ :return: HTML formatted string of object
1546
+ :rtype: str
1547
+ """
1548
+ str_obj = "<ul>"
1549
+ # loop through parts/objectives recursively
1550
+ for obj in objs:
1551
+ # check prose
1552
+ str_prose = obj.get("prose", "")
1553
+
1554
+ # check name
1555
+ str_name = obj.get("name", "")
1556
+
1557
+ # create the new part
1558
+ part = {
1559
+ "id": 0,
1560
+ "part_id": obj["id"].split("_")[0],
1561
+ "part_label": obj["props"][0]["value"] if "props" in obj else None,
1562
+ "name": obj["id"],
1563
+ "objectiveType": str_name if "objective" not in str_name else "objective",
1564
+ "description": str_prose,
1565
+ "parentControl": ctrl["id"],
1566
+ "parentObjective": parent_id,
1567
+ }
1568
+ if "obj" in obj["id"] or "item" in obj["name"] or "overview" in obj["name"]:
1569
+ parts.append(part)
1570
+ str_obj += LI + "{{" + obj["id"] + "}}"
1571
+ if "prose" in obj:
1572
+ str_obj += " - " + str_prose
1573
+ str_obj += LI_CLOSE
1574
+ if "parts" in obj:
1575
+ str_obj += process_objectives(obj["parts"], parts, ctrl, obj["id"], fedramp)
1576
+ str_obj += UL_CLOSE
1577
+ return str_obj
1578
+
1579
+
1580
+ def process_control(
1581
+ ctrl: Union[list, dict],
1582
+ resources: list,
1583
+ str_family: str,
1584
+ parameters: list,
1585
+ parts: list,
1586
+ assessments: list,
1587
+ objectives: list,
1588
+ fedramp: bool,
1589
+ ) -> dict:
1590
+ """
1591
+ Function to process each control and formats it as a dictionary
1592
+
1593
+ :param Union[list, dict] ctrl: RegScale control
1594
+ :param list resources: resources of control
1595
+ :param str str_family: Family of control
1596
+ :param list parameters: Parameters for the control
1597
+ :param list parts: Parts of the control
1598
+ :param list assessments: Assessments belonging to the control
1599
+ :param list objectives: A list of OSCAL objective strings
1600
+ :param bool fedramp: Boolean - is a FedRAMP catalog
1601
+ :return: Dictionary of new control
1602
+ :rtype: dict
1603
+ """
1604
+ # see if parameters exist
1605
+ if "params" in ctrl:
1606
+ # loop through each parameter
1607
+ for param in ctrl["params"]:
1608
+ # create a new parameter object
1609
+ p_new = {
1610
+ "name": param["id"],
1611
+ "value": "",
1612
+ "paramType": "",
1613
+ "default": "",
1614
+ "controlId": ctrl["id"],
1615
+ }
1616
+ # process basic label
1617
+ if "label" in param:
1618
+ p_new["paramType"] = "text"
1619
+ p_new["value"] = param["label"]
1620
+ else:
1621
+ # initialize
1622
+ str_params = "Select ("
1623
+ # process select types
1624
+ if "select" in param:
1625
+ select = param["select"]
1626
+ if "how-many" in select:
1627
+ str_params += select["how-many"]
1628
+ p_new["paramType"] = "how-many"
1629
+ if "choice" in select:
1630
+ p_new["paramType"] = "choice"
1631
+ str_params += "select) - "
1632
+ for cho in select["choice"]:
1633
+ str_params += cho + ", "
1634
+ p_new["value"] = str_params
1635
+
1636
+ # check for default
1637
+ if "constraints" in param:
1638
+ for c in param["constraints"]:
1639
+ p_new["default"] += c["description"] + "; "
1640
+
1641
+ # add to the array
1642
+ parameters.append(p_new)
1643
+
1644
+ # get enhancements
1645
+ str_enhance = ""
1646
+ if "controls" in ctrl:
1647
+ child_enhc = ctrl["controls"]
1648
+ str_enhance += "<strong>Enhancements</strong><br/><br/>"
1649
+ str_enhance += "<ul>"
1650
+ for che in child_enhc:
1651
+ str_enhance += LI + "{{" + che["id"] + "}} - " + che["title"] + LI_CLOSE
1652
+ str_enhance += UL_CLOSE
1653
+
1654
+ # process control links
1655
+ int_link = 1
1656
+ str_links = ""
1657
+ if "links" in ctrl:
1658
+ for link in ctrl["links"]:
1659
+ # lookup the OSCAL control to enrich the data
1660
+ link_lookup = next(
1661
+ (item for item in resources if ("#" + item["uuid"]) == link["href"]),
1662
+ None,
1663
+ )
1664
+ if link_lookup is not None:
1665
+ str_links += (
1666
+ str(int_link) + ") " + link_lookup["title"] + " (OSCAL ID: " + link_lookup["uuid"] + ")<br/>"
1667
+ )
1668
+ int_link += 1
1669
+ else:
1670
+ str_links += link["href"] + "<br/>"
1671
+
1672
+ # process parts
1673
+ part_info = process_parts(ctrl, parts, assessments, objectives, fedramp)
1674
+
1675
+ # process sort Id if provided (FedRAMP specific)
1676
+ str_sort_id = ctrl["id"]
1677
+ if "props" in ctrl:
1678
+ for p in ctrl["props"]:
1679
+ if p["name"] == "sort-id":
1680
+ str_sort_id = p["value"]
1681
+
1682
+ # add control
1683
+ new_ctrl = {
1684
+ "id": ctrl["id"],
1685
+ "title": ctrl["title"],
1686
+ "sortId": str_sort_id,
1687
+ "family": (str_family if package_version.parse(schema) <= package_version.parse("1.0.2") else ctrl["class"]),
1688
+ "links": str_links,
1689
+ "parameters": "",
1690
+ "parts": part_info["parts"],
1691
+ "assessment": part_info["assessments"],
1692
+ "guidance": part_info["guidance"],
1693
+ "enhancements": str_enhance,
1694
+ }
1695
+
1696
+ # return the result
1697
+ return new_ctrl
1698
+
1699
+
1700
+ def process_parts(ctrl: Union[list, dict], parts: list, assessments: list, objectives: list, fedramp: bool) -> dict:
1701
+ """
1702
+ Function to format control
1703
+
1704
+ :param Union[list, dict] ctrl: RegScale control
1705
+ :param list parts: Parts of the control
1706
+ :param list assessments: Assessments of the control
1707
+ :param list objectives: A list of OSCAL objective strings
1708
+ :param bool fedramp: Boolean - is a FedRAMP catalog
1709
+ :return: Formatted dictionary
1710
+ :rtype: dict
1711
+ """
1712
+ # process parts
1713
+ if "parts" in ctrl:
1714
+ # initialize
1715
+ str_parts = ""
1716
+ str_guidance = ""
1717
+ str_assessment = ""
1718
+
1719
+ # create text field for human display
1720
+ str_parts += "<ul>"
1721
+ for dat in ctrl["parts"]:
1722
+ if (
1723
+ ("id" in dat)
1724
+ and (dat["name"].startswith("assessment") is False and dat["name"].startswith("assess") is False)
1725
+ ) or (
1726
+ ("id" in dat)
1727
+ and dat["name"]
1728
+ in [
1729
+ "item",
1730
+ "assessment-objective",
1731
+ "statement",
1732
+ "objective",
1733
+ "overview",
1734
+ ]
1735
+ ):
1736
+ # check prose
1737
+ str_prose = ""
1738
+ if "prose" in dat:
1739
+ str_prose = dat["prose"]
1740
+
1741
+ # check name
1742
+ str_name = ""
1743
+ if "name" in dat:
1744
+ str_name = dat["name"]
1745
+
1746
+ # create the new part
1747
+ part = {
1748
+ "id": 0,
1749
+ "name": dat["id"],
1750
+ "part_id": dat["id"].split("_")[0],
1751
+ "objectiveType": str_name,
1752
+ "description": (str_prose if str_prose else "Determine if the organization:"),
1753
+ "parentControl": ctrl["id"],
1754
+ "parentObjective": "",
1755
+ }
1756
+
1757
+ # process statements and objectives
1758
+ if str_name in [
1759
+ "assessment-objective",
1760
+ "statement",
1761
+ "objective",
1762
+ "overview",
1763
+ ]:
1764
+ parts.append(part)
1765
+ try:
1766
+ str_parts += LI + "{{" + dat["id"] + "}} - " + str_prose + LI_CLOSE
1767
+ except Exception:
1768
+ logger.error("Unable to parse part - %s.", dat["id"])
1769
+ if "parts" in dat:
1770
+ str_parts += process_objectives(dat["parts"], parts, ctrl, dat["id"], fedramp)
1771
+
1772
+ # FedRAMP specific processing
1773
+ if fedramp:
1774
+ process_fedramp_objectives(dat, "", objectives, ctrl)
1775
+
1776
+ # process guidance
1777
+ if dat["name"] in ["guidance", "overview"]:
1778
+ str_guidance = "<ul><li>Guidance</li>"
1779
+ if "prose" in dat:
1780
+ str_guidance += "<ul>"
1781
+ str_guidance += LI + dat["prose"] + LI_CLOSE
1782
+ str_guidance += UL_CLOSE
1783
+ if "links" in dat:
1784
+ str_guidance += "<ul>"
1785
+ for lkp in dat["links"]:
1786
+ str_guidance += LI + lkp["href"] + ", " + lkp["rel"] + LI_CLOSE
1787
+ str_guidance += UL_CLOSE
1788
+ str_guidance += UL_CLOSE
1789
+ else:
1790
+ # process assessments
1791
+ process_assessments(dat, ctrl, assessments)
1792
+
1793
+ str_parts += UL_CLOSE
1794
+ else:
1795
+ # no parts - set default values
1796
+ str_parts = ""
1797
+ str_guidance = ""
1798
+ str_assessment = ""
1799
+
1800
+ # return the result
1801
+ part_info = {
1802
+ "parts": str_parts,
1803
+ "guidance": str_guidance,
1804
+ "assessments": str_assessment,
1805
+ }
1806
+ return part_info
1807
+
1808
+
1809
+ def process_assessments(dat: dict, ctrl: dict, assessments: list) -> None:
1810
+ """
1811
+ Process assessment data
1812
+
1813
+ :param dict dat: Data to process
1814
+ :param dict ctrl: Controls
1815
+ :param list assessments: list of assessments
1816
+ :rtype: None
1817
+ """
1818
+ # process assessments
1819
+ if dat["name"].startswith("assessment") is True:
1820
+ # see if a lower level objective that has prose
1821
+ if "prose" in dat:
1822
+ # create new assessment objective
1823
+ ast = {
1824
+ "id": 0,
1825
+ "name": dat["id"],
1826
+ "testType": dat["name"],
1827
+ "description": dat["prose"],
1828
+ "parentControl": ctrl["id"],
1829
+ }
1830
+
1831
+ # see if it has any child tests
1832
+ if "parts" in dat:
1833
+ if len(dat["parts"]) > 0:
1834
+ for item in dat["parts"]:
1835
+ process_assessments(item, ctrl, assessments)
1836
+ else:
1837
+ # check the id
1838
+ str_part_id = ""
1839
+ if "id" in dat:
1840
+ str_part_id = dat["id"]
1841
+ else:
1842
+ str_part_id = str(uuid.uuid4())
1843
+
1844
+ # handle methods
1845
+ ast = {
1846
+ "id": 0,
1847
+ "name": str_part_id,
1848
+ "testType": "",
1849
+ "description": "",
1850
+ "parentControl": ctrl["id"],
1851
+ }
1852
+ props_data = dat.get("props", [{}])
1853
+ if "value" in props_data[0]:
1854
+ ast["testType"] = props_data[0]["value"]
1855
+ parts_data = dat.get("parts", [{}])
1856
+ if "prose" in parts_data[0]:
1857
+ ast["description"] = parts_data[0]["prose"]
1858
+
1859
+ # add test of the array
1860
+ if ast["description"] != "":
1861
+ assessments.append(ast)
1862
+ elif dat["name"].startswith("assess") is True:
1863
+ # check the id
1864
+ str_part_id = ""
1865
+ if "id" in dat:
1866
+ str_part_id = dat["id"]
1867
+ else:
1868
+ str_part_id = str(uuid.uuid4())
1869
+
1870
+ # handle methods
1871
+ ast = {
1872
+ "id": 0,
1873
+ "name": str_part_id,
1874
+ "testType": "",
1875
+ "description": "",
1876
+ "parentControl": ctrl["id"],
1877
+ }
1878
+
1879
+ # check test type
1880
+ str_test_type = "TEST"
1881
+ props_data = dat.get("props", [{}])
1882
+ if "value" in props_data[0]:
1883
+ str_test_type = props_data[0]["value"]
1884
+ ast["testType"] = str_test_type
1885
+
1886
+ # get the description
1887
+ str_description = ""
1888
+ parts_data = dat.get("parts", [{}])
1889
+ if "prose" in parts_data[0]:
1890
+ str_description = parts_data[0]["prose"]
1891
+ ast["description"] = str_description
1892
+
1893
+ # add test of the array
1894
+ if ast["description"] != "":
1895
+ assessments.append(ast)
1896
+
1897
+
1898
+ def oscal_version(metadata: dict) -> str:
1899
+ """
1900
+ Determine the oscal base version
1901
+
1902
+ :param dict metadata: The metadata from OSCAL
1903
+ :raises ValueError: CLI support statement
1904
+ :return: The schema version
1905
+ :rtype: str
1906
+ """
1907
+ supported_versions = ["1.0.0", "1.0.2", "1.0.4", "1.1.1", "1.1.2"]
1908
+ oscal_schema = metadata["oscal-version"]
1909
+ if oscal_schema not in supported_versions:
1910
+ raise ValueError(
1911
+ f"The RegScale CLI does not support OSCAL version {oscal_schema}, only "
1912
+ f"versions {', '.join(supported_versions)} are supported."
1913
+ )
1914
+ logger.info("Oscal Version: %s", oscal_schema)
1915
+ return oscal_schema
1916
+
1917
+
1918
+ def append_controls(
1919
+ oscal_controls: list,
1920
+ ctrl: Union[list, dict],
1921
+ resources: list,
1922
+ str_family: str,
1923
+ parameters: list,
1924
+ parts: list,
1925
+ assessments: list,
1926
+ objectives: list,
1927
+ fedramp: bool,
1928
+ ) -> list[dict]:
1929
+ """
1930
+ Process and append controls to list
1931
+
1932
+ :param list oscal_controls: A list of oscal control dictionaries.
1933
+ :param Union[list, dict] ctrl: An OSCAL control dictionary or list
1934
+ :param list resources: resources of control
1935
+ :param str str_family: The name of the control family
1936
+ :param list parameters: A list of parameters dictionaries
1937
+ :param list parts: A list of OSCAL part strings.
1938
+ :param list assessments: A list of OSCAL assesment strings
1939
+ :param list objectives: A list of OSCAL objective strings
1940
+ :param bool fedramp: Boolean - is a FedRAMP catalog
1941
+ :return: list[dict]
1942
+ :rtype: list[dict]
1943
+ """
1944
+ new_ctrl = process_control(ctrl, resources, str_family, parameters, parts, assessments, objectives, fedramp)
1945
+ oscal_controls.append(new_ctrl)
1946
+ return oscal_controls