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,1124 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # pylint: disable=C0415
4
+ """standard python imports"""
5
+ import json
6
+ import math
7
+ import re
8
+ from collections import Counter
9
+ from concurrent.futures import as_completed
10
+ from concurrent.futures.thread import ThreadPoolExecutor
11
+ from typing import Dict, List, Literal, Optional, Tuple
12
+ from urllib.parse import urljoin
13
+
14
+ import click
15
+
16
+ from regscale.core.app.api import Api
17
+ from regscale.core.app.utils.app_utils import create_progress_object, error_and_exit, get_current_datetime
18
+ from regscale.core.utils.graphql import GraphQLQuery
19
+ from regscale.integrations.public.fedramp.parts_mapper import PartMapper
20
+ from regscale.integrations.public.fedramp.ssp_logger import SSPLogger
21
+ from regscale.models import ControlObjective, ImplementationObjective
22
+ from regscale.models.regscale_models import (
23
+ ControlImplementation,
24
+ File,
25
+ LeveragedAuthorization,
26
+ SecurityControl,
27
+ SecurityPlan,
28
+ )
29
+ from regscale.models.regscale_models.control_implementation import ControlImplementationStatus
30
+
31
+ logger = SSPLogger()
32
+ part_mapper_rev5 = PartMapper()
33
+ part_mapper_rev4 = PartMapper()
34
+ progress = create_progress_object()
35
+
36
+ SERVICE_PROVIDER_CORPORATE = "Service Provider Corporate"
37
+ SERVICE_PROVIDER_SYSTEM_SPECIFIC = "Service Provider System Specific"
38
+ SERVICE_PROVIDER_HYBRID = "Service Provider Hybrid"
39
+ PROVIDER_SYSTEM_SPECIFIC = "Provider (System Specific)"
40
+ CUSTOMER_PROVIDED = "Provided by Customer"
41
+ CUSTOMER_CONFIGURED = "Customer Configured"
42
+ CONFIGURED_BY_CUSTOMER = "Configured by Customer"
43
+ NOT_IMPLEMENTED = ControlImplementationStatus.NotImplemented.value
44
+ PARTIALLY_IMPLEMENTED = ControlImplementationStatus.PartiallyImplemented.value
45
+ CONTROL_ID = "Control ID"
46
+ ALT_IMPLEMENTATION = "Alternate Implementation"
47
+ CAN_BE_INHERITED_CSP = "Can Be Inherited from CSP"
48
+ IMPACT_LEVEL = "Impact Level"
49
+ SYSTEM_NAME = "System Name"
50
+ CSP = "CSP"
51
+
52
+ STATUS_MAPPING = {
53
+ "Implemented": ControlImplementationStatus.Implemented,
54
+ PARTIALLY_IMPLEMENTED: ControlImplementationStatus.PartiallyImplemented,
55
+ ControlImplementationStatus.Planned.value: ControlImplementationStatus.Planned,
56
+ "N/A": ControlImplementationStatus.NA,
57
+ "Alternative Implementation": ControlImplementationStatus.Alternative,
58
+ ALT_IMPLEMENTATION: ControlImplementationStatus.Alternative,
59
+ }
60
+
61
+ RESPONSIBILITY_MAP = {
62
+ # Original keys
63
+ "SERVICE_PROVIDER_CORPORATE": "Provider",
64
+ SERVICE_PROVIDER_SYSTEM_SPECIFIC: PROVIDER_SYSTEM_SPECIFIC,
65
+ SERVICE_PROVIDER_HYBRID: "Hybrid",
66
+ CUSTOMER_PROVIDED: "Customer",
67
+ CONFIGURED_BY_CUSTOMER: CUSTOMER_CONFIGURED,
68
+ "Shared": "Shared",
69
+ "Inherited": "Inherited",
70
+ # Boolean keys
71
+ "bServiceProviderCorporate": "Provider",
72
+ "bServiceProviderSystemSpecific": PROVIDER_SYSTEM_SPECIFIC,
73
+ "bServiceProviderHybrid": "Hybrid",
74
+ "bProvidedByCustomer": "Customer",
75
+ "bConfiguredByCustomer": CUSTOMER_CONFIGURED,
76
+ "bShared": "Shared",
77
+ "bInherited": "Inherited",
78
+ }
79
+
80
+
81
+ def transform_control(control: str) -> str:
82
+ """
83
+ Function to parse the control string and transform it to the RegScale format
84
+ ex: AC-1 (a) -> ac-1.a or AC-6 (10) -> ac-6.10
85
+
86
+ :param str control: Control ID as a string
87
+ :return: Transformed control ID to match RegScale control ID format
88
+ :rtype: str
89
+ """
90
+ # Use regex to match the pattern and capture the parts
91
+ match = re.match(r"([A-Za-z]+)-(\d+)\s\((\d+|[a-z])\)", control)
92
+ if match:
93
+ control_name = match.group(1).lower()
94
+ control_number = match.group(2)
95
+ sub_control = match.group(3)
96
+
97
+ if sub_control.isdigit():
98
+ transformed_control = f"{control_name}-{control_number}.{sub_control}"
99
+ else:
100
+ transformed_control = f"{control_name}-{control_number}"
101
+
102
+ return transformed_control
103
+ return control.lower()
104
+
105
+
106
+ def new_leveraged_auth(
107
+ ssp: SecurityPlan, user_id: str, instructions_data: dict, version: Literal["rev4", "rev5"]
108
+ ) -> int:
109
+ """
110
+ Function to create a new Leveraged Authorization in RegScale.
111
+
112
+ :param SecurityPlan ssp: RegScale SSP Object
113
+ :param str user_id: RegScale user ID
114
+ :param dict instructions_data: Data parsed from Instructions worksheet in the FedRAMP CIS CRM workbook
115
+ :param Literal["rev4", "rev5"] version: FedRAMP revision version
116
+ :return: Newly created Leveraged Authorization ID in RegScale
117
+ :rtype: int
118
+ """
119
+ leveraged_auth = LeveragedAuthorization(
120
+ title=instructions_data[CSP],
121
+ servicesUsed=instructions_data[CSP],
122
+ fedrampId=(instructions_data["System Identifier"] if version == "rev5" else instructions_data[SYSTEM_NAME]),
123
+ authorizationType="FedRAMP Ready",
124
+ impactLevel=instructions_data[IMPACT_LEVEL],
125
+ dateAuthorized="",
126
+ natureOfAgreement="Other",
127
+ dataTypes="Other",
128
+ authorizedUserTypes="Other",
129
+ authenticationType="Other",
130
+ createdById=user_id,
131
+ securityPlanId=ssp.id,
132
+ ownerId=user_id,
133
+ lastUpdatedById=user_id,
134
+ description="Imported from FedRAMP CIS CRM Workbook on " + get_current_datetime("%m/%d/%Y %H:%M:%S"),
135
+ )
136
+ new_leveraged_auth_id = leveraged_auth.create()
137
+ return new_leveraged_auth_id.id
138
+
139
+
140
+ def gen_key(control_id: str):
141
+ """
142
+ Function to generate a key for the control ID
143
+
144
+ :param str control_id: The control ID to generate a key for
145
+ :return: The generated key
146
+ :rtype: str
147
+ """
148
+ # Match pattern: captures everything up to either:
149
+ # 1. The last (number) if it exists
150
+ # 2. The main control number if no enhancement exists
151
+ # And excludes any trailing (letter)
152
+ pattern = r"^((?:\w+-\d+(?:\(\d+\))?))(?:\([a-zA-Z]\))?$"
153
+
154
+ match = re.match(pattern, control_id)
155
+ if match:
156
+ return match.group(1)
157
+ return control_id
158
+
159
+
160
+ def map_implementation_status(control_id: str, cis_data: dict) -> str:
161
+ """
162
+ Function to map the selected implementation status on the CIS worksheet to a RegScale status
163
+
164
+ :param str control_id: The control ID from RegScale
165
+ :param dict cis_data: Data from the CIS worksheet to map the status from
166
+ :return: RegScale control implementation status
167
+ :rtype: str
168
+ """
169
+
170
+ # Extract matching records
171
+ cis_records = [
172
+ value
173
+ for value in cis_data.values()
174
+ if gen_key(value.get("regscale_control_id", "")).lower() == control_id.lower()
175
+ ]
176
+
177
+ status_ret = ControlImplementationStatus.NotImplemented
178
+
179
+ logger.debug("Found %d CIS records for control %s", len(cis_records), control_id)
180
+
181
+ if not cis_records:
182
+ logger.warning(f"No CIS records found for control {control_id}")
183
+ return status_ret
184
+
185
+ # Count implementation statuses
186
+ status_counts = Counter(record.get("implementation_status", "") for record in cis_records)
187
+ logger.debug("Status distribution for %s: %s", control_id, dict(status_counts))
188
+
189
+ # Early returns for simple cases
190
+ if len(status_counts) == 1:
191
+ status = next(iter(status_counts))
192
+ return STATUS_MAPPING.get(status, ControlImplementationStatus.NotImplemented)
193
+
194
+ # Priority-based status determination
195
+ if any(status in ["N/A", "Alternative Implementation"] for status in status_counts):
196
+ status_ret = ControlImplementationStatus.NA
197
+
198
+ implemented_count = status_counts.get("Implemented", 0)
199
+ total_count = sum(status_counts.values())
200
+
201
+ if implemented_count == total_count:
202
+ status_ret = ControlImplementationStatus.FullyImplemented
203
+ elif implemented_count > 0 or any(status == "Partially Implemented" for status in status_counts):
204
+ status_ret = ControlImplementationStatus.PartiallyImplemented
205
+ elif any(status == "Planned" for status in status_counts):
206
+ status_ret = ControlImplementationStatus.Planned
207
+
208
+ return status_ret
209
+
210
+
211
+ def map_origination(control_id: str, cis_data: dict) -> dict:
212
+ """
213
+ Function to map the responsibility for a control implementation from the CRM worksheet
214
+
215
+ :param str control_id: RegScale control ID
216
+ :param dict cis_data: Data from the CRM worksheet
217
+ :return: The responsibility information in regscale format
218
+ :rtype: dict
219
+ """
220
+ origination_bools = {
221
+ "bInherited": False,
222
+ "bServiceProviderCorporate": False,
223
+ "bServiceProviderSystemSpecific": False,
224
+ "bServiceProviderHybrid": False,
225
+ "bConfiguredByCustomer": False,
226
+ "bProvidedByCustomer": False,
227
+ "bShared": False,
228
+ "record_text": "",
229
+ }
230
+ cis_records = [
231
+ value for _, value in cis_data.items() if gen_key(value["regscale_control_id"]).lower() == control_id.lower()
232
+ ]
233
+ for record in cis_records:
234
+ # Create the implementation objective, and save.
235
+ control_origination = record.get("control_origination", "")
236
+ if SERVICE_PROVIDER_CORPORATE in control_origination:
237
+ # responsibility = "Provider"
238
+ origination_bools["bServiceProviderCorporate"] = True
239
+ if SERVICE_PROVIDER_SYSTEM_SPECIFIC in control_origination:
240
+ # responsibility = "Provider (System Specific)"
241
+ origination_bools["bServiceProviderSystemSpecific"] = True
242
+ if SERVICE_PROVIDER_HYBRID in control_origination:
243
+ # responsibility = "Hybrid"
244
+ origination_bools["bServiceProviderHybrid"] = True
245
+ if CUSTOMER_PROVIDED in control_origination:
246
+ # responsibility = "Customer"
247
+ origination_bools["bProvidedByCustomer"] = True
248
+ if CONFIGURED_BY_CUSTOMER in control_origination:
249
+ # responsibility = "Customer Configured"
250
+ origination_bools["bConfiguredByCustomer"] = True
251
+ if "Shared" in control_origination:
252
+ # responsibility = "Shared"
253
+ origination_bools["bShared"] = True
254
+ if "Inherited" in control_origination:
255
+ # responsibility = "Inherited"
256
+ origination_bools["bInherited"] = True
257
+ origination_bools["record_text"] += control_origination
258
+ return origination_bools
259
+
260
+
261
+ def clean_customer_responsibility(value: str):
262
+ """
263
+ Function to clean the customer responsibility value
264
+
265
+ :param str value: The value to clean
266
+ :return: The cleaned value
267
+ :rtype: str
268
+ """
269
+ if not value:
270
+ return ""
271
+ try:
272
+ return "" if math.isnan(float(value)) else str(value)
273
+ except (ValueError, TypeError):
274
+ return str(value)
275
+
276
+
277
+ def update_imp_objective(
278
+ leverage_auth_id: int,
279
+ existing_imp_obj: List[ImplementationObjective],
280
+ imp: ControlImplementation,
281
+ objectives: List[ControlObjective],
282
+ record: dict,
283
+ ) -> Optional[ImplementationObjective]:
284
+ """
285
+ Update the control objective with the given record data.
286
+
287
+ :param int leverage_auth_id: The leveraged authorization ID
288
+ :param List[ImplementationObjective] existing_imp_obj: The existing implementation objective
289
+ :param ControlImplementation imp: The control implementation to update
290
+ :param List[ControlObjective] objectives: The control objective to update
291
+ :param dict record: The CIS/CRM record data to update the objective with
292
+ :rtype: Optional[ImplementationObjective]
293
+ :return: The updated or created implementation objective
294
+ """
295
+ status_map = {
296
+ "Implemented": ControlImplementationStatus.Implemented.value,
297
+ "Planned": ControlImplementationStatus.Implemented.Planned.value,
298
+ PARTIALLY_IMPLEMENTED: PARTIALLY_IMPLEMENTED,
299
+ "N/A": ControlImplementationStatus.NA.value,
300
+ NOT_IMPLEMENTED: NOT_IMPLEMENTED,
301
+ }
302
+
303
+ responsibility_map = {
304
+ "Provider": SERVICE_PROVIDER_CORPORATE,
305
+ PROVIDER_SYSTEM_SPECIFIC: SERVICE_PROVIDER_SYSTEM_SPECIFIC,
306
+ "Customer": "Provided by Customer (Customer System Specific)",
307
+ "Hybrid": "Service Provider Hybrid (Corporate and System Specific)",
308
+ CUSTOMER_CONFIGURED: "Configured by Customer (Customer System Specific)",
309
+ "Shared": "Shared (Service Provider and Customer Responsibility)",
310
+ "Inherited": "Inherited from pre-existing FedRAMP Authorization",
311
+ }
312
+
313
+ cis_record = record.get("cis", {})
314
+ crm_record = record.get("crm", {})
315
+ responsibility = RESPONSIBILITY_MAP.get(
316
+ cis_record.get("control_origination", ""), ControlImplementationStatus.NA.value
317
+ )
318
+ customer_responsibility = clean_customer_responsibility(
319
+ crm_record.get("specific_inheritance_and_customer_agency_csp_responsibilities")
320
+ )
321
+ ret_objective = None
322
+ existing_pairs = {(obj.objectiveId, obj.implementationId) for obj in existing_imp_obj}
323
+ for objective in objectives:
324
+ current_pair = (objective.id, imp.id)
325
+ if current_pair not in existing_pairs:
326
+ imp_obj = ImplementationObjective(
327
+ id=0,
328
+ uuid="",
329
+ inherited=crm_record.get("can_be_inherited_from_csp") == "Yes",
330
+ implementationId=imp.id,
331
+ status=status_map.get(cis_record.get("implementation_status", NOT_IMPLEMENTED), NOT_IMPLEMENTED),
332
+ objectiveId=objective.id,
333
+ notes=objective.name,
334
+ securityControlId=objective.securityControlId,
335
+ responsibility=responsibility_map.get(responsibility, responsibility),
336
+ cloudResponsibility=customer_responsibility,
337
+ customerResponsibility=customer_responsibility,
338
+ authorizationId=leverage_auth_id,
339
+ )
340
+ ret_objective = imp_obj.create()
341
+ else:
342
+ # NOTE: Don't overwrite the responsibility text and only append.
343
+ ex_obj = next((obj for obj in existing_imp_obj if obj.objectiveId == objective.id), None)
344
+ if ex_obj:
345
+ ex_obj.status = status_map.get(
346
+ cis_record.get("implementation_status", NOT_IMPLEMENTED), NOT_IMPLEMENTED
347
+ )
348
+ try:
349
+ seperator = " \n---------------\n "
350
+ if ex_obj.responsibility:
351
+ ex_obj.responsibility = (
352
+ seperator.join([ex_obj.responsibility, responsibility])
353
+ if ex_obj.responsibility != responsibility
354
+ else ex_obj.responsibility
355
+ )
356
+ if ex_obj.cloudResponsibility:
357
+ ex_obj.cloudResponsibility = (
358
+ seperator.join([ex_obj.cloudResponsibility, responsibility])
359
+ if ex_obj.cloudResponsibility != responsibility
360
+ else ex_obj.cloudResponsibility
361
+ )
362
+ if ex_obj.customerResponsibility:
363
+ ex_obj.customerResponsibility = (
364
+ seperator.join([ex_obj.customerResponsibility, responsibility])
365
+ if ex_obj.cloudResponsibility != responsibility
366
+ else ex_obj.customerResponsibility
367
+ )
368
+ except TypeError:
369
+ logger.warning(f"Failed to update responsibility on Implementation Objective #{ex_obj.id}")
370
+ ret_objective = ex_obj.save()
371
+
372
+ return ret_objective
373
+
374
+
375
+ def parse_control_details(
376
+ version: Literal["rev4", "rev5"], control_imp: ControlImplementation, control: SecurityControl, cis_data: dict
377
+ ) -> ControlImplementation:
378
+ """
379
+ Function to parse control details from RegScale and CIS data and returns an updated ControlImplementation object
380
+
381
+ :param Literal["rev4", "rev5"] version: The version of the workbook
382
+ :param ControlImplementation control_imp: RegScale ControlImplementation object to update
383
+ :param SecurityControl control: RegScale control
384
+ :param dict cis_data: Data from the CIS worksheet
385
+ :return: Updated ControlImplementation object
386
+ :rtype: ControlImplementation
387
+ """
388
+ control_id = control.controlId if version == "rev5" else control.sortId
389
+ status = map_implementation_status(control_id=control_id, cis_data=cis_data)
390
+ origination_bool = map_origination(control_id=control_id, cis_data=cis_data)
391
+ control_imp.status = status
392
+ if status == ControlImplementationStatus.Planned:
393
+ control_imp.plannedImplementationDate = get_current_datetime("%Y-%m-%d")
394
+ control_imp.stepsToImplement = "To be updated"
395
+ control_imp.controlSource = "Baseline" if not origination_bool["bInherited"] else "Inherited"
396
+ control_imp.exclusionJustification = (
397
+ "Imported from FedRAMP CIS CRM Workbook" if status == ControlImplementationStatus.NA else None
398
+ )
399
+
400
+ control_imp.bInherited = origination_bool["bInherited"]
401
+ control_imp.inheritable = origination_bool["bInherited"]
402
+ control_imp.bServiceProviderCorporate = origination_bool["bServiceProviderCorporate"]
403
+ control_imp.bServiceProviderSystemSpecific = origination_bool["bServiceProviderSystemSpecific"]
404
+ control_imp.bServiceProviderHybrid = origination_bool["bServiceProviderHybrid"]
405
+ control_imp.bConfiguredByCustomer = origination_bool["bConfiguredByCustomer"]
406
+ control_imp.bProvidedByCustomer = origination_bool["bProvidedByCustomer"]
407
+ # NOTE Dale was concerned with overwriting the responsibility text, so we will only update if empty
408
+ if not control_imp.responsibility:
409
+ control_imp.responsibility = get_responsibility(origination_bool)
410
+ if updated_control := control_imp.save():
411
+ logger.debug("Control Implementation #%s updated successfully", control_imp.id)
412
+ return updated_control
413
+ logger.error("Failed to update Control Implementation \n" + json.dumps(control_imp.model_dump()))
414
+ return control_imp
415
+
416
+
417
+ def get_responsibility(origination_bool: dict) -> str:
418
+ """
419
+ Function to map the responsibility based on origination booleans.
420
+
421
+ :param dict origination_bool: Dictionary containing origination booleans
422
+ :return: Responsibility string
423
+ :rtype: str
424
+ """
425
+ responsibility = ControlImplementationStatus.NA.value
426
+
427
+ if origination_bool["bServiceProviderCorporate"]:
428
+ responsibility = SERVICE_PROVIDER_CORPORATE
429
+ if origination_bool["bServiceProviderSystemSpecific"]:
430
+ responsibility = SERVICE_PROVIDER_SYSTEM_SPECIFIC
431
+ if origination_bool["bServiceProviderHybrid"]:
432
+ responsibility = "Service Provider Hybrid"
433
+ if origination_bool["bProvidedByCustomer"]:
434
+ responsibility = "Provided by Customer"
435
+ if origination_bool["bConfiguredByCustomer"]:
436
+ responsibility = "Configured by Customer (Customer System Specific)"
437
+ if origination_bool["bInherited"]:
438
+ responsibility = "Inherited from pre-existing FedRAMP Authorization"
439
+ if origination_bool["bShared"]:
440
+ responsibility = "Shared (Service Provider and Customer Responsibility)"
441
+
442
+ return responsibility
443
+
444
+
445
+ def fetch_and_update_imps(
446
+ control: dict, api: Api, cis_data: dict, version: Literal["rev4", "rev5"]
447
+ ) -> Optional[ControlImplementation]:
448
+ """
449
+ Function to fetch implementation objectives from RegScale via API
450
+
451
+ :param dict control: RegScale control as a dictionary
452
+ :param Api api: RegScale API object
453
+ :param dict cis_data: Data from the CIS worksheet
454
+ :param Literal["rev4", "rev5"] version: The version of the workbook
455
+ :return: An updated control implementation if found
456
+ :rtype: Optional[ControlImplementation]
457
+ """
458
+ # get the control and control implementation objects
459
+ regscale_control = SecurityControl.get_object(control["scId"])
460
+ regscale_control_imp = ControlImplementation.get_object(control["id"])
461
+
462
+ if not regscale_control or not regscale_control_imp:
463
+ api.logger.error("Failed to fetch control or control implementation")
464
+ return regscale_control_imp
465
+
466
+ updated_control = parse_control_details(
467
+ version=version, control_imp=regscale_control_imp, control=regscale_control, cis_data=cis_data
468
+ )
469
+ return updated_control
470
+
471
+
472
+ def get_all_imps(api: Api, ssp_id: int, cis_data: dict, version: Literal["rev4", "rev5"]) -> list:
473
+ """
474
+ Function to retrieve control implementations and their objectives from RegScale
475
+
476
+ :param Api api: The RegScale API object
477
+ :param int ssp_id: The SSP ID
478
+ :param dict cis_data: The data from the CIS worksheet
479
+ :param Literal["rev4", "rev5"] version: The version of the workbook
480
+ :return: List of updated control implementations
481
+ :rtype: list
482
+ """
483
+ from requests import RequestException
484
+
485
+ updated_controls = []
486
+ url = urljoin(api.config["domain"], f"/api/controlImplementation/getSCListByPlan/{ssp_id}")
487
+ response = api.get(url)
488
+
489
+ if response.status_code == 404:
490
+ api.logger.warning(f"SSP with ID {ssp_id} has no controls.")
491
+ return updated_controls
492
+
493
+ # Check if the response is successful
494
+ if response.status_code == 200:
495
+ ssp_controls = response.json()
496
+ # Get Control Implementations For SSP
497
+ fetching_imps = progress.add_task(
498
+ f"[magenta]Fetching & updating {len(ssp_controls)} implementation(s)...", total=len(ssp_controls)
499
+ )
500
+ with ThreadPoolExecutor(max_workers=50) as executor:
501
+ futures = [
502
+ executor.submit(fetch_and_update_imps, control, api, cis_data, version) for control in ssp_controls
503
+ ]
504
+ for future in as_completed(futures):
505
+ progress.update(fetching_imps, advance=1)
506
+ try:
507
+ controls = future.result()
508
+ updated_controls.append(controls)
509
+ except (RequestException, TimeoutError) as ex:
510
+ api.logger.error(f"Error fetching control implementations: {ex}")
511
+ else:
512
+ api.logger.error(f"Failed to fetch controls: {response.status_code}: {response.reason}")
513
+
514
+ return updated_controls
515
+
516
+
517
+ def get_all_control_objectives(imps: List[ControlImplementation]) -> List[ControlObjective]:
518
+ """
519
+ Get All Control Objectives from GraphQL
520
+
521
+ :param List[ControlImplementation] imps: The Implementations
522
+ :return: List of ControlObjective
523
+ :rtype: List[ControlObjective]
524
+ """
525
+ api = Api()
526
+ res = []
527
+ # list of int to string
528
+ if imps:
529
+ query = GraphQLQuery()
530
+ query.start_query()
531
+ query.add_query(
532
+ entity="controlObjectives",
533
+ items=["id", "description", "otherId", "name", "securityControlId"],
534
+ where={"securityControlId": {"in": [c.controlID for c in imps]}},
535
+ )
536
+ query.end_query()
537
+ dat = api.graph(query=query.build())
538
+ res = [ControlObjective(**d) for d in dat.get("controlObjectives", {}).get("items", [])]
539
+ return res
540
+
541
+
542
+ def update_all_objectives(
543
+ leveraged_auth_id: int,
544
+ cis_data: Dict[str, Dict[str, str]],
545
+ crm_data: Dict[str, Dict[str, str]],
546
+ control_implementations: List[ControlImplementation],
547
+ version: Literal["rev4", "rev5"],
548
+ ) -> set:
549
+ """
550
+ Updates all objectives for the given control implementations based on CIS worksheet data.
551
+ Uses parallel processing and displays progress bars.
552
+
553
+ :param int leveraged_auth_id: The leveraged authorization ID
554
+ :param Dict[str, Dict[str, str]] cis_data: The CIS data to update from
555
+ :param Dict[str, Dict[str, str]] crm_data: The CRM data to update from
556
+ :param List[ControlImplementation] control_implementations: The control implementations to update
557
+ :param Literal["rev4", "rev5"] version: The version of the workbook
558
+ :return: A set of errors, if any
559
+ :rtype: set
560
+ """
561
+ all_control_objectives = get_all_control_objectives(imps=control_implementations)
562
+ error_set = set()
563
+ task = progress.add_task("[cyan]Processing control objectives...", total=len(control_implementations))
564
+ # Create a combined dataset for easier access
565
+ combined_data = {key: {"cis": cis_data[key], "crm": crm_data.get(key, {})} for key in cis_data}
566
+
567
+ # Process implementations in parallel
568
+ with ThreadPoolExecutor(max_workers=50) as executor:
569
+ # Submit all tasks
570
+ future_to_control = {
571
+ executor.submit(
572
+ process_implementation, leveraged_auth_id, imp, combined_data, version, all_control_objectives
573
+ ): imp
574
+ for imp in control_implementations
575
+ }
576
+
577
+ # Process results as they complete
578
+ for future in as_completed(future_to_control):
579
+ result = future.result()
580
+ if isinstance(result[0], list):
581
+ error_lst = result[0]
582
+ for inf in error_lst:
583
+ error_set.add(inf)
584
+ progress.update(task, advance=1)
585
+
586
+ return error_set
587
+
588
+
589
+ def report(error_set: set):
590
+ """
591
+ Function to report errors to the user
592
+
593
+ :param set error_set: Set of errors to report
594
+ :rtype: None
595
+ """
596
+ from rich.console import Console
597
+ from rich.table import Table
598
+
599
+ console = Console()
600
+
601
+ if error_set:
602
+ table = Table(title="Unmapped Control Objectives")
603
+
604
+ table.add_column(justify="left", style="red", no_wrap=True)
605
+
606
+ for error in sorted(error_set):
607
+ table.add_row(error)
608
+
609
+ console.print(table)
610
+
611
+
612
+ def process_implementation(
613
+ leveraged_auth_id: int,
614
+ implementation: ControlImplementation,
615
+ sheet_data: dict,
616
+ version: Literal["rev4", "rev5"],
617
+ all_objectives: List[ControlObjective],
618
+ ) -> Tuple[List[str], List[ImplementationObjective]]:
619
+ """
620
+ Processes a single implementation and its associated records.
621
+
622
+ :param int leveraged_auth_id: The leveraged authorization ID
623
+ :param ControlImplementation implementation: The control implementation to process
624
+ :param dict sheet_data: The CIS/CRM data to process
625
+ :param Literal["rev4", "rev5"] version: The version of the workbook
626
+ :param List[ControlObjective] all_objectives: all the control objectives
627
+ :rtype Tuple[List[str], List[ImplementationObjective]]
628
+ :returns A list of updated implementation objectives
629
+ """
630
+
631
+ errors = []
632
+ processed_objectives = []
633
+
634
+ existing_objectives, filtered_records = gen_filtered_records(implementation, sheet_data, version)
635
+ result = None
636
+ for record in filtered_records:
637
+ res = process_single_record(
638
+ leveraged_auth_id=leveraged_auth_id,
639
+ implementation=implementation,
640
+ record=record,
641
+ control_objectives=all_objectives,
642
+ existing_objectives=existing_objectives,
643
+ version=version,
644
+ )
645
+ if isinstance(res, tuple):
646
+ method_errors, result = res
647
+ errors.extend(method_errors)
648
+ if result:
649
+ processed_objectives.append(result)
650
+ return errors, processed_objectives
651
+
652
+
653
+ def gen_filtered_records(
654
+ implementation: ControlImplementation, sheet_data: dict, version: Literal["rev4", "rev5"]
655
+ ) -> Tuple[List[ImplementationObjective], List[Dict[str, str]]]:
656
+ """
657
+ Generates filtered records for a given implementation.
658
+
659
+ :param ControlImplementation implementation: The control implementation to filter records for
660
+ :param dict sheet_data: The CIS/CRM data to filter
661
+ :param Literal["rev4", "rev5"] version: The version of the workbook
662
+ :returns A tuple of existing objectives, and filtered records
663
+ :rtype Tuple[List[ImplementationObjective], List[Dict[str, str]]]
664
+ """
665
+ security_control = SecurityControl.get_object(implementation.controlID)
666
+ existing_objectives = ImplementationObjective.get_by_control(implementation.id)
667
+ if version == "rev5":
668
+ filtered_records = filter(
669
+ lambda r: extract_control_name(r["cis"]["regscale_control_id"]).lower()
670
+ == security_control.controlId.lower(),
671
+ sheet_data.values(),
672
+ )
673
+ else:
674
+ try:
675
+ control_label = next(
676
+ dat for dat in part_mapper_rev4.data if dat.get("Oscal Control ID") == security_control.controlId
677
+ ).get("CONTROLLABEL")
678
+ except StopIteration:
679
+ control_label = None
680
+ if control_label:
681
+ filtered_records = [r for r in sheet_data.values() if r["cis"]["regscale_control_id"] == control_label]
682
+ else:
683
+ filtered_records = []
684
+
685
+ return existing_objectives, filtered_records
686
+
687
+
688
+ def get_matching_cis_records(control_id: str, cis_data: dict) -> List[Dict[str, str]]:
689
+ """
690
+ Finds matching CIS records for a given control ID.
691
+
692
+ :param str control_id: The control ID to match
693
+ :param dict cis_data: The CIS data to search
694
+ :rtype List[Dict[str, str]]
695
+ :returns A list of matching CIS records
696
+ """
697
+ return [value for value in cis_data.values() if value["regscale_control_id"].lower() == control_id.lower()]
698
+
699
+
700
+ def process_single_record(**kwargs) -> Tuple[List[str], Optional[ImplementationObjective]]:
701
+ """
702
+ Processes a single CIS record and returns updated objective if successful.
703
+
704
+ :rtype Tuple[List[str], Optional[ImplementationObjective]]
705
+ :returns A list of errors and the Implementation Objective if successful, otherwise None
706
+ """
707
+ errors = []
708
+ version = kwargs.get("version")
709
+ leveraged_auth_id: int = kwargs.get("leveraged_auth_id")
710
+ implementation: ControlImplementation = kwargs.get("implementation")
711
+ record: dict = kwargs.get("record")
712
+ control_objectives: List[ControlObjective] = kwargs.get("control_objectives")
713
+ existing_objectives: List[ImplementationObjective] = kwargs.get("existing_objectives")
714
+ mapped_objectives: List[ControlObjective] = []
715
+ result = None
716
+ parts = []
717
+ key = record["cis"]["control_id"]
718
+ if version == "rev5":
719
+ source = part_mapper_rev5.find_by_source(key)
720
+ else:
721
+ source = part_mapper_rev4.find_by_source(key)
722
+ if parts := part_mapper_rev4.find_sub_parts(key):
723
+ for part in parts:
724
+ try:
725
+ mapped_objectives.append(next(obj for obj in control_objectives if obj.name == part))
726
+ except StopIteration:
727
+ errors.append(f"Unable to find part {part} for control {key}")
728
+ if not source and not parts:
729
+ errors.append(f"Unable to find source and part for control {key}")
730
+
731
+ if source and not parts:
732
+ try:
733
+ objective = next(
734
+ obj
735
+ for obj in control_objectives
736
+ if obj.otherId == source and version == "rev5" or obj.name == source and version == "rev4"
737
+ )
738
+ mapped_objectives.append(objective)
739
+ except StopIteration:
740
+ logger.debug(f"Missing Source: {source}")
741
+ errors.append(f"Unable to find objective for control {key} ({source})")
742
+
743
+ if mapped_objectives:
744
+ result = update_imp_objective(
745
+ leverage_auth_id=leveraged_auth_id,
746
+ existing_imp_obj=existing_objectives,
747
+ imp=implementation,
748
+ objectives=mapped_objectives,
749
+ record=record,
750
+ )
751
+
752
+ return errors, result
753
+
754
+
755
+ def parse_crm_worksheet(file_path: click.Path, crm_sheet_name: str, version: Literal["rev4", "rev5"]) -> dict:
756
+ """
757
+ Function to format CRM content.
758
+
759
+ :param click.Path file_path: The file path to the FedRAMP CIS CRM workbook
760
+ :param str crm_sheet_name: The name of the CRM sheet to parse
761
+ :param Literal["rev4", "rev5"] version: The version of the workbook
762
+ :return: Formatted CRM content
763
+ :rtype: dict
764
+ """
765
+ formatted_crm = {}
766
+
767
+ if not crm_sheet_name:
768
+ return formatted_crm
769
+ import pandas as pd # Optimize import performance
770
+
771
+ if version == "rev5":
772
+ skip_rows = 2
773
+ else:
774
+ skip_rows = 3
775
+
776
+ data = pd.read_excel(
777
+ str(file_path),
778
+ sheet_name=crm_sheet_name,
779
+ skiprows=skip_rows,
780
+ usecols=[
781
+ CONTROL_ID,
782
+ "Can Be Inherited from CSP",
783
+ "Specific Inheritance and Customer Agency/CSP Responsibilities",
784
+ ],
785
+ )
786
+
787
+ # Filter rows where "Can Be Inherited from CSP" is not equal to "No"
788
+ exclude_no = data[data[CAN_BE_INHERITED_CSP] != "No"]
789
+
790
+ # Iterate through each row and add to the dictionary
791
+ for _, row in exclude_no.iterrows():
792
+ control_id = row[CONTROL_ID]
793
+
794
+ # Convert camel case to snake case, remove special characters, and convert to lowercase
795
+ clean_control_id = re.sub(r"\W+", "", control_id)
796
+ clean_control_id = re.sub("([a-z0-9])([A-Z])", r"\1_\2", clean_control_id).lower()
797
+
798
+ # Use clean_control_id as the key to avoid overwriting
799
+ formatted_crm[clean_control_id] = {
800
+ "control_id": clean_control_id,
801
+ "control_id_original": control_id,
802
+ "regscale_control_id": transform_control(control_id),
803
+ "can_be_inherited_from_csp": row[CAN_BE_INHERITED_CSP],
804
+ "specific_inheritance_and_customer_agency_csp_responsibilities": row[
805
+ "Specific Inheritance and Customer Agency/CSP Responsibilities"
806
+ ],
807
+ }
808
+
809
+ return formatted_crm
810
+
811
+
812
+ def parse_cis_worksheet(file_path: click.Path, cis_sheet_name: str) -> dict:
813
+ """
814
+ Function to parse and format the CIS worksheet content
815
+
816
+ :param click.Path file_path: The file path to the FedRAMP CIS CRM workbook
817
+ :param str cis_sheet_name: The name of the CIS sheet to parse
818
+ :return: Formatted CIS content
819
+ :rtype: dict
820
+ """
821
+ import pandas as pd # Optimize import performance
822
+
823
+ # Parse the worksheet named 'CIS GovCloud U.S.+DoD (H)', skipping the initial rows
824
+ cis_df = pd.read_excel(file_path, sheet_name=cis_sheet_name, skiprows=2)
825
+
826
+ # Set the appropriate headers
827
+ cis_df.columns = cis_df.iloc[0]
828
+
829
+ # Drop any fully empty rows
830
+ cis_df.dropna(how="all", inplace=True)
831
+
832
+ # Reset the index
833
+ cis_df.reset_index(drop=True, inplace=True)
834
+
835
+ # Rename columns to standardize names
836
+ cis_df.columns = [
837
+ CONTROL_ID,
838
+ "Implemented",
839
+ ControlImplementationStatus.PartiallyImplemented,
840
+ "Planned",
841
+ ALT_IMPLEMENTATION,
842
+ ControlImplementationStatus.NA,
843
+ SERVICE_PROVIDER_CORPORATE,
844
+ SERVICE_PROVIDER_SYSTEM_SPECIFIC,
845
+ SERVICE_PROVIDER_HYBRID,
846
+ CONFIGURED_BY_CUSTOMER,
847
+ CUSTOMER_PROVIDED,
848
+ "Shared Responsibility",
849
+ "Inherited Authorization",
850
+ ]
851
+
852
+ # Fill NaN values with an empty string for processing
853
+ cis_df = cis_df.fillna("")
854
+
855
+ # Function to extract the first non-empty implementation status
856
+ def _extract_status(data_row: pd.Series) -> str:
857
+ """
858
+ Function to extract the first non-empty implementation status from the CIS worksheet
859
+
860
+ :param pd.Series data_row: The data row to extract the status from
861
+ :return: The implementation status
862
+ :rtype: str
863
+ """
864
+ for col in [
865
+ "Implemented",
866
+ ControlImplementationStatus.PartiallyImplemented,
867
+ "Planned",
868
+ ALT_IMPLEMENTATION,
869
+ ControlImplementationStatus.NA,
870
+ ]:
871
+ if data_row[col]:
872
+ return col
873
+ return ""
874
+
875
+ # Function to extract the first non-empty control origination
876
+ def _extract_origination(data_row: pd.Series) -> str:
877
+ """
878
+ Function to extract the first non-empty control origination from the CIS worksheet
879
+
880
+ :param pd.Series data_row: The data row to extract the origination from
881
+ :return: The control origination
882
+ :rtype: str
883
+ """
884
+ selected_origination = []
885
+ for col in [
886
+ SERVICE_PROVIDER_CORPORATE,
887
+ SERVICE_PROVIDER_SYSTEM_SPECIFIC,
888
+ SERVICE_PROVIDER_HYBRID,
889
+ CONFIGURED_BY_CUSTOMER,
890
+ CUSTOMER_PROVIDED,
891
+ "Shared Responsibility",
892
+ "Inherited Authorization",
893
+ ]:
894
+ if data_row[col]:
895
+ selected_origination.append(col)
896
+ return ", ".join(selected_origination) if selected_origination else ""
897
+
898
+ def _process_row(row: pd.Series) -> dict:
899
+ """
900
+ Function to process a row from the CIS worksheet
901
+
902
+ :param pd.Series row: The row to process
903
+ :return: The processed row
904
+ :rtype: dict
905
+ """
906
+ return {
907
+ "control_id": row[CONTROL_ID],
908
+ "regscale_control_id": transform_control(row[CONTROL_ID]),
909
+ "implementation_status": _extract_status(row),
910
+ "control_origination": _extract_origination(row),
911
+ }
912
+
913
+ # use a threadexecutor to process the rows in parallel
914
+ with ThreadPoolExecutor() as executor:
915
+ results = list(executor.map(_process_row, [row for _, row in cis_df.iterrows()]))
916
+
917
+ # iterate the results and index by control_id
918
+ return {result["control_id"]: result for result in results}
919
+
920
+
921
+ def parse_instructions_worksheet(
922
+ file_path: click.Path, version: Literal["rev4", "rev5"], instructions_sheet_name: str = "Instructions"
923
+ ) -> list[dict]:
924
+ """
925
+ Function to parse the instructions sheet from the FedRAMP Rev5 CIS/CRM workbook
926
+
927
+ :param click.Path file_path: The file path to the FedRAMP CIS CRM workbook
928
+ :param Literal["rev4", "rev5"] version: The version of the FedRAMP CIS CRM workbook
929
+ :param str instructions_sheet_name: The name of the instructions sheet to parse, defaults to "Instructions"
930
+ :return: List of formatted instructions content as a dictionary
931
+ :rtype: list[dict]
932
+ """
933
+ import pandas as pd # Optimize import performance
934
+
935
+ instructions_df = pd.read_excel(str(file_path), sheet_name=instructions_sheet_name, skiprows=2)
936
+
937
+ if version == "rev5":
938
+ # Set the appropriate headers
939
+ instructions_df.columns = instructions_df.iloc[0]
940
+ instructions_df = instructions_df[1:]
941
+ relevant_columns = [SYSTEM_NAME, CSP, "System Identifier", IMPACT_LEVEL]
942
+ else:
943
+ for index in range(len(instructions_df)):
944
+ if CSP in instructions_df.iloc[index].values:
945
+ instructions_df.columns = instructions_df.iloc[index]
946
+ instructions_df = instructions_df[index + 1 :]
947
+ break
948
+ # delete the rows before the found row
949
+ relevant_columns = [SYSTEM_NAME, CSP, IMPACT_LEVEL]
950
+ try:
951
+ instructions_df = instructions_df[relevant_columns]
952
+ except KeyError:
953
+ error_and_exit(
954
+ f"Unable to find the relevant columns in the Instructions worksheet. Do you have the correct "
955
+ f"revision set?\nRevision: {version}",
956
+ show_exec=False,
957
+ )
958
+ # convert the dataframe to a dictionary
959
+ return instructions_df.to_dict(orient="records")
960
+
961
+
962
+ def parse_and_map_data(
963
+ leveraged_auth_id: int, api: Api, ssp_id: int, cis_data: dict, crm_data: dict, version: Literal["rev5", "rev4"]
964
+ ) -> None:
965
+ """
966
+ Function to parse and map data from RegScale and the workbook.
967
+
968
+ :param int leveraged_auth_id: The leveraged authorization ID
969
+ :param Api api: RegScale API object
970
+ :param int ssp_id: RegScale SSP ID #
971
+ :param dict cis_data: Parsed CIS data to update the control implementations and objectives
972
+ :param dict crm_data: Parsed CRM data to update the control implementations and objectives
973
+ :param version: Literal["rev4", "rev5", "4", "5"],
974
+ :rtype: None
975
+ """
976
+ with progress:
977
+ implementations = get_all_imps(api=api, ssp_id=ssp_id, cis_data=cis_data, version=version)
978
+ error_set = update_all_objectives(
979
+ leveraged_auth_id=leveraged_auth_id,
980
+ cis_data=cis_data,
981
+ crm_data=crm_data,
982
+ control_implementations=implementations,
983
+ version=version,
984
+ )
985
+
986
+ report(error_set)
987
+
988
+
989
+ def extract_control_name(control_string: str) -> str:
990
+ """
991
+ Extracts the control name (e.g., 'AC-20(1)') from a given string.
992
+
993
+ :param str control_string: The string to extract the control name from
994
+ :return: The extracted control name
995
+ :rtype: str
996
+ """
997
+ pattern = r"^[A-Za-z]{2}-\d{1,3}(?:\(\d+\))?"
998
+ match = re.match(pattern, control_string.upper())
999
+ return match.group() if match else ""
1000
+
1001
+
1002
+ def rev_4_map(control_id: str) -> Optional[str]:
1003
+ """
1004
+ Maps a control ID to its corresponding revision 4 control ID.
1005
+
1006
+ :param str control_id: The control ID to map
1007
+ :return: The mapped control ID or None if not found
1008
+ :rtype: Optional[str]
1009
+ """
1010
+ # Regex pattern to match different control ID formats
1011
+ pattern = r"^([A-Z]{2})-(\d{2})\s*(?:\((\d{2})\))?\s*(?:\(([a-z])\))?$"
1012
+
1013
+ match = re.match(pattern, control_id, re.IGNORECASE)
1014
+
1015
+ if not match:
1016
+ return None
1017
+
1018
+ # Extract components
1019
+ prefix, number, subnum, letter = match.groups()
1020
+
1021
+ # Convert to lowercase
1022
+ prefix = prefix.lower()
1023
+
1024
+ # Construct statement ID
1025
+ if subnum:
1026
+ # With sub-number
1027
+ base_id = f"{prefix}-{number}.{int(subnum)}_smt"
1028
+ return f"{base_id}{f'.{letter}' if letter else ''}"
1029
+ else:
1030
+ # Without sub-number
1031
+ base_id = f"{prefix}-{number}_smt"
1032
+ return f"{base_id}{f'.{letter}' if letter else ''}"
1033
+
1034
+
1035
+ def parse_and_import_ciscrm(
1036
+ file_path: click.Path,
1037
+ version: Literal["rev4", "rev5", "4", "5"],
1038
+ cis_sheet_name: str,
1039
+ crm_sheet_name: str,
1040
+ regscale_ssp_id: int,
1041
+ leveraged_auth_id: int = 0,
1042
+ ) -> None:
1043
+ """
1044
+ Parse and import the FedRAMP Rev5 CIS/CRM Workbook into a RegScale System Security Plan
1045
+
1046
+ :param click.Path file_path: The file path to the FedRAMP CIS CRM .xlsx file
1047
+ :param Literal["rev4", "rev5"] version: FedRAMP revision version
1048
+ :param str cis_sheet_name: CIS sheet name in the FedRAMP CIS CRM .xlsx to parse
1049
+ :param str crm_sheet_name: CRM sheet name in the FedRAMP CIS CRM .xlsx to parse
1050
+ :param int regscale_ssp_id: The ID number from RegScale of the System Security Plan
1051
+ :param int leveraged_auth_id: RegScale Leveraged Authorization ID #, if none provided, one will be created
1052
+ :raises ValueError: If the SSP with the given ID is not found in RegScale
1053
+ :rtype: None
1054
+ """
1055
+ sys_name_key = "System Name"
1056
+ api = Api()
1057
+ ssp: SecurityPlan = SecurityPlan.get_object(regscale_ssp_id)
1058
+ if not ssp:
1059
+ raise ValueError(f"SSP with ID {regscale_ssp_id} not found in RegScale.")
1060
+
1061
+ if "5" in version:
1062
+ version = "rev5"
1063
+ part_mapper_rev5.load_fedramp_version_5_mapping()
1064
+ else:
1065
+ version = "rev4"
1066
+ part_mapper_rev4.load_fedramp_version_4_mapping()
1067
+ # parse the instructions worksheet to get the csp name, system name, and other data
1068
+ instructions_data = parse_instructions_worksheet(file_path=file_path, version=version) # type: ignore
1069
+
1070
+ # get the system names from the instructions data by dropping any non-string values
1071
+ system_names = [entry[sys_name_key] for entry in instructions_data if isinstance(entry[sys_name_key], str)]
1072
+ name_match: str = system_names[0]
1073
+
1074
+ # update the instructions data to the matched system names
1075
+ instructions_data = [
1076
+ (
1077
+ entry
1078
+ if isinstance(entry[sys_name_key], str)
1079
+ and entry[sys_name_key] == name_match
1080
+ or entry[sys_name_key] == ssp.systemName
1081
+ else None
1082
+ )
1083
+ for entry in instructions_data
1084
+ ]
1085
+ # remove any None values from the instructions data
1086
+ instructions_data = [entry for entry in instructions_data if entry][0]
1087
+ if not any(instructions_data):
1088
+ raise ValueError("Unable to parse data from Instructions sheet.")
1089
+
1090
+ # start parsing the workbook
1091
+ cis_data = parse_cis_worksheet(file_path=file_path, cis_sheet_name=cis_sheet_name)
1092
+ crm_data = {}
1093
+ if crm_sheet_name:
1094
+ crm_data = parse_crm_worksheet(
1095
+ file_path=file_path, crm_sheet_name=crm_sheet_name, version=version # type: ignore
1096
+ )
1097
+ if leveraged_auth_id == 0:
1098
+ auths = LeveragedAuthorization.get_all_by_parent(ssp.id)
1099
+ if auths:
1100
+ leveraged_auth_id = next((auth.id for auth in auths))
1101
+ else:
1102
+ leveraged_auth_id = new_leveraged_auth(
1103
+ ssp=ssp,
1104
+ user_id=api.config["userId"],
1105
+ instructions_data=instructions_data,
1106
+ version=version, # type: ignore
1107
+ )
1108
+ # Update objectives using the mapped data using threads
1109
+ parse_and_map_data(
1110
+ leveraged_auth_id=leveraged_auth_id,
1111
+ api=api,
1112
+ ssp_id=regscale_ssp_id,
1113
+ cis_data=cis_data,
1114
+ crm_data=crm_data,
1115
+ version=version, # type: ignore
1116
+ )
1117
+
1118
+ # upload workbook to the SSP
1119
+ File.upload_file_to_regscale(
1120
+ file_name=str(file_path),
1121
+ parent_id=regscale_ssp_id,
1122
+ parent_module="securityplans",
1123
+ api=api,
1124
+ )