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