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