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,1511 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """RegScale Microsoft Defender recommendations and alerts integration"""
4
+ # standard python imports
5
+ from concurrent.futures import ThreadPoolExecutor, as_completed
6
+ from datetime import datetime, timedelta
7
+ from json import JSONDecodeError
8
+ from os import PathLike
9
+ from typing import Literal, Optional, Tuple, Union
10
+
11
+ import click
12
+ import requests
13
+ from pathlib import Path
14
+ from rich.console import Console
15
+ from rich.progress import Progress
16
+
17
+ from regscale.core.app.api import Api
18
+ from regscale.core.app.internal.login import is_valid
19
+ from regscale.core.app.logz import create_logger
20
+ from regscale.core.app.utils.app_utils import (
21
+ check_license,
22
+ create_progress_object,
23
+ error_and_exit,
24
+ flatten_dict,
25
+ get_current_datetime,
26
+ reformat_str_date,
27
+ uncamel_case,
28
+ save_data_to,
29
+ )
30
+ from regscale.models import regscale_id, regscale_module, regscale_ssp_id, Asset, Component, File, Issue
31
+ from regscale.models.integration_models.defender_data import DefenderData
32
+ from regscale.models.integration_models.flat_file_importer import FlatFileImporter
33
+ from regscale.utils.string import generate_html_table_from_dict
34
+
35
+ LOGIN_ERROR = "Login Invalid RegScale Credentials, please login for a new token."
36
+ console = Console()
37
+ job_progress = create_progress_object()
38
+ logger = create_logger()
39
+ unique_recs = []
40
+ issues_to_create = []
41
+ closed = []
42
+ updated = []
43
+ DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
44
+ IDENTIFICATION_TYPE = "Vulnerability Assessment"
45
+ CLOUD_RECS = "Microsoft Defender for Cloud Recommendation"
46
+ APP_JSON = "application/json"
47
+ AFD_ENDPOINTS = "microsoft.cdn/profiles/afdendpoints"
48
+
49
+
50
+ ######################################################################################################
51
+ #
52
+ # Adding application to Microsoft Defender API:
53
+ # https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/exposed-apis-create-app-webapp
54
+ # Microsoft Defender 365 APIs Docs:
55
+ # https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/exposed-apis-list?view=o365-worldwide
56
+ # Microsoft Defender for Cloud Alerts API Docs:
57
+ # https://learn.microsoft.com/en-us/rest/api/defenderforcloud/alerts?view=rest-defenderforcloud-2022-01-01
58
+ # Microsoft Defender for Cloud Recommendations API Docs:
59
+ # https://learn.microsoft.com/en-us/rest/api/defenderforcloud/assessments/list?view=rest-defenderforcloud-2020-01-01
60
+ # Microsoft Defender for Cloud Resources API Docs:
61
+ # https://learn.microsoft.com/en-us/rest/api/azureresourcegraph/resourcegraph/resources/resources
62
+ #
63
+ ######################################################################################################
64
+
65
+
66
+ @click.group()
67
+ def defender():
68
+ """Create RegScale issues for each Microsoft Defender 365 Recommendation"""
69
+
70
+
71
+ @defender.command(name="authenticate")
72
+ @click.option(
73
+ "--system",
74
+ type=click.Choice(["cloud", "365"], case_sensitive=False),
75
+ help="Pull recommendations from Microsoft Defender 365 or Microsoft Defender for Cloud.",
76
+ prompt="Please choose a system",
77
+ required=True,
78
+ )
79
+ def authenticate_in_defender(system: Literal["cloud", "365"]):
80
+ """Obtains an access token using the credentials provided in init.yaml."""
81
+ authenticate(system=system)
82
+
83
+
84
+ @defender.command(name="sync_365_alerts")
85
+ @regscale_id(required=False, default=None, prompt=False)
86
+ @regscale_module(required=False, default=None, prompt=False)
87
+ def sync_365_alerts(regscale_id: Optional[int] = None, regscale_module: Optional[str] = None):
88
+ """
89
+ Get Microsoft Defender 365 alerts and create RegScale
90
+ issues with the information from Microsoft Defender 365.
91
+ """
92
+ sync_defender_and_regscale(
93
+ parent_id=regscale_id, parent_module=regscale_module, system="365", defender_object="alerts"
94
+ )
95
+
96
+
97
+ @defender.command(name="sync_365_recommendations")
98
+ @regscale_id(required=False, default=None, prompt=False)
99
+ @regscale_module(required=False, default=None, prompt=False)
100
+ def sync_365_recommendations(regscale_id: Optional[int] = None, regscale_module: Optional[str] = None):
101
+ """
102
+ Get Microsoft Defender 365 recommendations and create RegScale
103
+ issues with the information from Microsoft Defender 365.
104
+ """
105
+ sync_defender_and_regscale(
106
+ parent_id=regscale_id, parent_module=regscale_module, system="365", defender_object="recommendations"
107
+ )
108
+
109
+
110
+ @defender.command(name="sync_cloud_resources")
111
+ @regscale_ssp_id()
112
+ def sync_cloud_resources(regscale_ssp_id: int):
113
+ """
114
+ Get Microsoft Defender for Cloud resources and create RegScale assets with the information from Microsoft
115
+ Defender for Cloud.
116
+ """
117
+ sync_resources(ssp_id=regscale_ssp_id)
118
+
119
+
120
+ @defender.command(name="export_resources")
121
+ @regscale_id()
122
+ @regscale_module()
123
+ @click.option(
124
+ "--query_name",
125
+ "-q",
126
+ "-n",
127
+ type=click.STRING,
128
+ help="The name of the saved query to export from Microsoft Defender for Cloud resource graph queries.",
129
+ prompt="Enter the name of the query to export",
130
+ default=None,
131
+ )
132
+ @click.option(
133
+ "--no_upload",
134
+ "-n",
135
+ is_flag=True,
136
+ help="Flag to skip uploading the exported .csv file to RegScale.",
137
+ default=False,
138
+ )
139
+ @click.option(
140
+ "--all_queries",
141
+ "-a",
142
+ is_flag=True,
143
+ help="Export all saved queries from Microsoft Defender for Cloud resource graph queries.",
144
+ )
145
+ def export_resources_to_csv(
146
+ regscale_id: int, regscale_module: str, query_name: str, no_upload: bool, all_queries: bool
147
+ ):
148
+ """
149
+ Export data from Microsoft Defender for Cloud queries and save them to a .csv file.
150
+ """
151
+ export_resources(
152
+ parent_id=regscale_id,
153
+ parent_module=regscale_module,
154
+ query_name=query_name,
155
+ no_upload=no_upload,
156
+ all_queries=all_queries,
157
+ )
158
+
159
+
160
+ @defender.command(name="sync_cloud_alerts")
161
+ @regscale_id(required=False, default=None, prompt=False)
162
+ @regscale_module(required=False, default=None, prompt=False)
163
+ def sync_cloud_alerts(regscale_id: Optional[int] = None, regscale_module: Optional[str] = None):
164
+ """
165
+ Get Microsoft Defender for Cloud alerts and create RegScale
166
+ issues with the information from Microsoft Defender for Cloud.
167
+ """
168
+ sync_defender_and_regscale(
169
+ parent_id=regscale_id, parent_module=regscale_module, system="cloud", defender_object="alerts"
170
+ )
171
+
172
+
173
+ @defender.command(name="sync_cloud_recommendations")
174
+ @regscale_id(required=False, default=None, prompt=False)
175
+ @regscale_module(required=False, default=None, prompt=False)
176
+ def sync_cloud_recommendations(regscale_id: Optional[int] = None, regscale_module: Optional[str] = None):
177
+ """
178
+ Get Microsoft Defender for Cloud recommendations and create RegScale
179
+ issues with the information from Microsoft Defender for Cloud.
180
+ """
181
+ sync_defender_and_regscale(
182
+ parent_id=regscale_id, parent_module=regscale_module, system="cloud", defender_object="recommendations"
183
+ )
184
+
185
+
186
+ @defender.command(name="import_alerts")
187
+ @FlatFileImporter.common_scanner_options(
188
+ message="File path to the folder containing Defender .csv files to process to RegScale.",
189
+ prompt="File path to Defender files",
190
+ import_name="defender",
191
+ )
192
+ def import_alerts(
193
+ folder_path: PathLike[str],
194
+ regscale_ssp_id: int,
195
+ scan_date: datetime,
196
+ mappings_path: Path,
197
+ disable_mapping: bool,
198
+ s3_bucket: str,
199
+ s3_prefix: str,
200
+ aws_profile: str,
201
+ upload_file: bool,
202
+ ):
203
+ """
204
+ Import Microsoft Defender alerts from a CSV file
205
+ """
206
+ import_defender_alerts(
207
+ folder_path,
208
+ regscale_ssp_id,
209
+ scan_date,
210
+ mappings_path,
211
+ disable_mapping,
212
+ s3_bucket,
213
+ s3_prefix,
214
+ aws_profile,
215
+ upload_file,
216
+ )
217
+
218
+
219
+ def import_defender_alerts(
220
+ folder_path: PathLike[str],
221
+ regscale_ssp_id: int,
222
+ scan_date: datetime,
223
+ mappings_path: Path,
224
+ disable_mapping: bool,
225
+ s3_bucket: str,
226
+ s3_prefix: str,
227
+ aws_profile: str,
228
+ upload_file: Optional[bool] = True,
229
+ ) -> None:
230
+ """
231
+ Import Microsoft Defender alerts from a CSV file
232
+
233
+ :param PathLike[str] folder_path: File path to the folder containing Defender .csv files to process to RegScale
234
+ :param int regscale_ssp_id: The RegScale SSP ID
235
+ :param datetime scan_date: The date of the scan
236
+ :param Path mappings_path: The path to the mappings file
237
+ :param bool disable_mapping: Whether to disable custom mappings
238
+ :param str s3_bucket: The S3 bucket to download the files from
239
+ :param str s3_prefix: The S3 prefix to download the files from
240
+ :param str aws_profile: The AWS profile to use for S3 access
241
+ :param Optional[bool] upload_file: Whether to upload the file to RegScale after processing, defaults to True
242
+ :rtype: None
243
+ """
244
+ from regscale.models.integration_models.defenderimport import DefenderImport
245
+
246
+ FlatFileImporter.import_files(
247
+ import_type=DefenderImport,
248
+ import_name="Defender",
249
+ file_types=".csv",
250
+ folder_path=folder_path,
251
+ regscale_ssp_id=regscale_ssp_id,
252
+ scan_date=scan_date,
253
+ mappings_path=mappings_path,
254
+ disable_mapping=disable_mapping,
255
+ s3_bucket=s3_bucket,
256
+ s3_prefix=s3_prefix,
257
+ aws_profile=aws_profile,
258
+ upload_file=upload_file,
259
+ )
260
+
261
+
262
+ def authenticate(system: Literal["cloud", "365"]) -> None:
263
+ """
264
+ Obtains an access token using the credentials provided in init.yaml
265
+
266
+ :param Literal["cloud", "365"] system: The system to authenticate for, either Defender 365 or Defender for Cloud
267
+ :rtype: None
268
+ """
269
+ app = check_license()
270
+ api = Api()
271
+ if system == "365":
272
+ url = "https://api.securitycenter.microsoft.com/api/alerts"
273
+ elif system == "cloud":
274
+ url = (
275
+ f'https://management.azure.com/subscriptions/{app.config["azureCloudSubscriptionId"]}/'
276
+ + "providers/Microsoft.Security/alerts?api-version=2022-01-01"
277
+ )
278
+ else:
279
+ error_and_exit("Please enter 365 or cloud for the system.")
280
+ check_token(api=api, system=system, url=url)
281
+
282
+
283
+ def sync_defender_and_regscale(
284
+ parent_id: Optional[int] = None,
285
+ parent_module: Optional[str] = None,
286
+ system: Literal["365", "cloud"] = "365",
287
+ defender_object: Literal["alerts", "recommendations"] = "recommendations",
288
+ ) -> None:
289
+ """
290
+ Sync Microsoft Defender data with RegScale
291
+
292
+ :param Optional[int] parent_id: The RegScale ID to sync the alerts to, defaults to None
293
+ :param Optional[str] parent_module: The RegScale module to sync the alerts to, defaults to None
294
+ :param Literal["365", "cloud"] system: The system to sync the alerts from, defaults to "365"
295
+ :param Literal["alerts", "recommendations"] defender_object: The type of data to sync, defaults to "recommendations"
296
+ :rtype: None
297
+ """
298
+ app = check_license()
299
+ api = Api()
300
+ # check if RegScale token is valid:
301
+ if not is_valid(app=app):
302
+ error_and_exit(LOGIN_ERROR)
303
+ mapping_key = f"{system}_{defender_object}"
304
+ url_mapping = {
305
+ "365_alerts": "https://api.securitycenter.microsoft.com/api/alerts",
306
+ "365_recommendations": "https://api.securitycenter.microsoft.com/api/recommendations",
307
+ "cloud_alerts": f'https://management.azure.com/subscriptions/{app.config["azureCloudSubscriptionId"]}/'
308
+ + "providers/Microsoft.Security/alerts?api-version=2022-01-01",
309
+ "cloud_recommendations": f"https://management.azure.com/subscriptions/{app.config['azureCloudSubscriptionId']}/"
310
+ + "providers/Microsoft.Security/assessments?api-version=2020-01-01&$expand=metadata",
311
+ }
312
+ url = url_mapping[mapping_key]
313
+ defender_key = "id" if system == "365" else "name"
314
+ mapping_func = {
315
+ "365_alerts": map_365_alert_to_issue,
316
+ "365_recommendations": map_365_recommendation_to_issue,
317
+ "cloud_alerts": map_cloud_alert_to_issue,
318
+ "cloud_recommendations": map_cloud_recommendation_to_issue,
319
+ }
320
+ # check the azure token, get a new one if needed
321
+ token = check_token(api=api, system=system, url=url)
322
+
323
+ # set headers for the data
324
+ headers = {"Content-Type": APP_JSON, "Authorization": token}
325
+ logging_object = f"{defender_object[:-1]}(s)"
326
+ logging_system = "365" if system == "365" else "for Cloud"
327
+ logger.info(f"Retrieving Microsoft Defender {system.title()} {logging_object}...")
328
+ if defender_objects := get_items_from_azure(
329
+ api=api,
330
+ headers=headers,
331
+ url=url,
332
+ ):
333
+ defender_data = [
334
+ DefenderData(id=data[defender_key], data=data, system=system, object=defender_object)
335
+ for data in defender_objects
336
+ ]
337
+ integration_field = defender_data[0].integration_field
338
+ logger.info(f"Found {len(defender_data)} Microsoft Defender {logging_system} {logging_object}.")
339
+ else:
340
+ defender_data = []
341
+ integration_field = DefenderData.get_integration_field(system=system, object=defender_object)
342
+ logger.info(f"No Microsoft Defender {logging_system} {defender_object} found.")
343
+
344
+ # get all issues from RegScale where the defenderId field is populated
345
+ # if regscale_id and regscale_module aren't provided
346
+ if parent_id and parent_module:
347
+ app.logger.info(f"Retrieving issues from RegScale for {parent_module} #{parent_id}...")
348
+ issues = Issue.get_all_by_parent(parent_id=parent_id, parent_module=parent_module)
349
+ # sort the issues that have the integration field populated
350
+ issues = [issue for issue in issues if getattr(issue, integration_field, None)]
351
+ elif mapping_key == "cloud_recommendations":
352
+ app.logger.warning(f"Retrieving all issues with {integration_field} populated in RegScale...")
353
+ issues = Issue.get_all_by_manual_detection_source(value=CLOUD_RECS)
354
+ else:
355
+ app.logger.warning(f"Retrieving all issues with {integration_field} populated in RegScale...")
356
+ issues = Issue.get_all_by_integration_field(field=integration_field)
357
+ logger.info(f"Retrieved {len(issues)} issue(s) from RegScale.")
358
+
359
+ regscale_issues = [
360
+ DefenderData(
361
+ id=getattr(issue, integration_field, ""), data=issue.model_dump(), system=system, object=defender_object
362
+ )
363
+ for issue in issues
364
+ ]
365
+ new_issues = []
366
+ # create progress bars for each threaded task
367
+ with job_progress:
368
+ # see if there are any issues with defender id populated
369
+ if regscale_issues:
370
+ logger.info(f"{len(regscale_issues)} RegScale issue(s) will be analyzed.")
371
+ # create progress bar and analyze the RegScale issues
372
+ analyze_regscale_issues = job_progress.add_task(
373
+ f"[#f8b737]Analyzing {len(regscale_issues)} RegScale issue(s)...", total=len(regscale_issues)
374
+ )
375
+ # evaluate open issues in RegScale
376
+ app.thread_manager.submit_tasks_from_list(
377
+ evaluate_open_issues,
378
+ regscale_issues,
379
+ (
380
+ api,
381
+ defender_data,
382
+ analyze_regscale_issues,
383
+ ),
384
+ )
385
+ _ = app.thread_manager.execute_and_verify()
386
+ else:
387
+ logger.info("No issues from RegScale need to be analyzed.")
388
+ # compare defender 365 recommendations and RegScale issues
389
+ # while removing duplicates, updating existing RegScale Issues,
390
+ # and adding new unique recommendations to unique_recs global variable
391
+ if defender_data:
392
+ logger.info(
393
+ f"Comparing {len(defender_data)} Microsoft Defender {logging_system} {logging_object} "
394
+ f"and {len(regscale_issues)} RegScale issue(s).",
395
+ )
396
+ compare_task = job_progress.add_task(
397
+ f"[#ef5d23]Comparing {len(defender_data)} Microsoft Defender {logging_system} {logging_object} and "
398
+ + f"{len(regscale_issues)} RegScale issue(s)...",
399
+ total=len(defender_data),
400
+ )
401
+ app.thread_manager.submit_tasks_from_list(
402
+ compare_defender_and_regscale,
403
+ defender_data,
404
+ (
405
+ api,
406
+ regscale_issues,
407
+ defender_key,
408
+ compare_task,
409
+ ),
410
+ )
411
+ _ = app.thread_manager.execute_and_verify()
412
+ # start threads and progress bar for # of issues that need to be created
413
+ if len(unique_recs) > 0:
414
+ logger.info("Prepping %s issue(s) for creation in RegScale.", len(unique_recs))
415
+ create_issues = job_progress.add_task(
416
+ f"[#21a5bb]Prepping {len(unique_recs)} issue(s) for creation in RegScale...",
417
+ total=len(unique_recs),
418
+ )
419
+ app.thread_manager.submit_tasks_from_list(
420
+ prep_issues_for_creation,
421
+ unique_recs,
422
+ (
423
+ mapping_func[mapping_key],
424
+ api.config,
425
+ defender_key,
426
+ parent_id,
427
+ parent_module,
428
+ create_issues,
429
+ ),
430
+ )
431
+ _ = app.thread_manager.execute_and_verify()
432
+ logger.info(
433
+ "%s/%s issue(s) ready for creation in RegScale.",
434
+ len(issues_to_create),
435
+ len(unique_recs),
436
+ )
437
+ new_issues = Issue.batch_create(issues_to_create, progress_context=job_progress)
438
+ logger.info(f"Created {len(new_issues)} issue(s) in RegScale.")
439
+ # check if issues needed to be created, updated or closed and print the appropriate message
440
+ if (len(unique_recs) + len(updated) + len(closed)) == 0:
441
+ logger.info("[green]No changes required for existing RegScale issue(s)!")
442
+ else:
443
+ logger.info(
444
+ f"{len(new_issues)} issue(s) created, {len(updated)} issue(s)"
445
+ + f" updated and {len(closed)} issue(s) were closed in RegScale."
446
+ )
447
+
448
+
449
+ def check_token(api: Api, system: Literal["cloud", "365"], url: Optional[str] = None) -> str:
450
+ """
451
+ Function to check if current Azure token from init.yaml is valid, if not replace it
452
+
453
+ :param Api api: API object
454
+ :param Literal["cloud", "365"] system: Which system to check JWT for, either Defender 365 or Defender for Cloud
455
+ :param str url: The URL to use for authentication, defaults to None
456
+ :return: returns JWT for Microsoft 365 Defender or Microsoft Defender for Cloud depending on system provided
457
+ :rtype: str
458
+ """
459
+ # set up variables for the provided system
460
+ if system == "cloud":
461
+ key = "azureCloudAccessToken"
462
+ elif system.lower() == "365":
463
+ key = "azure365AccessToken"
464
+ else:
465
+ error_and_exit(
466
+ f"{system.title()} is not supported, only Microsoft 365 Defender and Microsoft Defender for Cloud."
467
+ )
468
+ current_token = api.config[key]
469
+ # check the token if it isn't blank
470
+ if current_token and url:
471
+ # set the headers
472
+ header = {"Content-Type": APP_JSON, "Authorization": current_token}
473
+ # test current token by getting recommendations
474
+ token_pass = api.get(url=url, headers=header)
475
+ # check the status code
476
+ if getattr(token_pass, "status_code", 0) == 200:
477
+ # token still valid, return it
478
+ token = api.config[key]
479
+ logger.info(
480
+ "Current token for %s is still valid and will be used for future requests.",
481
+ system.title(),
482
+ )
483
+ elif getattr(token_pass, "status_code", 0) == 403:
484
+ # token doesn't have permissions, notify user and exit
485
+ error_and_exit(
486
+ "Incorrect permissions set for application. Cannot retrieve recommendations.\n"
487
+ + f"{token_pass.status_code}: {token_pass.reason}\n{token_pass.text}"
488
+ )
489
+ else:
490
+ # token is no longer valid, get a new one
491
+ token = get_token(api=api, system=system)
492
+ # token is empty, get a new token
493
+ else:
494
+ token = get_token(api=api, system=system)
495
+ return token
496
+
497
+
498
+ def get_token(api: Api, system: Literal["cloud", "365"]) -> str:
499
+ """
500
+ Function to get a token from Microsoft Azure and saves it to init.yaml
501
+
502
+ :param Api api: API object
503
+ :param Literal[str] system: Which platform to authenticate for Microsoft Defender, cloud or 365
504
+ :return: JWT from Azure
505
+ :rtype: str
506
+ """
507
+ # set the url and body for request
508
+ if system == "365":
509
+ url = f'https://login.windows.net/{api.config["azure365TenantId"]}/oauth2/token'
510
+ client_id = api.config["azure365ClientId"]
511
+ client_secret = api.config["azure365Secret"]
512
+ resource = "https://api.securitycenter.windows.com"
513
+ key = "azure365AccessToken"
514
+ elif system == "cloud":
515
+ url = f'https://login.microsoftonline.com/{api.config["azureCloudTenantId"]}/oauth2/token'
516
+ client_id = api.config["azureCloudClientId"]
517
+ client_secret = api.config["azureCloudSecret"]
518
+ resource = "https://management.azure.com"
519
+ key = "azureCloudAccessToken"
520
+ else:
521
+ error_and_exit(
522
+ f"{system.title()} is not supported, only Microsoft `365` Defender and Microsoft Defender for `Cloud`."
523
+ )
524
+ data = {
525
+ "resource": resource,
526
+ "client_id": client_id,
527
+ "client_secret": client_secret,
528
+ "grant_type": "client_credentials",
529
+ }
530
+ # get the data
531
+ response = api.post(
532
+ url=url,
533
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
534
+ data=data,
535
+ )
536
+ try:
537
+ return parse_and_save_token(response, api, key, system)
538
+ except KeyError as ex:
539
+ # notify user we weren't able to get a token and exit
540
+ error_and_exit(f"Didn't receive token from Azure.\n{ex}\n{response.text}")
541
+ except JSONDecodeError as ex:
542
+ # notify user we weren't able to get a token and exit
543
+ error_and_exit(f"Unable to authenticate with Azure.\n{ex}\n{response.text}")
544
+
545
+
546
+ def parse_and_save_token(response: requests.Response, api: Api, key: str, system: str) -> str:
547
+ """
548
+ Function to parse the token from the response and save it to init.yaml
549
+
550
+ :param requests.Response response: Response from API call
551
+ :param Api api: API object
552
+ :param str key: Key to use for init.yaml token update
553
+ :param str system: Which system to check JWT for, either Defender 365 or Defender for Cloud
554
+ :return: JWT from Azure for the provided system
555
+ :rtype: str
556
+ """
557
+ # try to read the response and parse the token
558
+ res = response.json()
559
+ token = res["access_token"]
560
+
561
+ # add the token to init.yaml
562
+ api.config[key] = f"Bearer {token}"
563
+
564
+ # write the changes back to file
565
+ api.app.save_config(api.config) # type: ignore
566
+
567
+ # notify the user we were successful
568
+ logger.info(f"Azure {system.title()} Login Successful! Init.yaml file was updated with the new access token.")
569
+ # return the token string
570
+ return api.config[key]
571
+
572
+
573
+ def get_items_from_azure(api: Api, headers: dict, url: str) -> list:
574
+ """
575
+ Function to get data from Microsoft Defender returns the data as a list while handling pagination
576
+
577
+ :param Api api: API object
578
+ :param dict headers: Headers used for API call
579
+ :param str url: URL to use for the API call
580
+ :return: list of recommendations
581
+ :rtype: list
582
+ """
583
+ # get the data via api call
584
+ response = api.get(url=url, headers=headers)
585
+ if response.status_code != 200:
586
+ error_and_exit(
587
+ f"Received unexpected response from Microsoft Defender.\n{response.status_code}:{response.reason}"
588
+ + f"\n{response.text}",
589
+ )
590
+ # try to read the response
591
+ try:
592
+ response_data = response.json()
593
+ # try to get the values from the api response
594
+ defender_data = response_data["value"]
595
+ except JSONDecodeError:
596
+ # notify user if there was a json decode error from API response and exit
597
+ error_and_exit("JSON Decode error")
598
+ except KeyError:
599
+ # notify user there was no data from API response and exit
600
+ error_and_exit(
601
+ f"Received unexpected response from Microsoft Defender.\n{response.status_code}: {response.text}"
602
+ )
603
+ # check if pagination is required to fetch all data from Microsoft Defender
604
+ if next_link := response_data.get("nextLink"):
605
+ # get the rest of the data
606
+ defender_data.extend(get_items_from_azure(api=api, headers=headers, url=next_link))
607
+ # return the defender recommendations
608
+ return defender_data
609
+
610
+
611
+ def get_due_date(score: Union[str, int, None], config: dict, key: str) -> str:
612
+ """
613
+ Function to return due date based on the severity score of
614
+ the Microsoft Defender recommendation; the values are in the init.yaml
615
+ and if not, use the industry standards
616
+
617
+ :param Union[str, int, None] score: Severity score from Microsoft Defender
618
+ :param dict config: Application config
619
+ :param str key: The key to use for init.yaml
620
+ :return: Due date for the issue
621
+ :rtype: str
622
+ """
623
+ # check severity score and assign it to the appropriate due date
624
+ # using the init.yaml specified days
625
+ today = datetime.now().strftime("%m/%d/%y")
626
+
627
+ if not score:
628
+ score = 0
629
+
630
+ # check if the score is a string, if so convert it to an int & determine due date
631
+ if isinstance(score, str):
632
+ if score.lower() == "low":
633
+ score = 3
634
+ elif score.lower() == "medium":
635
+ score = 5
636
+ elif score.lower() == "high":
637
+ score = 9
638
+ else:
639
+ score = 0
640
+ if score >= 7:
641
+ days = config["issues"][key]["high"]
642
+ elif 4 <= score < 7:
643
+ days = config["issues"][key]["moderate"]
644
+ else:
645
+ days = config["issues"][key]["low"]
646
+ due_date = datetime.strptime(today, "%m/%d/%y") + timedelta(days=days)
647
+ return due_date.strftime(DATE_FORMAT)
648
+
649
+
650
+ def format_description(defender_data: dict, tenant_id: str) -> str:
651
+ """
652
+ Function to format the provided dictionary into an HTML table
653
+
654
+ :param dict defender_data: Microsoft Defender data as a dictionary
655
+ :param str tenant_id: The Microsoft Defender tenant ID
656
+ :return: HTML table as a string
657
+ :rtype: str
658
+ """
659
+ url = get_defender_url(defender_data, tenant_id)
660
+ defender_data = flatten_dict(data=defender_data)
661
+ payload = create_payload(defender_data) # type: ignore
662
+ description = create_html_table(payload, url)
663
+ return description
664
+
665
+
666
+ def get_defender_url(rec: dict, tenant_id: str) -> str:
667
+ """
668
+ Function to get the URL for the Microsoft Defender data
669
+
670
+ :param dict rec: Microsoft Defender data as a dictionary
671
+ :param str tenant_id: The Microsoft Defender tenant ID
672
+ :return: URL as a string
673
+ :rtype: str
674
+ """
675
+ try:
676
+ url = rec["properties"]["alertUri"]
677
+ except KeyError:
678
+ url = f"https://security.microsoft.com/security-recommendations?tid={tenant_id}"
679
+ return f'<a href="{url}">{url}</a>'
680
+
681
+
682
+ def create_payload(rec: dict) -> dict:
683
+ """
684
+ Function to create a payload for the Microsoft Defender data
685
+
686
+ :param dict rec: Microsoft Defender data as a dictionary
687
+ :return: Payload as a dictionary
688
+ :rtype: dict
689
+ """
690
+ payload = {}
691
+ skip_keys = ["associatedthreats", "alerturi", "investigation steps"]
692
+ for key, value in rec.items():
693
+ key = key.replace("propertiesExtendedProperties", "").replace("properties", "")
694
+ if isinstance(value, list) and len(value) > 0 and key.lower() not in skip_keys:
695
+ payload[uncamel_case(key)] = process_list_value(value)
696
+ elif key.lower() not in skip_keys and "entities" not in key.lower():
697
+ if not isinstance(value, list):
698
+ payload[uncamel_case(key)] = value
699
+ return payload
700
+
701
+
702
+ def process_list_value(value: list) -> str:
703
+ """
704
+ Function to process the list value for the Microsoft Defender data
705
+
706
+ :param list value: List of values
707
+ :return: Processed list value as a string
708
+ :rtype: str
709
+ """
710
+ if isinstance(value[0], dict):
711
+ return "".join(f"</br>{k}: {v}" for item in value for k, v in item.items())
712
+ elif isinstance(value[0], list):
713
+ return "".join("</br>".join(item) for item in value)
714
+ else:
715
+ return "</br>".join(value)
716
+
717
+
718
+ def create_html_table(payload: dict, url: str) -> str:
719
+ """
720
+ Function to create an HTML table for the Microsoft Defender data
721
+
722
+ :param dict payload: Payload for the Microsoft Defender data
723
+ :param str url: URL for the Microsoft Defender data
724
+ :return: HTML table as a string
725
+ :rtype: str
726
+ """
727
+ description = '<table style="border: 1px solid;">'
728
+ for key, value in payload.items():
729
+ if value:
730
+ if "time" in key.lower():
731
+ value = reformat_str_date(value, dt_format="%b %d, %Y")
732
+ description += (
733
+ f'<tr><td style="border: 1px solid;"><b>{key}</b></td>'
734
+ f'<td style="border: 1px solid;">{value}</td></tr>'
735
+ )
736
+ description += (
737
+ '<tr><td style="border: 1px solid;"><b>View in Defender</b></td>'
738
+ f'<td style="border: 1px solid;">{url}</td></tr>'
739
+ )
740
+ description += "</table>"
741
+ return description
742
+
743
+
744
+ def compare_defender_and_regscale(def_data: DefenderData, args: Tuple) -> None:
745
+ """
746
+ Function to check for duplicates between issues in RegScale
747
+ and recommendations/alerts from Microsoft Defender while using threads
748
+
749
+ :param DefenderData def_data: Microsoft Defender data
750
+ :param Tuple args: Tuple of args to use during the process
751
+ :rtype: None
752
+ """
753
+ # set local variables with the args that were passed
754
+ api, issues, defender_key, task = args
755
+
756
+ # see if recommendation has been analyzed already
757
+ if not def_data.analyzed:
758
+ # change analyzed flag
759
+ def_data.analyzed = True
760
+
761
+ # set duplication flag to false
762
+ dupe_check = False
763
+
764
+ # iterate through the RegScale issues with defenderId populated
765
+ for issue in issues:
766
+ # check if the RegScale key == Windows Defender ID
767
+ if issue.data.get(issue.integration_field) == def_data.data[defender_key]:
768
+ # change the duplication flag to True
769
+ dupe_check = True
770
+ # check if the RegScale issue is closed or cancelled
771
+ if issue.data["status"].lower() in ["closed", "cancelled"]:
772
+ # reopen RegScale issue because Microsoft Defender has
773
+ # recommended it again
774
+ change_issue_status(
775
+ api=api,
776
+ status=api.config["issues"][issue.init_key]["status"],
777
+ issue=issue.data,
778
+ rec=def_data,
779
+ rec_type=issue.init_key,
780
+ )
781
+ # check if the recommendation is a duplicate
782
+ if dupe_check is False:
783
+ # append unique recommendation to global unique_reqs
784
+ unique_recs.append(def_data)
785
+ job_progress.update(task, advance=1)
786
+
787
+
788
+ def evaluate_open_issues(issue: DefenderData, args: Tuple) -> None:
789
+ """
790
+ function to check for Open RegScale issues against Microsoft
791
+ Defender recommendations and will close the issues that are
792
+ no longer recommended by Microsoft Defender while using threads
793
+
794
+ :param DefenderData issue: Microsoft Defender data
795
+ :param Tuple args: Tuple of args to use during the process
796
+ :rtype: None
797
+ """
798
+ # set up local variables from the passed args
799
+ api, defender_data, task = args
800
+
801
+ defender_data_dict = {defender_data.id: defender_data for defender_data in defender_data if defender_data.id}
802
+
803
+ # check if the issue has already been analyzed
804
+ if not issue.analyzed:
805
+ # set analyzed to true
806
+ issue.analyzed = True
807
+
808
+ # check if the RegScale defenderId was recommended by Microsoft Defender
809
+ if issue.data.get(issue.integration_field) not in defender_data_dict and issue.data["status"] not in [
810
+ "Closed",
811
+ "Cancelled",
812
+ ]:
813
+ # the RegScale issue is no longer being recommended and the issue
814
+ # status is not closed or cancelled, we need to close the issue
815
+ change_issue_status(
816
+ api=api,
817
+ status="Closed",
818
+ issue=issue.data,
819
+ rec=defender_data_dict.get(issue.data.get(issue.integration_field)),
820
+ rec_type=issue.init_key,
821
+ )
822
+ job_progress.update(task, advance=1)
823
+
824
+
825
+ def change_issue_status(
826
+ api: Api,
827
+ status: str,
828
+ issue: dict,
829
+ rec: Optional[DefenderData] = None,
830
+ rec_type: str = None,
831
+ ) -> None:
832
+ """
833
+ Function to change a RegScale issue to the provided status
834
+
835
+ :param Api api: API object
836
+ :param str status: Status to change the provided issue to
837
+ :param dict issue: RegScale issue
838
+ :param dict rec: Microsoft Defender recommendation, defaults to None
839
+ :param str rec_type: The platform of Microsoft Defender (cloud or 365), defaults to None
840
+ :rtype: None
841
+ """
842
+ # update issue last updated time, set user to current user and change status
843
+ # to the status that was passed
844
+ issue["lastUpdatedById"] = api.config["userId"]
845
+ issue["dateLastUpdated"] = get_current_datetime(DATE_FORMAT)
846
+ issue["status"] = status
847
+
848
+ if not rec:
849
+ return
850
+ rec = rec.data
851
+
852
+ # check if rec dictionary was passed, if not create it
853
+ if rec_type == "defender365":
854
+ issue["title"] = rec["recommendationName"]
855
+ issue["description"] = format_description(defender_data=rec, tenant_id=api.config["azure365TenantId"])
856
+ issue["severityLevel"] = Issue.assign_severity(rec["severityScore"])
857
+ issue["issueOwnerId"] = api.config["userId"]
858
+ issue["dueDate"] = get_due_date(score=rec["severityScore"], config=api.config, key="defender365")
859
+ elif rec_type == "defenderCloud":
860
+ issue["title"] = (f'{rec["properties"]["productName"]} Alert - {rec["properties"]["compromisedEntity"]}',)
861
+ issue["description"] = format_description(defender_data=rec, tenant_id=api.config["azureCloudTenantId"])
862
+ issue["severityLevel"] = (Issue.assign_severity(rec["properties"]["severity"]),)
863
+ issue["issueOwnerId"] = api.config["userId"]
864
+ issue["dueDate"] = get_due_date(
865
+ score=rec["properties"]["severity"],
866
+ config=api.config,
867
+ key="defenderCloud",
868
+ )
869
+
870
+ # if we are closing the issue, update the date completed
871
+ if status.lower() == "closed":
872
+ if rec_type == "defender365":
873
+ message = "via Microsoft 365 Defender"
874
+ elif rec_type == "defenderCloud":
875
+ message = "via Microsoft Defender for Cloud"
876
+ else:
877
+ message = "via Microsoft Defender"
878
+ issue["dateCompleted"] = get_current_datetime(DATE_FORMAT)
879
+ issue["description"] += f'<p>No longer reported {message} as of {get_current_datetime("%b %d,%Y")}</p>'
880
+ closed.append(issue)
881
+ else:
882
+ issue["dateCompleted"] = ""
883
+ updated.append(issue)
884
+
885
+ # use the api to change the status of the given issue
886
+ Issue(**issue).save()
887
+
888
+
889
+ def prep_issues_for_creation(def_data: DefenderData, args: Tuple) -> None:
890
+ """
891
+ Function to utilize threading and create an issues in RegScale for the assigned thread
892
+
893
+ :param DefenderData def_data: Microsoft Defender data to create an issue for
894
+ :param Tuple args: Tuple of args to use during the process
895
+ :rtype: None
896
+ """
897
+ # set up local variables from args passed
898
+ mapping_func, config, defender_key, parent_id, parent_module, task = args
899
+
900
+ # set the recommendation for the thread for later use in the function
901
+ description = format_description(defender_data=def_data.data, tenant_id=config["azure365TenantId"])
902
+
903
+ # check if the recommendation was already created as a RegScale issue
904
+ if not def_data.created:
905
+ # set created flag to true
906
+ def_data.created = True
907
+
908
+ # set up the data payload for RegScale API
909
+ issue = mapping_func(data=def_data, config=config, description=description)
910
+ issue.__setattr__(def_data.integration_field, def_data.data[defender_key])
911
+ if parent_id and parent_module:
912
+ issue.parentId = parent_id
913
+ issue.parentModule = parent_module
914
+ issues_to_create.append(issue)
915
+ job_progress.update(task, advance=1)
916
+
917
+
918
+ def map_365_alert_to_issue(data: DefenderData, config: dict, description: str) -> Issue:
919
+ """
920
+ Function to map a Microsoft 365 Defender alert to a RegScale issue
921
+
922
+ :param DefenderData data: Microsoft Defender recommendation
923
+ :param dict config: Application config
924
+ :param str description: Description of the alert
925
+ :return: RegScale issue object
926
+ :rtype: Issue
927
+ """
928
+ return Issue(
929
+ title=f'{data.data["title"]}',
930
+ description=description,
931
+ severityLevel=Issue.assign_severity(data.data["severity"]),
932
+ dueDate=get_due_date(score=data.data["severity"], config=config, key=data.init_key),
933
+ identification=IDENTIFICATION_TYPE,
934
+ assetIdentifier=f'Machine ID:{data.data["machineId"]}\n'
935
+ f'DNS Name({data.data.get("computerDnsName", "No DNS Name found")})',
936
+ status=config["issues"][data.init_key]["status"],
937
+ sourceReport="Microsoft Defender 365 Alert",
938
+ )
939
+
940
+
941
+ def map_365_recommendation_to_issue(data: DefenderData, config: dict, description: str) -> Issue:
942
+ """
943
+ Function to map a Microsoft 365 Defender recommendation to a RegScale issue
944
+
945
+ :param DefenderData data: Microsoft Defender recommendation
946
+ :param dict config: Application config
947
+ :param str description: Description of the recommendation
948
+ :return: RegScale issue object
949
+ :rtype: Issue
950
+ """
951
+ severity = data.data["severityScore"]
952
+ return Issue(
953
+ title=f'{data.data["recommendationName"]}',
954
+ description=description,
955
+ severityLevel=Issue.assign_severity(severity),
956
+ dueDate=get_due_date(score=severity, config=config, key=data.init_key),
957
+ identification=IDENTIFICATION_TYPE,
958
+ status=config["issues"][data.init_key]["status"],
959
+ vendorName=data.data["vendor"],
960
+ sourceReport="Microsoft Defender 365 Recommendation",
961
+ )
962
+
963
+
964
+ def map_cloud_alert_to_issue(data: DefenderData, config: dict, description: str) -> Issue:
965
+ """
966
+ Function to map a Microsoft Defender for Cloud alert to a RegScale issue
967
+
968
+ :param DefenderData data: Microsoft Defender for Cloud alert
969
+ :param dict config: Application config
970
+ :param str description: Description of the alert
971
+ :return: RegScale issue object
972
+ :rtype: Issue
973
+ """
974
+ severity = data.data["properties"]["severity"]
975
+ return Issue(
976
+ title=f'{data.data["properties"]["productName"]} Alert - {data.data["properties"]["compromisedEntity"]}',
977
+ description=description,
978
+ severityLevel=Issue.assign_severity(severity),
979
+ dueDate=get_due_date(
980
+ score=severity,
981
+ config=config,
982
+ key=data.init_key,
983
+ ),
984
+ assetIdentifier="\n".join(
985
+ resource["azureResourceId"]
986
+ for resource in data.data["properties"].get("resourceIdentifiers", [])
987
+ if "azureResourceId" in resource
988
+ ),
989
+ recommendedActions="\n".join(data.data["properties"].get("remediationSteps", [])),
990
+ identification=IDENTIFICATION_TYPE,
991
+ status=config["issues"]["defenderCloud"]["status"],
992
+ vendorName=data.data["properties"]["vendorName"],
993
+ sourceReport="Microsoft Defender for Cloud Alert",
994
+ otherIdentifier=data.data["id"],
995
+ )
996
+
997
+
998
+ def map_cloud_recommendation_to_issue(data: DefenderData, config: dict, description: str) -> Issue:
999
+ """
1000
+ Function to map a Microsoft Defender for Cloud alert to a RegScale issue
1001
+
1002
+ :param DefenderData data: Microsoft Defender for Cloud alert
1003
+ :param dict config: Application config
1004
+ :param str description: Description of the alert
1005
+ :return: RegScale issue object
1006
+ :rtype: Issue
1007
+ """
1008
+ metadata = data.data["properties"].get("metadata", {})
1009
+ severity = metadata.get("severity")
1010
+ resource_details = data.data["properties"].get("resourceDetails", {})
1011
+ res_parts = [
1012
+ resource_details.get("ResourceProvider"),
1013
+ resource_details.get("ResourceType"),
1014
+ resource_details.get("ResourceName"),
1015
+ ]
1016
+ res_parts = filter(None, res_parts)
1017
+ title = f"{metadata.get('displayName')}{' on ' if res_parts else ''}{'/'.join(res_parts)}"
1018
+ return Issue(
1019
+ title=title,
1020
+ description=description,
1021
+ severityLevel=Issue.assign_severity(severity),
1022
+ dueDate=get_due_date(
1023
+ score=severity,
1024
+ config=config,
1025
+ key=data.init_key,
1026
+ ),
1027
+ identification=IDENTIFICATION_TYPE,
1028
+ status=config["issues"]["defenderCloud"]["status"],
1029
+ recommendedActions=metadata.get("remediationDescription"),
1030
+ assetIdentifier=resource_details.get("Id"),
1031
+ sourceReport=CLOUD_RECS,
1032
+ manualDetectionId=data.id,
1033
+ manualDetectionSource=CLOUD_RECS,
1034
+ otherIdentifier=data.data["id"],
1035
+ )
1036
+
1037
+
1038
+ def fetch_resources_from_azure(
1039
+ api: Api, headers: dict, query: Optional[str] = None, skip_token: Optional[str] = None, record_count: int = 0
1040
+ ) -> list[dict]:
1041
+ """
1042
+ Function to fetch Microsoft Defender resources from Azure
1043
+
1044
+ :param Api api: API object
1045
+ :param dict headers: Headers used for API call
1046
+ :param Optional[str] query: Query to use for the API call, if none provided,
1047
+ :param Optional[str] skip_token: Token to skip results, used during pagination, defaults to None
1048
+ :param int record_count: Number of records fetched, defaults to 0, used for logging during pagination
1049
+ :return: list of Microsoft Defender resources
1050
+ :rtype: list[dict]
1051
+ """
1052
+ url = "https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2024-04-01"
1053
+ if query:
1054
+ payload = {"query": query}
1055
+ else:
1056
+ payload = {
1057
+ "query": query or "resources",
1058
+ "subscriptions": [api.config["azureCloudSubscriptionId"]],
1059
+ }
1060
+ if skip_token:
1061
+ payload["options"] = {"$skipToken": skip_token}
1062
+ api.logger.info("Retrieving more Microsoft Defender resources from Azure...")
1063
+ else:
1064
+ api.logger.info("Retrieving Microsoft Defender resources from Azure...")
1065
+ response = api.post(url=url, headers=headers, json=payload)
1066
+ if response.status_code != 200:
1067
+ error_and_exit(
1068
+ f"Received unexpected response from Microsoft Defender.\n{response.status_code}:{response.reason}"
1069
+ + f"\n{response.text}",
1070
+ )
1071
+ try:
1072
+ response_data = response.json()
1073
+ total_records = response_data.get("totalRecords", 0)
1074
+ count = response_data.get("count", 0)
1075
+ api.logger.info(f"Received {count + record_count}/{total_records} items from Microsoft Defender.")
1076
+ # try to get the values from the api response
1077
+ defender_data = response_data["data"]
1078
+ except JSONDecodeError:
1079
+ # notify user if there was a json decode error from API response and exit
1080
+ error_and_exit("JSON Decode error")
1081
+ except KeyError:
1082
+ # notify user there was no data from API response and exit
1083
+ error_and_exit(
1084
+ f"Received unexpected response from Microsoft Defender.\n{response.status_code}: {response.reason}\n"
1085
+ + f"{response.text}"
1086
+ )
1087
+ # check if pagination is required to fetch all data from Microsoft Defender
1088
+ skip_token = response_data.get("$skipToken")
1089
+ if response.status_code == 200 and skip_token:
1090
+ # get the rest of the data
1091
+ defender_data.extend(
1092
+ fetch_resources_from_azure(api=api, headers=headers, query=query, skip_token=skip_token, record_count=count)
1093
+ )
1094
+ # return the defender recommendations
1095
+ return defender_data
1096
+
1097
+
1098
+ def map_asset(data: dict, existing_assets: dict[str, Asset]) -> Asset:
1099
+ """
1100
+ Function to map data to an Asset object
1101
+
1102
+ :param dict data: Data from Microsoft Defender
1103
+ :param dict[str, Asset] existing_assets: Existing assets from RegScale
1104
+ :return: Asset object
1105
+ :rtype: Asset
1106
+ """
1107
+ asset_id = data.get("id")
1108
+ properties = data.get("properties", {})
1109
+ resource_type = data.get("type", "").lower()
1110
+ try:
1111
+ ip_mapping = {
1112
+ "microsoft.network/networksecuritygroups": properties.get("securityRules", [{}])[0]
1113
+ .get("properties", {})
1114
+ .get("destinationAddressPrefix"),
1115
+ "microsoft.network/virtualnetworks": properties.get("addressSpace", {}).get("addressPrefixes"),
1116
+ "microsoft.app/managedenvironments": properties.get("staticIp"),
1117
+ "microsoft.network/networkinterfaces": properties.get("ipConfigurations", [{}])[0]
1118
+ .get("properties", {})
1119
+ .get("privateIPAddress"),
1120
+ }
1121
+ except IndexError:
1122
+ ip_mapping = {}
1123
+ try:
1124
+ fqdn_mapping = {
1125
+ "microsoft.keyvault/vaults": properties.get("vaultUri"),
1126
+ "microsoft.storage/storageaccounts": properties.get("primaryEndpoints", {}).get("blob"),
1127
+ "microsoft.appconfiguration/configurationstores": properties.get("endpoint"),
1128
+ "microsoft.dbforpostgresql/flexibleservers": properties.get("fullyQualifiedDomainName"),
1129
+ AFD_ENDPOINTS: properties.get("hostName"),
1130
+ "microsoft.containerregistry/registries": properties.get("loginServer"),
1131
+ "microsoft.app/containerapps": properties.get("configuration", {}).get("ingress", {}).get("fqdn"),
1132
+ "microsoft.network/privatednszones": data.get("name"),
1133
+ "microsoft.cognitiveservices/accounts": properties.get("endpoint"),
1134
+ }
1135
+ except IndexError:
1136
+ fqdn_mapping = {}
1137
+ # pylint: disable=line-too-long
1138
+ function_mapping = {
1139
+ "microsoft.network/privateendpoints": "Private endpoint that links the private link and the nic together",
1140
+ "microsoft.network/networkinterfaces": "Network Interface that connects to everything internal to the resource group",
1141
+ "microsoft.network/privatednszones": "Dns zone that will connect to the private endpoint and network interfaces",
1142
+ "microsoft.network/privatednszones/virtualnetworklinks": "Link for the Private DNS zone back to the vnet",
1143
+ "microsoft.app/containerapps": "Application runner that houses the running Docker Container",
1144
+ "microsoft.network/publicipaddresses": "Public ip address used for load balancing the container apps",
1145
+ "microsoft.storage/storageaccounts": "Storage blob to house unstructured files uploaded to the platform",
1146
+ "microsoft.network/networksecuritygroups": "Network protection for internal communications and load balancing",
1147
+ "microsoft.network/networkwatchers/flowlogs": "Logs that determine the flow of traffic",
1148
+ "microsoft.sql/servers/databases": "Database that houses application logs",
1149
+ "microsoft.network/virtualnetworks": "Network Interface that determines what the valid IP range is for all internal resources",
1150
+ "microsoft.portal/dashboards": "Dashboard that shows the status of the application and traffic",
1151
+ "microsoft.dataprotection/backupvaults": "Azure Blob Storage Account backup location",
1152
+ "microsoft.keyvault/vaults": "To securely store API keys, passwords, certificates, or cryptographic keys",
1153
+ "microsoft.managedidentity/userassignedidentities": "Identity that connects all internal resources in the resource group",
1154
+ "microsoft.app/managedenvironments": "Application environment to connect to the vnet",
1155
+ "microsoft.sql/servers": "Server that will house the database for the application logs",
1156
+ "microsoft.sql/servers/encryptionprotector": "Server encryption",
1157
+ "microsoft.appconfiguration/configurationstores": "Configure, store, and retrieve parameters and settings. Store configuration for all system components in the environment",
1158
+ "microsoft.insights/metricalerts": "Alerts that trigger when exceptions hit above 100",
1159
+ "microsoft.insights/webtests": "Test to ensure the integrity of the app and alert when availability drops",
1160
+ "microsoft.insights/components": "Insights and mapping for the data flow through the platform container application",
1161
+ "microsoft.dbforpostgresql/flexibleservers": "Application Database for OpenAI and Automation containers",
1162
+ "microsoft.network/loadbalancers": "Load Balancer that handles the load traffic for the containerapp",
1163
+ "microsoft.insights/activitylogalerts": "Alert rule to send an email to the Action Group when the trigger event happens",
1164
+ "microsoft.operationalinsights/workspaces": "Collection of Logs contained in a workspace",
1165
+ "microsoft.insights/actiongroups": "Action Group to send Emails to when alerts trigger",
1166
+ "microsoft.network/networkwatchers": "Monitor on the network to look for any suspecious activity",
1167
+ "microsoft.app/managedenvironments/certificates": "Tls cert for the application environment",
1168
+ "microsoft.authorization/roledefinitions": "Custom role definition",
1169
+ "microsoft.alertsmanagement/actionrules": "Alert Processing Rule to show when to trigger",
1170
+ "microsoft.network/frontdoorwebapplicationfirewallpolicies": "Waf protection policy that connects to the firewall and frontdoor",
1171
+ "microsoft.cdn/profiles": "Monitoring and controlling inbound and outbound traffic to the environment. Functions as a Web Application Firewall (WAF) and performs Network Address Translation (NAT) connecting public networks to a series of private tenant Virtual Networks (VNets)",
1172
+ "microsoft.resourcegraph/queries": "Query to return all resources in the SaaS subscription in the resource graph",
1173
+ "microsoft.network/firewallpolicies": "Firewall policy that connects to frontdoor and handles our traffic coming into the system",
1174
+ AFD_ENDPOINTS: "Endpoint that all of the routes attach to",
1175
+ "microsoft.containerregistry/registries": "House the Docker container image for ContainerApp pull",
1176
+ "microsoft.operationalinsights/querypacks": "Log analytics query that loads default queries for running",
1177
+ "microsoft.alertsmanagement/smartdetectoralertrules": "Failure Anomalies notifies you of an unusual rise in the rate of failed HTTP requests or dependency calls.",
1178
+ }
1179
+ # pylint: enable=line-too-long
1180
+ from regscale.models.regscale_models import AssetType, AssetCategory, AssetStatus
1181
+
1182
+ if asset_id in existing_assets:
1183
+ return existing_assets[asset_id]
1184
+ mapped_asset = Asset(
1185
+ extra_data={"type": f'{data.get("type")}'},
1186
+ id=0,
1187
+ description=generate_html_table_from_dict(data),
1188
+ status=AssetStatus.Active.value,
1189
+ name=data.get("name", asset_id),
1190
+ assetType=AssetType.Other,
1191
+ assetCategory=AssetCategory.Software,
1192
+ otherTrackingNumber=asset_id,
1193
+ softwareFunction=function_mapping.get(resource_type, properties.get("description")),
1194
+ ipAddress=str(ip_mapping.get(resource_type, properties.get("ipAddress"))),
1195
+ bPublicFacing=resource_type in ["microsoft.cdn/profiles", AFD_ENDPOINTS],
1196
+ bAuthenticatedScan=resource_type
1197
+ not in [
1198
+ "microsoft.alertsmanagement/actionrules",
1199
+ "microsoft.alertsmanagement/smartdetectoralertrules",
1200
+ ],
1201
+ bVirtual=True,
1202
+ baselineConfiguration="Azure Hardening Guide",
1203
+ )
1204
+ if fqdn := fqdn_mapping.get(resource_type, properties.get("dnsSettings", {}).get("fqdn")):
1205
+ mapped_asset.fqdn = fqdn
1206
+ mapped_asset.description += f"<p>FQDN: {fqdn}</p>"
1207
+ return mapped_asset
1208
+
1209
+
1210
+ def map_assets(data: list[dict], existing_assets: list[Asset], progress: Progress) -> list[Asset]:
1211
+ """
1212
+ Function to map data to an Asset object using threads
1213
+
1214
+ :param list[dict] data: Data from Microsoft Defender Resource APi
1215
+ :param list[Asset] existing_assets: List of existing assets, used to prevent duplicates
1216
+ :param Progress progress: Progress object to track progress
1217
+ :return: List of Asset objects
1218
+ :rtype: list[Asset]
1219
+ """
1220
+ existing_assets = {asset.otherTrackingNumber: asset for asset in existing_assets}
1221
+ from regscale.integrations.variables import ScannerVariables
1222
+
1223
+ with ThreadPoolExecutor(max_workers=ScannerVariables.threadMaxWorkers) as executor:
1224
+ futures = [executor.submit(map_asset, asset, existing_assets) for asset in data]
1225
+ mapping_assets = progress.add_task(
1226
+ f"[#f8b737]Mapping Microsoft Defender {len(data)} resource(s) to RegScale assets...", total=len(data)
1227
+ )
1228
+ assets = []
1229
+ for future in as_completed(futures):
1230
+ if result := future.result():
1231
+ assets.append(result)
1232
+ progress.update(mapping_assets, advance=1)
1233
+ logger.info(f"Mapped {len(assets)}/{len(data)} Microsoft Defender resource(s) to RegScale asset(s).")
1234
+ return assets
1235
+
1236
+
1237
+ def sync_resources(ssp_id: int):
1238
+ """
1239
+ Function to sync Microsoft Defender resources with RegScale assets
1240
+
1241
+ :param int ssp_id: The RegScale SSP ID to sync resources to
1242
+ :rtype: None
1243
+ """
1244
+ app = check_license()
1245
+ api = Api()
1246
+ # check if RegScale token is valid:
1247
+ if not is_valid(app=app):
1248
+ error_and_exit(LOGIN_ERROR)
1249
+ token = check_token(api=api, system="cloud")
1250
+ headers = {"Content-Type": APP_JSON, "Authorization": token}
1251
+ cloud_resources = fetch_resources_from_azure(api=api, headers=headers)
1252
+ app.logger.info(f"Retrieving assets from RegScale for security plan #{ssp_id}...")
1253
+ if assets := Asset.get_map(plan_id=ssp_id):
1254
+ assets = list(assets.values())
1255
+ with create_progress_object() as progress:
1256
+ logger.info(f"Retrieved {len(assets)} asset(s) from RegScale.")
1257
+ cloud_assets = map_assets(data=cloud_resources, existing_assets=assets, progress=progress)
1258
+ azure_comps = {asset.extra_data.get("type") for asset in cloud_assets if asset.extra_data.get("type")}
1259
+ api.logger.info("Fetching components from RegScale...")
1260
+ if existing_components := Component.get_map(plan_id=ssp_id):
1261
+ logger.info(f"Retrieved {len(existing_components)} component(s) from RegScale.")
1262
+ existing_components = list(existing_components.values())
1263
+ comp_mapping = {
1264
+ component.title: component for component in existing_components if component.title in azure_comps
1265
+ }
1266
+ logger.info(
1267
+ f"Found {len(comp_mapping)}/{len(azure_comps)} component(s) required for importing "
1268
+ "Microsoft Defender resources as asset(s) in RegScale."
1269
+ )
1270
+ else:
1271
+ existing_components = []
1272
+ comp_mapping = {}
1273
+ if missing_comps_mapping := map_missing_components(
1274
+ components=azure_comps,
1275
+ existing_components=existing_components,
1276
+ ssp_id=ssp_id,
1277
+ progress=progress,
1278
+ ):
1279
+ new_components = create_objects_with_threads(
1280
+ "components", list(missing_comps_mapping.values()), progress=progress
1281
+ )
1282
+ missing_comps_mapping = {component.description: component for component in new_components}
1283
+ comp_mapping.update(missing_comps_mapping)
1284
+ if assets_to_create := map_assets_to_components(
1285
+ assets=[asset for asset in cloud_assets if asset.id == 0],
1286
+ component_mapping=comp_mapping,
1287
+ ssp_id=ssp_id,
1288
+ progress=progress,
1289
+ ):
1290
+ new_assets = create_objects_with_threads("assets", assets_to_create, progress=progress)
1291
+ logger.info(f"Created {len(new_assets)}/{len(cloud_assets)} asset(s) in RegScale.")
1292
+ else:
1293
+ logger.info(f"[green]All {len(cloud_assets)} Microsoft Defender resource(s) already exist in RegScale.")
1294
+
1295
+
1296
+ def map_assets_to_components(
1297
+ assets: list[Asset], component_mapping: dict[str, Component], ssp_id: int, progress: Progress
1298
+ ) -> list[Asset]:
1299
+ """
1300
+ Function to map assets to components
1301
+
1302
+ :param list[Asset] assets: List of assets to map
1303
+ :param dict[str, Component] component_mapping: Dictionary of component titles and their corresponding component
1304
+ :param int ssp_id: The RegScale SSP ID to add the assets to, used if no component is found to map to
1305
+ :param Progress progress: Progress object to track progress
1306
+ :return: List of assets with updated parentIds and parentModules
1307
+ :rtype: list[Asset]
1308
+ """
1309
+ updated_assets = []
1310
+ if assets:
1311
+ mapping_assets = progress.add_task(
1312
+ f"[#f8b737]Mapping {len(assets)} asset(s) to RegScale components...", total=len(assets)
1313
+ )
1314
+ for asset in assets:
1315
+ if asset_type := asset.extra_data.get("type"):
1316
+ if component := component_mapping.get(asset_type):
1317
+ asset.extra_data["componentId"] = component.id
1318
+ asset.parentId = ssp_id
1319
+ asset.parentModule = "securityplans"
1320
+ updated_assets.append(asset)
1321
+ progress.update(mapping_assets, advance=1)
1322
+ logger.info(f"Updated parentIds and parentModules for {len(assets)} asset(s).")
1323
+ return updated_assets
1324
+
1325
+
1326
+ def map_missing_components(components: set, existing_components: list, ssp_id: int, progress: Progress) -> dict:
1327
+ """
1328
+ Function to create missing components in RegScale
1329
+
1330
+ :param set components: Set of expected components to create
1331
+ :param list existing_components: List of existing components
1332
+ :param int ssp_id: The RegScale SSP ID to add the components to
1333
+ :param Progress progress: Progress object to track progress
1334
+ :return: Dictionary of component titles and their corresponding component objects
1335
+ :rtype: dict
1336
+ """
1337
+ from regscale.models.regscale_models import ComponentType, ComponentStatus
1338
+
1339
+ missing_components = components - {component.title for component in existing_components}
1340
+ component_mapping = {}
1341
+ if missing_components:
1342
+ mapping_components = progress.add_task(
1343
+ f"[#ef5d23]Mapping {len(missing_components)} missing component(s)...", total=len(missing_components)
1344
+ )
1345
+ for component in missing_components:
1346
+ component_obj = Component(
1347
+ id=0,
1348
+ title=component,
1349
+ description=component,
1350
+ componentType=ComponentType.Software.value,
1351
+ status=ComponentStatus.Active.value,
1352
+ securityPlansId=ssp_id,
1353
+ )
1354
+ component_mapping[component] = component_obj
1355
+ progress.update(mapping_components, advance=1)
1356
+ logger.info(f"Mapped {len(component_mapping)}/{len(missing_components)} missing component(s).")
1357
+ return component_mapping
1358
+
1359
+
1360
+ def create_objects_with_threads(object_name: str, objects: list, progress: Progress) -> list:
1361
+ """
1362
+ Create a list of objects in RegScale using threads
1363
+
1364
+ :param str object_name: Type of object to create
1365
+ :param list objects: A list of objects to create
1366
+ :param Progress progress: Progress object to track progress
1367
+ :rtype: List of created objects
1368
+ :rtype: list
1369
+ """
1370
+ from regscale.integrations.variables import ScannerVariables
1371
+
1372
+ created_objects = []
1373
+ created_mappings = []
1374
+ failed_count = 0
1375
+ asset_component_ids = {
1376
+ obj.otherTrackingNumber: obj.extra_data["componentId"]
1377
+ for obj in objects
1378
+ if isinstance(obj, Asset) and obj.extra_data.get("componentId")
1379
+ }
1380
+ with ThreadPoolExecutor(max_workers=ScannerVariables.threadMaxWorkers) as executor:
1381
+ futures = [executor.submit(obj.create) for obj in objects]
1382
+ create_task = progress.add_task(f"[#21a5bb]Creating {len(objects)} {object_name}...", total=len(objects))
1383
+ for future in as_completed(futures):
1384
+ try:
1385
+ if future.result():
1386
+ res = future.result()
1387
+ if isinstance(res, Asset) and res.otherTrackingNumber in asset_component_ids:
1388
+ from regscale.models.regscale_models import AssetMapping
1389
+
1390
+ new_mapping = AssetMapping(
1391
+ assetId=res.id, componentId=asset_component_ids[res.otherTrackingNumber]
1392
+ ).create()
1393
+ created_mappings.append(new_mapping)
1394
+ elif isinstance(res, Component):
1395
+ from regscale.models.regscale_models import ComponentMapping
1396
+
1397
+ new_mapping = ComponentMapping(securityPlanId=res.securityPlansId, componentId=res.id).create()
1398
+ created_mappings.append(new_mapping)
1399
+ created_objects.append(res)
1400
+ else:
1401
+ failed_count += 1
1402
+ except Exception as e:
1403
+ logger.error(f"Failed to create {object_name[:-1]}: {e}")
1404
+ failed_count += 1
1405
+ progress.update(create_task, advance=1)
1406
+ logger.info(
1407
+ f"Created {len(created_objects)}/{len(objects)} {object_name}, {len(created_mappings)} mappings, and failed "
1408
+ f"to create {failed_count} {object_name}."
1409
+ )
1410
+ return created_objects
1411
+
1412
+
1413
+ def export_resources(parent_id: int, parent_module: str, query_name: str, no_upload: bool, all_queries: bool) -> None:
1414
+ """
1415
+ Export data from Microsoft Defender for Cloud queries and save them to a .csv file
1416
+
1417
+ :param int parent_id: The RegScale ID to save the data to
1418
+ :param str parent_module: The RegScale module to save the data to
1419
+ :param str query_name: The name of the query to export from Microsoft Defender for Cloud resource graph queries
1420
+ :param bool no_upload: Flag to skip uploading the exported .csv file to RegScale
1421
+ :param bool all_queries: If True, export all saved queries from Microsoft Defender for Cloud resource graph queries
1422
+ :rtype: None
1423
+ """
1424
+ app = check_license()
1425
+ api = Api()
1426
+ # check if RegScale token is valid:
1427
+ if not is_valid(app=app):
1428
+ error_and_exit(LOGIN_ERROR)
1429
+ token = check_token(api=api, system="cloud")
1430
+ headers = {"Content-Type": APP_JSON, "Authorization": token}
1431
+ url = f"https://management.azure.com/subscriptions/{api.config['azureCloudSubscriptionId']}/providers/Microsoft.ResourceGraph/queries?api-version=2024-04-01"
1432
+ response = api.get(url=url, headers=headers)
1433
+ if response.raise_for_status():
1434
+ response.raise_for_status()
1435
+ cloud_queries = response.json().get("value", [])
1436
+ if all_queries:
1437
+ for query in cloud_queries:
1438
+ fetch_save_and_upload_query(
1439
+ api=api,
1440
+ headers=headers,
1441
+ query=query,
1442
+ parent_id=parent_id,
1443
+ parent_module=parent_module,
1444
+ no_upload=no_upload,
1445
+ )
1446
+ else:
1447
+ query = prompt_user_for_query_selection(queries=cloud_queries, query_name=query_name)
1448
+ fetch_save_and_upload_query(
1449
+ api=api, headers=headers, query=query, parent_id=parent_id, parent_module=parent_module, no_upload=no_upload
1450
+ )
1451
+
1452
+
1453
+ def prompt_user_for_query_selection(queries: list, query_name: Optional[str] = None) -> dict:
1454
+ """
1455
+ Function to prompt the user to select a query from a list of queries
1456
+
1457
+ :param list queries: The list of queries to select from
1458
+ :param str query_name: The name of the query to select, defaults to None
1459
+ :return: The selected query
1460
+ :rtype: dict
1461
+ """
1462
+ if query_name and any(q for q in queries if q["name"].lower() == query_name.lower()):
1463
+ return next(q for q in queries if q["name"].lower() == query_name.lower())
1464
+ query = click.prompt("Select a query", type=click.Choice([query["name"] for query in queries]), show_choices=True)
1465
+ return next(q for q in queries if q["name"].lower() == query.lower())
1466
+
1467
+
1468
+ def fetch_save_and_upload_query(
1469
+ api: Api, headers: dict, query: dict, parent_id: int, parent_module: str, no_upload: bool
1470
+ ) -> None:
1471
+ """
1472
+ Function to fetch Microsoft Defender queries from Azure and save them to a .xlsx file
1473
+
1474
+ :param Api api: The API object, used to call Microsoft Defender
1475
+ :param dict headers: The headers to use for the request
1476
+ :param dict query: The query object to parse and run
1477
+ :param int parent_id: The RegScale ID to upload the results to
1478
+ :param str parent_module: The RegScale module to upload the results to
1479
+ :param bool no_upload: Flag to skip uploading the exported .csv file to RegScale
1480
+ :rtype: None
1481
+ """
1482
+ api.logger.info(f"Exporting data from Microsoft Defender for Cloud query: {query['name']}...")
1483
+ data = fetch_and_run_query(api=api, headers=headers, query=query)
1484
+ todays_date = get_current_datetime(dt_format="%Y%m%d")
1485
+ file_path = Path(f"./artifacts/{query['name']}_{todays_date}.csv")
1486
+ save_data_to(file=file_path, data=data, transpose_data=False)
1487
+ if not no_upload and File.upload_file_to_regscale(
1488
+ file_name=file_path,
1489
+ parent_id=parent_id,
1490
+ parent_module=parent_module,
1491
+ api=api,
1492
+ ):
1493
+ api.logger.info(f"Successfully uploaded {file_path.name} to {parent_module} #{parent_id} in RegScale.")
1494
+
1495
+
1496
+ def fetch_and_run_query(api: Api, headers: dict, query: dict) -> list[dict]:
1497
+ """
1498
+ Function to fetch Microsoft Defender queries from Azure and run them
1499
+
1500
+ :param Api api: The API object, used to call Microsoft Defender
1501
+ :param dict headers: The headers to use for the request
1502
+ :param dict query: The query object to parse and run
1503
+ :return: list of Microsoft Defender resources by using the query
1504
+ :rtype: list[dict]
1505
+ """
1506
+ url = f"https://management.azure.com/subscriptions/{query['subscriptionId']}/resourceGroups/{query['resourceGroup']}/providers/Microsoft.ResourceGraph/queries/{query['name']}?api-version=2024-04-01"
1507
+ response = api.get(url=url, headers=headers)
1508
+ if response.raise_for_status():
1509
+ response.raise_for_status()
1510
+ query = response.json().get("properties", {}).get("query")
1511
+ return fetch_resources_from_azure(api=api, headers=headers, query=query)