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,1117 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """CrowdStrike RegScale integration"""
4
+ import json
5
+ import os
6
+ import sys
7
+ from enum import Enum
8
+ from typing import Callable, Optional, Union, Dict
9
+ from urllib.parse import urljoin
10
+
11
+ import click
12
+ from falconpy import Incidents, Intel, OAuth2, UserManagement
13
+ from rich.console import Console
14
+ from rich.progress import track
15
+ from rich.table import Table
16
+
17
+ from regscale.core.app.api import Api
18
+ from regscale.core.app.application import Application
19
+ from regscale.core.app.logz import create_logger
20
+ from regscale.core.app.utils.app_utils import (
21
+ error_and_exit,
22
+ format_data_to_html,
23
+ get_current_datetime,
24
+ )
25
+ from regscale.core.app.utils.app_utils import remove_keys
26
+ from regscale.core.app.utils.regscale_utils import verify_provided_module
27
+ from regscale.models import regscale_id, regscale_module, regscale_models
28
+ from regscale.models.regscale_models import Catalog
29
+ from regscale.models.regscale_models import (
30
+ ControlImplementation,
31
+ ControlImplementationStatus,
32
+ )
33
+ from regscale.models.regscale_models.asset import Asset
34
+ from regscale.models.regscale_models.incident import Incident
35
+
36
+ #####################################################################################################
37
+ #
38
+ # CrowdStrike API Documentation: https://dash.readme.com/to/crowdstrike-enterprise?redirect=%2Fcrowdstrike%2Fdocs
39
+
40
+ # Sync incidents from CrowdStrike EDR into RegScale
41
+
42
+ # Allow customer to set severity level of what level of alerts they want to sync, set via init.yaml
43
+
44
+ # Check to make sure alert does not already exist, if it does, update with latest info, if it doesn't, create a new one
45
+
46
+ # Ensure you can link back to the alert in CrowdStrike
47
+
48
+ # Get with Jim Townsend on access to a DEV environment or customer sandbox
49
+
50
+ #####################################################################################################
51
+
52
+ logger = create_logger()
53
+ console = Console()
54
+ project_dir = os.getcwd()
55
+
56
+
57
+ class Status(Enum):
58
+ """Enum used to describe status values."""
59
+
60
+ NEW = 20
61
+ REOPENED = 25
62
+ INPROGRESS = 30
63
+ CLOSED = 40
64
+
65
+
66
+ class StatusColor(Enum):
67
+ """Enum to describe colors used for status displays."""
68
+
69
+ NEW = "[cornsilk1]"
70
+ REOPENED = "[bright_yellow]"
71
+ INPROGRESS = "[deep_sky_blue1]"
72
+ CLOSED = "[bright_green]"
73
+
74
+
75
+ @click.group()
76
+ def crowdstrike():
77
+ """[BETA] CrowdStrike Integration to load threat intelligence to RegScale."""
78
+
79
+
80
+ @crowdstrike.command(name="query_incidents")
81
+ @regscale_id(help="RegScale will create and update issues as children of this record.")
82
+ @regscale_module()
83
+ @click.option(
84
+ "--filter",
85
+ type=click.STRING,
86
+ default=None,
87
+ hide_input=False,
88
+ required=False,
89
+ help="Falcon Query Language(FQL) string.",
90
+ )
91
+ def query_incidents(regscale_id: int, regscale_module: str, filter: str, limit=500) -> None:
92
+ """Query Incidents from CrowdStrike."""
93
+ query_crowdstrike_incidents(regscale_id, regscale_module, filter, limit)
94
+
95
+
96
+ def determine_incident_level(fine_score: int) -> str:
97
+ """
98
+ Determine the incident level based on a fine_score
99
+
100
+ :param int fine_score: The fine_score as an integer
101
+ :return: The incident level as a string
102
+ :rtype: str
103
+ """
104
+ # Convert fine_score to the displayed score
105
+ displayed_score = fine_score / 10.0
106
+
107
+ if displayed_score >= 10.0:
108
+ return "S1 - High Severity"
109
+ elif displayed_score >= 8.0:
110
+ return "S1 - High Severity"
111
+ elif displayed_score >= 6.0:
112
+ return "S2 - Moderate Severity"
113
+ elif displayed_score >= 4.0:
114
+ return "S3 - Low Severity"
115
+ elif displayed_score >= 2.0:
116
+ return "S4 - Non-Incident"
117
+ else:
118
+ return "S5 - Uncategorized"
119
+
120
+
121
+ def map_status_to_phase(status_code: int) -> str:
122
+ """Map a CrowdStrike status code to a RegScale phase
123
+
124
+ :param int status_code: The status code from CrowdStrike as an integer.
125
+ :return: The corresponding phase in RegScale as a string.
126
+ :rtype: str
127
+ """
128
+ crowdstrike_to_regcale_mapping = {
129
+ 20: "Detection",
130
+ 25: "Analysis",
131
+ 30: "Containment",
132
+ 40: "Closed",
133
+ }
134
+
135
+ return crowdstrike_to_regcale_mapping.get(status_code, "Analysis")
136
+
137
+
138
+ def create_properties_for_incident(device_data: dict, regscale_id: int) -> None:
139
+ """Create properties in RegScale based on the given device data dictionary
140
+
141
+ :param dict device_data: The device data as a dictionary.
142
+ :param int regscale_id: The parent ID of the incidents.
143
+ :rtype: None
144
+ """
145
+ # Simulate RegScale API or database connection
146
+ properties = []
147
+ api = Api()
148
+ domain = api.config.get("domain")
149
+ url = urljoin(domain, "/api/properties/batchCreate")
150
+ for key, value in device_data.items():
151
+ # Skip list pieces
152
+ if not isinstance(value, list):
153
+ # Create property in RegScale (simulated)
154
+ prop = {
155
+ "isPublic": "true",
156
+ "key": key,
157
+ "value": value,
158
+ "parentId": regscale_id,
159
+ "parentModule": "incidents",
160
+ }
161
+ properties.append(prop)
162
+ response = api.post(url=url, json=properties)
163
+ if not response.ok:
164
+ logger.error("Failed to create property.")
165
+
166
+
167
+ def get_existing_regscale_incidents(parent_id: int, parent_module: str) -> list[dict]:
168
+ """Get existing RegScale incidents for the given parent ID and parent module
169
+
170
+ :param int parent_id: The parent ID of the incidents.
171
+ :param str parent_module: The parent module of the incidents.
172
+ :return: The existing incidents as a list of dictionaries.
173
+ :rtype: list[dict]
174
+ """
175
+ existing_incidents = Incident.get_all_by_parent(parent_id, parent_module)
176
+ return [incident.dict() for incident in existing_incidents]
177
+
178
+
179
+ def incidents_exist(title: str, existing_incidents: list[dict]) -> bool:
180
+ """Determine if an incident already exists in RegScale
181
+
182
+ :param str title: The title of the incident.
183
+ :param list[dict] existing_incidents: The existing incidents as a list of dictionaries.
184
+ :return: If the incident already exists in RegScale
185
+ :rtype: bool
186
+ """
187
+ if existing_incidents:
188
+ for existing_incident in existing_incidents:
189
+ if existing_incident["title"] == title:
190
+ logger.info(f"Incident {existing_incident['title']} already exists in RegScale.")
191
+ return True
192
+ return False
193
+
194
+
195
+ def create_regscale_incidents(incidents: list[dict], regscale_id: int, regscale_module: str) -> None:
196
+ """
197
+ Create Incidents in RegScale based on given Falcon incidents.
198
+
199
+ :param list[dict] incidents: List of Falcon Incident dictionaries
200
+ :param int regscale_id: RegScale parent ID
201
+ :param str regscale_module: RegScale parent module
202
+ :rtype: None
203
+ """
204
+
205
+ app = Application()
206
+ existing_incidents = get_existing_regscale_incidents(regscale_id, regscale_module)
207
+ logger.info(f"Found {len(existing_incidents)} existing incidents.")
208
+ user_id = app.config.get("userId")
209
+
210
+ for incident in track(
211
+ incidents,
212
+ description="[#f8b737]Comparing Crowdstrike and RegScale incident(s)...",
213
+ ):
214
+ title = f"CrowdStrike incidentId: {incident['incident_id']}"
215
+
216
+ if not incidents_exist(title, existing_incidents):
217
+ regscale_incident = create_regscale_incident(incident, regscale_id, regscale_module, user_id)
218
+ post_incident_and_associate_properties(regscale_incident, incident, user_id)
219
+
220
+
221
+ def create_regscale_incident(
222
+ incident: dict,
223
+ regscale_id: int,
224
+ regscale_module: str,
225
+ user_id: Optional[str] = None,
226
+ ) -> Incident:
227
+ """
228
+ Creates a RegScale incident object from a Falcon incident
229
+
230
+ :param dict incident: Falcon incident dictionary
231
+ :param int regscale_id: RegScale parent ID
232
+ :param str regscale_module: RegScale parent module
233
+ :param Optional[str] user_id: User ID of the user performing the operation, defaults to None
234
+ :return: RegScale incident object
235
+ :rtype: Incident
236
+ """
237
+ severity = determine_incident_level(incident["fine_score"])
238
+ source_cause = ", ".join(incident.get("techniques", "")) if incident else ""
239
+
240
+ return Incident(
241
+ title=f"CrowdStrike incidentId: {incident['incident_id']}",
242
+ severity=severity,
243
+ sourceCause=source_cause,
244
+ category="CAT 6 - Investigation",
245
+ phase=map_status_to_phase(incident["status"]),
246
+ description=format_data_to_html(incident), # Assuming this function formats the data to HTML
247
+ detectionMethod="Intrusion Detection System",
248
+ incidentPOCId=user_id,
249
+ dateDetected=incident["created"],
250
+ parentId=regscale_id if regscale_id is not None else None,
251
+ parentModule=regscale_module if regscale_module is not None else None,
252
+ lastUpdatedById=user_id,
253
+ dateCreated=incident["created"],
254
+ dateLastUpdated=get_current_datetime(),
255
+ dateResolved=(
256
+ incident["end"] if "end" in incident and map_status_to_phase(incident["status"]) == "Closed" else None
257
+ ),
258
+ createdById=user_id,
259
+ )
260
+
261
+
262
+ def post_incident_and_associate_properties(
263
+ regscale_incident: Incident, incident: dict, user_id: Optional[str] = None
264
+ ) -> None:
265
+ """
266
+ Posts the incident to RegScale and associates related properties
267
+
268
+ :param Incident regscale_incident: RegScale incident object
269
+ :param dict incident: Falcon incident dictionary
270
+ :param Optional[str] user_id: ID of the user performing the operation, defaults to None
271
+ :rtype: None
272
+ """
273
+ response = Incident.post_incident(regscale_incident)
274
+ if response.ok:
275
+ incident_id = response.json()["id"]
276
+ create_and_associate_incident_properties(incident, incident_id, user_id)
277
+ logger.info(f"Created Incident: {regscale_incident.title} with ID: {incident_id}")
278
+ else:
279
+ response.raise_for_status()
280
+ logger.error(f"Failed to create Incident: {regscale_incident.title} in RegScale.")
281
+
282
+
283
+ def create_and_associate_incident_properties(incident: dict, incident_id: int, user_id: Optional[str] = None) -> None:
284
+ """
285
+ Creates and associates properties for a RegScale incident
286
+
287
+ :param dict incident: Falcon incident dictionary
288
+ :param int incident_id: ID of the RegScale incident
289
+ :param Optional[str] user_id: ID of the user performing the operation, defaults to None
290
+ :rtype: None
291
+ """
292
+ for host in incident.get("hosts", []):
293
+ create_asset(data=host, parent_id=incident_id, parent_module="incidents", user_id=user_id)
294
+
295
+ for tactic in incident.get("tactics", []):
296
+ create_properties_for_incident({"tactic": tactic}, incident_id)
297
+
298
+ for technique in incident.get("techniques", []):
299
+ create_properties_for_incident({"technique": technique}, incident_id)
300
+
301
+ for objective in incident.get("objectives", []):
302
+ create_properties_for_incident({"objective": objective}, incident_id)
303
+
304
+ for user in incident.get("users", []):
305
+ create_properties_for_incident({"user": user}, incident_id)
306
+
307
+
308
+ def create_asset(data: dict, parent_id: int, parent_module: str, user_id: str) -> Dict:
309
+ """
310
+ Create an asset in RegScale
311
+
312
+ :param dict data: The asset data as a dictionary
313
+ :param int parent_id: The parent ID in RegScale
314
+ :param str parent_module: The parent module in RegScale
315
+ :param str user_id: The user ID in RegScale
316
+ :return: Asset object as a dictionary
317
+ :rtype: Dict
318
+ """
319
+ device_id = data.get("device_id", "")
320
+ asset = Asset(
321
+ parentId=parent_id,
322
+ parentModule=parent_module,
323
+ name=f'{data.get("hostname", device_id)} - {data.get("system_product_name", "Asset")}',
324
+ description=data.get("product_type_desc", None),
325
+ ipAddress=data.get("external_ip", None),
326
+ macAddress=data.get("mac_address", None),
327
+ manufacturer=data.get("bios_manufacturer", None),
328
+ model=data.get("bios_version", None),
329
+ serialNumber=data.get("serial-number", None),
330
+ assetCategory=regscale_models.AssetCategory.Hardware,
331
+ assetType="Desktop",
332
+ fqdn=data.get("fqdn", None),
333
+ notes=data.get("remarks", None),
334
+ operatingSystem=data.get("os_version", None),
335
+ osVersion=(
336
+ f"{data.get('major_version', None)}.{data.get('minor_version', None)}"
337
+ if data.get("major_version", None)
338
+ else None
339
+ ),
340
+ netBIOS=data.get("netbios-name", None),
341
+ iPv6Address=data.get("ipv6-address", None),
342
+ ram=0,
343
+ diskStorage=0,
344
+ cpu=0,
345
+ assetOwnerId=user_id,
346
+ status="Active (On Network)",
347
+ isPublic=True,
348
+ dateCreated=get_current_datetime(),
349
+ dateLastUpdated=get_current_datetime(),
350
+ ).create()
351
+ return asset.dict()
352
+
353
+
354
+ def query_crowdstrike_incidents(regscale_id: int, regscale_module: str, filter: str, limit: int = 500) -> None:
355
+ """
356
+ Query Incidents from CrowdStrike
357
+
358
+ :param int regscale_id: RegScale parent ID
359
+ :param str regscale_module: RegScale parent module
360
+ :param str filter: Falcon Query Language Filter
361
+ :param int limit: Record limit, 1-500, defaults to 500
362
+ :rtype: None
363
+ """
364
+ incident_list = []
365
+ avail = True
366
+ offset = 500
367
+ while avail:
368
+ incidents = open_sdk().query_incidents(filter=filter, limit=limit, offset=offset)
369
+ logger.info(f"Found {len(incidents['body']['resources'])} incidents.")
370
+ if incidents["status_code"] != 200:
371
+ error_and_exit(incidents["body"]["errors"][0])
372
+ if not incidents["body"]["resources"]:
373
+ avail = False
374
+ else:
375
+ offset += limit
376
+ incident_list.extend(incidents["body"]["resources"])
377
+ if incident_list:
378
+ create_regscale_incidents(incident_list, regscale_id, regscale_module)
379
+
380
+
381
+ def open_sdk() -> Optional[Incidents]:
382
+ """
383
+ Function to create an instance of the Crowdstrike SDK
384
+
385
+ :return: Incidents object
386
+ :rtype: Optional[Incidents]
387
+ """
388
+ app = Application()
389
+ falcon_client_id = app.config.get("crowdstrikeClientId")
390
+ falcon_client_secret = app.config.get("crowdstrikeClientSecret")
391
+ try:
392
+ inc_object = Incidents(client_id=falcon_client_id, client_secret=falcon_client_secret)
393
+ if inc_object is not None:
394
+ logger.info("Successfully created Crowdstrike client.")
395
+ return inc_object
396
+ else:
397
+ logger.error("Unable to create Crowdstrike object.")
398
+ except AttributeError as aex:
399
+ logger.error(aex)
400
+ if str(aex) == """'str' object has no attribute 'authenticated'""":
401
+ error_and_exit("Unable to Authenticate with CrowdStrike API. Please check your credentials.")
402
+
403
+
404
+ def users() -> UserManagement:
405
+ """
406
+ Create instances of our two Service Classes and returns them
407
+
408
+ :return: UserManagement object
409
+ :rtype: UserManagement
410
+ """
411
+ return UserManagement(auth_object=open_sdk())
412
+
413
+
414
+ def get_incident_ids(sdk: Incidents, filter_string: Optional[str]) -> list:
415
+ """
416
+ Retrieve all available incident IDs from Crowdstrike
417
+
418
+ :param Incidents sdk: Crowdstrike SDK object
419
+ :param Optional[str] filter_string: Filter string to use for query
420
+ :raises General Error: If unable to retrieve incident IDs from Crowdstrike
421
+ :return: List of incident IDs
422
+ :rtype: list
423
+ """
424
+ params = {}
425
+ if filter_string:
426
+ params = {"filter": filter_string}
427
+ incident_id_lookup = sdk.query_incidents(**params)
428
+ if incident_id_lookup["status_code"] != 200:
429
+ error_and_exit(incident_id_lookup["body"]["errors"][0])
430
+ if not incident_id_lookup["body"]["resources"]:
431
+ logger.warning("No incidents found.")
432
+
433
+ return incident_id_lookup["body"]["resources"]
434
+
435
+
436
+ def get_incident_data(id_list: list, sdk: Incidents) -> list[dict]:
437
+ """Retrieve incident details using the IDs provided
438
+
439
+ :param list id_list: List of incident IDs from Crowdstrike
440
+ :param Incidents sdk: Crowdstrike SDK object
441
+ :raises General Error: If unable to retrieve incident details from Crowdstrike
442
+ :return: List of incident details
443
+ :rtype: list
444
+ """
445
+ incident_detail_lookup = sdk.get_incidents(ids=id_list)
446
+ if incident_detail_lookup["status_code"] != 200:
447
+ error_and_exit(incident_detail_lookup["body"]["errors"][0])
448
+ return incident_detail_lookup["body"]["resources"]
449
+
450
+
451
+ def tagging(inc_id: str, tags: list, untag: bool = False) -> None:
452
+ """Assign or remove all tags provided
453
+
454
+ :param str inc_id: Incident ID to tag
455
+ :param list tags: List of tags to assign
456
+ :param bool untag: Flag to remove tags instead of assign, defaults to False
457
+ :rtype: None
458
+ """
459
+ sdk = open_sdk()
460
+ action = {"ids": get_incident_full_id(inc_id)}
461
+ if untag:
462
+ action["delete_tag"] = tags
463
+ else:
464
+ action["add_tag"] = tags
465
+ change_result = sdk.perform_incident_action(**action)
466
+ if change_result["status_code"] != 200:
467
+ error_and_exit(change_result["body"]["errors"][0])
468
+
469
+
470
+ def get_user_detail(uuid: str) -> str:
471
+ """Retrieve assigned to user information for tabular display
472
+
473
+ :param str uuid: User ID to retrieve information for in CrowdStrike
474
+ :return: User information
475
+ :rtype: str
476
+ """
477
+ lookup_result = users.retrieve_user(ids=uuid)
478
+ if lookup_result["status_code"] != 200:
479
+ error_and_exit(lookup_result["body"]["errors"][0])
480
+ user_info = lookup_result["body"]["resources"][0]
481
+ first = user_info["firstName"]
482
+ last = user_info["lastName"]
483
+ uid = user_info["uid"]
484
+
485
+ return f"{first} {last} ({uid})"
486
+
487
+
488
+ def get_incident_full_id(partial: str) -> Union[str, bool]:
489
+ """
490
+ Retrieve the full incident ID based off of the partial ID provided
491
+
492
+ :param str partial: Partial incident ID to search for
493
+ :raises General Error: If api call != 200
494
+ :raises General Error: If unable to find incident ID
495
+ :return: Full incident ID
496
+ :rtype: Union[str, bool]
497
+ """
498
+ sdk = open_sdk()
499
+ search_result = sdk.query_incidents()
500
+ if search_result["status_code"] != 200:
501
+ error_and_exit(search_result["body"]["errors"][0])
502
+ found = False
503
+ for inc in search_result["body"]["resources"]:
504
+ incnum = inc.split(":")[2]
505
+ if incnum == partial:
506
+ found = inc
507
+ break
508
+
509
+ if not found:
510
+ error_and_exit("Unable to find incident ID specified.")
511
+
512
+ return found
513
+
514
+
515
+ def assignment(inc_id: str, assign_to: str = "", unassign: bool = False) -> None:
516
+ """
517
+ Assign the incident specified to the user specified
518
+
519
+ :param str inc_id: Incident ID to assign
520
+ :param str assign_to: User ID to assign incident to
521
+ :param bool unassign: Flag to unassign incident, defaults to False
522
+ :raises General Error: If API Call != 200
523
+ :rtype: None
524
+ """
525
+ sdk = open_sdk()
526
+ if unassign:
527
+ change_result = sdk.perform_incident_action(ids=get_incident_full_id(inc_id), unassign=True)
528
+ if change_result["status_code"] != 200:
529
+ error_and_exit(change_result["body"]["errors"][0])
530
+ else:
531
+ lookup_result = users.retrieve_user_uuid(uid=assign_to)
532
+
533
+ if lookup_result["status_code"] != 200:
534
+ error_and_exit(lookup_result["body"]["errors"][0])
535
+ change_result = sdk.perform_incident_action(
536
+ ids=get_incident_full_id(inc_id),
537
+ update_assigned_to_v2=lookup_result["body"]["resources"][0],
538
+ )
539
+ if change_result["status_code"] != 200:
540
+ error_and_exit(change_result["body"]["errors"][0])
541
+
542
+
543
+ def status_information(inc_data: dict) -> str:
544
+ """
545
+ Parse status information for tabular display
546
+
547
+ :param dict inc_data: Incident data to parse
548
+ :return: Status information
549
+ :rtype: str
550
+ """
551
+ inc_status = [
552
+ f"{StatusColor[Status(inc_data['status']).name].value}"
553
+ f"{Status(inc_data['status']).name.title().replace('Inp', 'InP')}[/]"
554
+ ]
555
+ tag_list = inc_data.get("tags", [])
556
+ if tag_list:
557
+ inc_status.append(" ")
558
+ tag_list = [f"[magenta]{tg}[/]" for tg in tag_list]
559
+ inc_status.extend(tag_list)
560
+
561
+ return "\n".join(inc_status)
562
+
563
+
564
+ def incident_information(inc_data: dict) -> str:
565
+ """
566
+ Parse incident overview information for tabular display
567
+
568
+ :param dict inc_data: Incident data to parse
569
+ :return: Incident overview information
570
+ :rtype: str
571
+ """
572
+ inc_info = []
573
+ inc_info.append(inc_data.get("name", ""))
574
+ inc_info.append(f"[bold]{inc_data['incident_id'].split(':')[2]}[/]")
575
+ inc_info.append(f"Start: {inc_data.get('start', 'Unknown').replace('T', ' ')}")
576
+ inc_info.append(f" End: {inc_data.get('end', 'Unknown').replace('T', ' ')}")
577
+ if assigned := inc_data.get("assigned_to"):
578
+ inc_info.append("\n[underline]Assignment[/]")
579
+ inc_info.append(get_user_detail(assigned))
580
+ if inc_data.get("description"):
581
+ inc_info.append(" ")
582
+ inc_info.append(chunk_long_description(inc_data["description"], 50))
583
+
584
+ return "\n".join(inc_info)
585
+
586
+
587
+ def chunk_long_description(desc: str, col_width: int) -> str:
588
+ """
589
+ Chunk a long string by delimiting with CR based upon column length
590
+
591
+ :param str desc: Description to parse
592
+ :param int col_width: Column width to chunk by
593
+ :return: Chunked description
594
+ :rtype: str
595
+ """
596
+ desc_chunks = []
597
+ chunk = ""
598
+ for word in desc.split():
599
+ new_chunk = f"{chunk}{word.strip()} "
600
+ if len(new_chunk) >= col_width:
601
+ desc_chunks.append(new_chunk)
602
+ chunk = ""
603
+ else:
604
+ chunk = new_chunk
605
+
606
+ delim = "\n"
607
+ desc_chunks.append(chunk)
608
+
609
+ return delim.join(desc_chunks)
610
+
611
+
612
+ def hosts_information(inc_data: dict) -> str:
613
+ """
614
+ Parse hosts information for tabular display
615
+
616
+ :param dict inc_data: Incident data to parse
617
+ :return: Host information
618
+ :rtype: str
619
+ """
620
+ returned = ""
621
+ if "hosts" in inc_data:
622
+ host_str = []
623
+ for host in inc_data["hosts"]:
624
+ host_info = []
625
+ host_info.append(
626
+ f"<strong>{host.get('hostname', 'Unidentified')}</strong>"
627
+ f" ({host.get('platform_name', 'Not available')})"
628
+ )
629
+ host_info.append(f"<span style='color:cyan'>{host.get('device_id', 'Not available')}</span>")
630
+ host_info.append(f" Int: {host.get('local_ip', 'Not available')}")
631
+ host_info.append(f" Ext: {host.get('external_ip', 'Not available')}")
632
+ first = host.get("first_seen", "Unavailable").replace("T", " ").replace("Z", " ")
633
+ host_info.append(f"First: {first}")
634
+ last = host.get("last_seen", "Unavailable").replace("T", " ").replace("Z", " ")
635
+ host_info.append(f" Last: {last}")
636
+ host_str.append("\n".join(host_info))
637
+ if host_str:
638
+ returned = "\n".join(host_str)
639
+ else:
640
+ returned = "Unidentified"
641
+
642
+ return returned
643
+
644
+
645
+ def show_incident_table(incident_listing: list) -> None:
646
+ """
647
+ Display all returned incidents in tabular fashion
648
+
649
+ :param list incident_listing: List of incidents to parse and print to console
650
+ :raises General Error: If incident_listing is empty
651
+ :rtype: None
652
+ """
653
+ if not incident_listing:
654
+ error_and_exit("No incidents found, code 404")
655
+ table = Table(show_header=True, header_style="bold magenta", title="Incidents")
656
+ headers = {
657
+ "status": "[bold]Status[/] ",
658
+ "incident": "[bold]Incident[/]",
659
+ "hostname": "[bold]Host[/]",
660
+ "tactics": "[bold]Tactics[/]",
661
+ "techniques": "[bold]Techniques[/]",
662
+ "objectives": "[bold]Objective[/]s",
663
+ }
664
+ for value in headers.values():
665
+ table.add_column(value, justify="left")
666
+ for inc in incident_listing:
667
+ inc_detail = {"status": status_information(inc)}
668
+ inc_detail["incident"] = incident_information(inc)
669
+ inc_detail["hostname"] = hosts_information(inc)
670
+ inc_detail["tactics"] = "\n".join(inc["tactics"])
671
+ inc_detail["techniques"] = "\n".join(inc["techniques"])
672
+ inc_detail["objectives"] = "\n".join(inc["objectives"])
673
+ table.add_row(
674
+ inc_detail["status"],
675
+ inc_detail["incident"],
676
+ inc_detail["hostname"],
677
+ inc_detail["tactics"],
678
+ inc_detail["techniques"],
679
+ inc_detail["objectives"],
680
+ )
681
+ console.print(table)
682
+
683
+
684
+ def get_token() -> str:
685
+ """
686
+ Get the token for the CrowdStrike API
687
+
688
+ :raises General Error: If unable to authenticate with CrowdStrike via API
689
+ :return: CrowdStrike API token
690
+ :rtype: str
691
+ """
692
+ app = Application()
693
+ falcon_client_id = app.config.get("crowdstrikeClientId")
694
+ falcon_client_secret = app.config.get("crowdstrikeClientSecret")
695
+ falcon_url = app.config.get("crowdstrikeBaseUrl")
696
+
697
+ if not falcon_client_id:
698
+ falcon_client_id = click.prompt("Please provide your Falcon Client API Key", hide_input=True)
699
+ if not falcon_client_secret:
700
+ falcon_client_secret = click.prompt("Please provide your Falcon Client API Secret", hide_input=True)
701
+ auth = OAuth2(
702
+ client_id=falcon_client_id,
703
+ client_secret=falcon_client_secret,
704
+ base_url=falcon_url,
705
+ )
706
+ # Generate a token
707
+ auth.token()
708
+ if auth.token_status != 201:
709
+ raise error_and_exit("Unable to authenticate with Crowdstrike!")
710
+ return auth.token_value
711
+
712
+
713
+ @crowdstrike.command(name="sync_incidents")
714
+ @regscale_id(help="RegScale will create and update incidents as children of this record.")
715
+ @regscale_module()
716
+ def sync_incidents(regscale_id: int, regscale_module: str):
717
+ """Sync Incidents and Assets from CrowdStrike to RegScale."""
718
+ sync_incidents_to_regscale(regscale_id, regscale_module)
719
+
720
+
721
+ def sync_incidents_to_regscale(regscale_id: int, regscale_module: str) -> None:
722
+ """
723
+ Sync Incidents and Assets from CrowdStrike to RegScale
724
+
725
+ :param int regscale_id: RegScale record ID
726
+ :param str regscale_module: RegScale Module
727
+ :rtype: None
728
+ """
729
+ verify_provided_module(regscale_module)
730
+ sdk = open_sdk()
731
+ incident_id_list = get_incident_ids(filter_string=None, sdk=sdk)
732
+ if not incident_id_list:
733
+ error_and_exit("No incidents found!")
734
+ incidents = get_incident_data(id_list=incident_id_list, sdk=sdk)
735
+ logger.info(f"Found {len(incidents)} incidents to sync.")
736
+ create_regscale_incidents(incidents=incidents, regscale_id=regscale_id, regscale_module=regscale_module)
737
+
738
+
739
+ def get_intel() -> Intel:
740
+ """
741
+ Get the Intel SDK object
742
+
743
+ :return: Intel SDK object
744
+ :rtype: Intel
745
+ """
746
+ app = Application()
747
+ client_id = app.config.get("crowdstrikeClientId")
748
+ client_secret = app.config.get("crowdstrikeClientSecret")
749
+ intel = Intel(
750
+ client_id=client_id,
751
+ client_secret=client_secret,
752
+ )
753
+ return intel
754
+
755
+
756
+ def get_vulnerability_ids(intel, limit=100) -> list:
757
+ """Fetch ID's from CrowdStrike for Intel Model
758
+
759
+ :param Intel intel: The Intel SDK object
760
+ :param int limit: The number of records to fetch from CrowdStrike, defaults to 100
761
+ :raises General Error: If errors are returned from CrowdStrike
762
+ :raises Exception: If unable to fetch ID's from CrowdStrike
763
+ :return: List of vulnerability ID's
764
+ :rtype: list
765
+ """
766
+ try:
767
+ id_lookup = intel.query_vulnerabilities(limit=limit)
768
+ if "errors" in id_lookup["body"]:
769
+ error_and_exit(id_lookup["body"]["errors"])
770
+ number_of_records = len(id_lookup["body"]["resources"])
771
+ if number_of_records == 0:
772
+ logger.info(f"Found {number_of_records} Records.")
773
+ sys.exit(0)
774
+ return id_lookup["body"]["resources"]
775
+ except Exception as e:
776
+ error_and_exit(f"Error: {e}")
777
+
778
+
779
+ def get_vulnerabilities_by_id(ids: list, intel: Intel) -> list[dict]:
780
+ """
781
+ Retrieve record details using the IDs provided
782
+
783
+ :param list ids: The IDs of the records to retrieve
784
+ :param Intel intel: The Intel SDK object
785
+ :return: A list of dictionaries containing the record details
786
+ :rtype: list[dict]
787
+ """
788
+ detail_lookup = intel.get_vulnerabilities(ids=ids)
789
+ if detail_lookup["status_code"] != 200:
790
+ error_and_exit(detail_lookup["body"]["errors"][0])
791
+ return detail_lookup["body"]["resources"]
792
+
793
+
794
+ # FIXME: this will pull the data from the CrowdStrike API but need to figure out what the data looks like before we can map it
795
+ # @crowdstrike.command(name="fetch_vulnerabilities")
796
+ # @regscale_id(help="RegScale will create vulnerabilities as children of this record.")
797
+ # @regscale_module()
798
+ # @click.option("--limit", "-l", default=100, help="Limit the number of records")
799
+ # def sync_vulnerabilities(regscale_id: int, regscale_module: str, limit: int) -> None:
800
+ # """
801
+ # Fetch all vulnerabilities from CrowdStrike.
802
+ # :param int regscale_id: The ID of the RegScale record.
803
+ # :param str regscale_module: The module of the RegScale record.
804
+ # :param int limit: The number of records to fetch.
805
+ # """
806
+ # _sync_vulnerabilities(regscale_id=regscale_id, regscale_module=regscale_module, limit=limit)
807
+ #
808
+ #
809
+ # def _sync_vulnerabilities(regscale_id: int, regscale_module: str, limit) -> None:
810
+ # """
811
+ # Fetch all vulnerabilities from CrowdStrike.
812
+ # :param int regscale_id: The ID of the RegScale record.
813
+ # :param str regscale_module: The module of the RegScale record.
814
+ # :param int limit: The number of records to fetch.
815
+ # """
816
+ # intel = get_intel()
817
+ # ids = get_vulnerability_ids(intel=intel, limit=limit)
818
+ # data = get_vulnerabilities_by_id(ids=ids, intel=intel)
819
+ # logger.info(json.dumps(data, indent=4))
820
+
821
+
822
+ def load_compliance_data(framework_file: str) -> dict:
823
+ """
824
+ Load compliance data for the given framework from the package resources
825
+
826
+ :param str framework_file: The framework to load the compliance data for
827
+ :return: The compliance data as a dictionary
828
+ :rtype: dict
829
+ """
830
+ from importlib.resources import open_text
831
+
832
+ with open_text("regscale.integrations.commercial.mappings", f"{framework_file}.json") as f:
833
+ return json.load(f)
834
+
835
+
836
+ def sync_compliance(ssp_id: int, catalog_id: int, framework: str) -> None:
837
+ """
838
+ Sync compliance data from CrowdStrike to RegScale
839
+
840
+ :param int ssp_id: The ID of the SSP record
841
+ :param int catalog_id: The ID of the catalog to use for the sync
842
+ :param str framework: The framework to use for the sync
843
+ :rtype: None
844
+ """
845
+ catalog = Catalog.get_with_all_details(catalog_id=catalog_id)
846
+ if framework == "NIST800-53R5":
847
+ compliance_data = load_compliance_data("nist_800_53_r5_controls")
848
+ elif framework == "CSF":
849
+ compliance_data = load_compliance_data("csf_controls")
850
+ else:
851
+ logger.warning(f"Invalid framework: {framework}")
852
+ return
853
+
854
+ logger.info(f"Loading: {framework}")
855
+ logger.info(f"Mapping file loaded items mapped: {len(compliance_data)}")
856
+ cat_controls = catalog.get("controls", None)
857
+ if not cat_controls:
858
+ logger.error(f"No controls found for catalog {catalog_id}")
859
+ return
860
+ full_controls = {}
861
+ partial_controls = {}
862
+ for control in cat_controls:
863
+ compliance_control = compliance_data.get(control.get("controlId"), None)
864
+
865
+ if compliance_control and compliance_control.get("support") in [
866
+ "Full",
867
+ "Partial",
868
+ ]:
869
+ notes = [f"<p>{note}</p>" for note in compliance_control.get("notes", [])]
870
+ if compliance_control.get("support") == "Full":
871
+ control["implementation"] = "\n".join(notes)
872
+ full_controls[control.get("controlId").lower()] = control
873
+ elif compliance_control.get("support") == "Partial":
874
+ control["implementation"] = "\n".join(notes)
875
+ partial_controls[control.get("controlId").lower()] = control
876
+ logger.info(f"found fully implemented controls len: {len(full_controls)}")
877
+ logger.info(f"found partial implemented controls len: {len(partial_controls)}")
878
+ create_control_implementations(
879
+ cat_controls,
880
+ parent_id=ssp_id,
881
+ parent_module="securityplans",
882
+ existing_implementation_dict=ControlImplementation.get_existing_control_implementations(parent_id=ssp_id),
883
+ full_controls=full_controls,
884
+ partial_controls=partial_controls,
885
+ failing_controls={},
886
+ )
887
+
888
+
889
+ @crowdstrike.command(name="sync_compliance")
890
+ @click.option("--ssp_id", "-s", type=click.INT, required=True, help="The ID of the SSP record.")
891
+ @click.option(
892
+ "--catalog_id",
893
+ "-c",
894
+ type=click.INT,
895
+ required=True,
896
+ help="The ID of the catalog to use for the sync.",
897
+ )
898
+ @click.option(
899
+ "--framework",
900
+ "-f",
901
+ type=click.Choice(["CSF", "NIST800-53R5"], case_sensitive=False),
902
+ help="Choose either CSF or NIST800-53R5",
903
+ )
904
+ def run_compliance_sync(
905
+ ssp_id: int,
906
+ catalog_id: int,
907
+ framework: str,
908
+ ) -> None:
909
+ """Run a compliance sync from CrowdStrike to RegScale."""
910
+ sync_compliance(ssp_id=ssp_id, catalog_id=catalog_id, framework=framework)
911
+
912
+
913
+ def create_control_implementations(
914
+ controls: list,
915
+ parent_id: int,
916
+ parent_module: str,
917
+ existing_implementation_dict: dict,
918
+ full_controls: dict,
919
+ partial_controls: dict,
920
+ failing_controls: dict,
921
+ ) -> None:
922
+ """
923
+ Creates and updates control implementations based on given controls
924
+
925
+ :param list controls: List of control details
926
+ :param int parent_id: Identifier for the parent control
927
+ :param str parent_module: Name of the parent module
928
+ :param dict existing_implementation_dict: Dictionary of existing implementations
929
+ :param dict full_controls: Dictionary of fully implemented controls
930
+ :param dict partial_controls: Dictionary of partially implemented controls
931
+ :param dict failing_controls: Dictionary of failing controls
932
+ :rtype: None
933
+ """
934
+ app = Application()
935
+ user_id = app.config.get("userId")
936
+
937
+ to_create, to_update = process_controls(
938
+ controls,
939
+ parent_id,
940
+ parent_module,
941
+ existing_implementation_dict,
942
+ full_controls,
943
+ partial_controls,
944
+ failing_controls,
945
+ user_id,
946
+ )
947
+
948
+ post_batch_if_needed(app, to_create, ControlImplementation.post_batch_implementation)
949
+ put_batch_if_needed(app, to_update, ControlImplementation.put_batch_implementation)
950
+
951
+
952
+ def process_controls(
953
+ controls: list,
954
+ parent_id: int,
955
+ parent_module: str,
956
+ existing_implementation_dict: dict,
957
+ full_controls: dict,
958
+ partial_controls: dict,
959
+ failing_controls: dict,
960
+ user_id: Optional[str] = None,
961
+ ) -> tuple[list, list]:
962
+ """
963
+ Processes each control for creation or update
964
+
965
+ :param list controls: List of control details
966
+ :param int parent_id: Identifier for the parent control
967
+ :param str parent_module: Name of the parent module
968
+ :param dict existing_implementation_dict: Dictionary of existing implementations
969
+ :param dict full_controls: Dictionary of fully implemented controls
970
+ :param dict partial_controls: Dictionary of partially implemented controls
971
+ :param dict failing_controls: Dictionary of failing controls
972
+ :param Optional[str] user_id: ID of the user performing the operation, defaults to None
973
+ :return: Tuple containing lists of controls to create and update
974
+ :rtype: tuple[list, list]
975
+ """
976
+ to_create = []
977
+ to_update = []
978
+
979
+ for control in controls:
980
+ lower_case_control_id = control["controlId"].lower()
981
+ status = check_implementation(full_controls, partial_controls, failing_controls, lower_case_control_id)
982
+
983
+ if control["controlId"] not in existing_implementation_dict:
984
+ cim = create_new_control_implementation(control, parent_id, parent_module, status, user_id)
985
+ to_create.append(cim)
986
+ else:
987
+ update_existing_control_implementation(control, existing_implementation_dict, status, to_update, user_id)
988
+
989
+ return to_create, to_update
990
+
991
+
992
+ def create_new_control_implementation(
993
+ control: dict,
994
+ parent_id: int,
995
+ parent_module: str,
996
+ status: str,
997
+ user_id: Optional[str] = None,
998
+ ) -> ControlImplementation:
999
+ """
1000
+ Creates a new control implementation object
1001
+
1002
+ :param dict control: Control details
1003
+ :param int parent_id: Identifier for the parent control
1004
+ :param str parent_module: Name of the parent module
1005
+ :param str status: Status of the control implementation
1006
+ :param Optional[str] user_id: ID of the user performing the operation, defaults to None
1007
+ :return: New control implementation object
1008
+ :rtype: ControlImplementation
1009
+ """
1010
+ cim = ControlImplementation(
1011
+ controlOwnerId=user_id,
1012
+ dateLastAssessed=get_current_datetime(),
1013
+ implementation=control.get("implementation", None),
1014
+ status=status,
1015
+ controlID=control["id"],
1016
+ parentId=parent_id,
1017
+ parentModule=parent_module,
1018
+ createdById=user_id,
1019
+ dateCreated=get_current_datetime(),
1020
+ lastUpdatedById=user_id,
1021
+ dateLastUpdated=get_current_datetime(),
1022
+ ).dict()
1023
+ cim["controlSource"] = "Baseline"
1024
+ return cim
1025
+
1026
+
1027
+ def update_existing_control_implementation(
1028
+ control: dict,
1029
+ existing_implementation_dict: dict,
1030
+ status: str,
1031
+ to_update: list,
1032
+ user_id: Optional[str] = None,
1033
+ ):
1034
+ """
1035
+ Updates an existing control implementation
1036
+
1037
+ :param dict control: Control details
1038
+ :param dict existing_implementation_dict: Dictionary of existing implementations
1039
+ :param str status: Status of the control implementation
1040
+ :param list to_update: List of controls to update
1041
+ :param Optional[str] user_id: ID of the user performing the operation, defaults to None
1042
+ """
1043
+ existing_imp = existing_implementation_dict[control["controlId"]]
1044
+ existing_imp.update(
1045
+ {
1046
+ "implementation": control.get("implementation"),
1047
+ "status": status,
1048
+ "dateLastAssessed": get_current_datetime(),
1049
+ "lastUpdatedById": user_id,
1050
+ "dateLastUpdated": get_current_datetime(),
1051
+ }
1052
+ )
1053
+
1054
+ remove_keys(existing_imp, ["createdBy", "systemRole", "controlOwner", "lastUpdatedBy"])
1055
+
1056
+ if existing_imp not in to_update:
1057
+ to_update.append(existing_imp)
1058
+
1059
+
1060
+ def post_batch_if_needed(
1061
+ app: Application,
1062
+ to_create: list,
1063
+ post_function: Callable[[Application, list], None],
1064
+ ) -> None:
1065
+ """
1066
+ Posts a batch of new implementations if the list is not empty
1067
+
1068
+ :param Application app: RegScale CLI application object
1069
+ :param list to_create: List of new implementations to post
1070
+ :param Callable[[Application, list], None] post_function: The function to call for posting the batch
1071
+ :rtype: None
1072
+ """
1073
+ if to_create:
1074
+ post_function(app, to_create)
1075
+
1076
+
1077
+ def put_batch_if_needed(app: Application, to_update: list, put_function: Callable[[Application, list], None]) -> None:
1078
+ """
1079
+ Puts a batch of updated implementations if the list is not empty
1080
+
1081
+ :param Application app: RegScale CLI application object
1082
+ :param list to_update: List of implementations to update
1083
+ :param Callable[[Application, list], None] put_function: The function to call for putting the batch
1084
+ :rtype: None
1085
+ """
1086
+ if to_update:
1087
+ put_function(app, to_update)
1088
+
1089
+
1090
+ def check_implementation(
1091
+ full_controls: dict,
1092
+ partial_controls: dict,
1093
+ failing_controls: dict,
1094
+ control_id: str,
1095
+ ) -> str:
1096
+ """
1097
+ Checks the status of a control implementation
1098
+
1099
+ :param dict full_controls: Dictionary of passing controls
1100
+ :param dict partial_controls: Dictionary of partially implemented controls
1101
+ :param dict failing_controls: Dictionary of failing control implementations
1102
+ :param str control_id: control id
1103
+ :return: status of control implementation
1104
+ :rtype: str
1105
+ """
1106
+ if control_id in full_controls.keys():
1107
+ logger.debug(f"Found fully implemented control: {control_id}")
1108
+ return ControlImplementationStatus.FullyImplemented.value
1109
+ elif control_id in partial_controls.keys():
1110
+ logger.debug(f"Found partially implemented control: {control_id}")
1111
+ return ControlImplementationStatus.PartiallyImplemented.value
1112
+ elif control_id in failing_controls.keys():
1113
+ logger.debug(f"Found failing control: {control_id}")
1114
+ return ControlImplementationStatus.InRemediation.value
1115
+ else:
1116
+ logger.debug(f"Found not implemented control: {control_id}")
1117
+ return ControlImplementationStatus.NotImplemented.value