regscale-cli 6.21.2.0__py3-none-any.whl → 6.28.2.1__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.
Files changed (314) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/api.py +5 -2
  4. regscale/core/app/application.py +36 -6
  5. regscale/core/app/internal/control_editor.py +73 -21
  6. regscale/core/app/internal/evidence.py +727 -204
  7. regscale/core/app/internal/login.py +4 -2
  8. regscale/core/app/internal/model_editor.py +219 -64
  9. regscale/core/app/utils/app_utils.py +86 -12
  10. regscale/core/app/utils/catalog_utils/common.py +1 -1
  11. regscale/core/login.py +21 -4
  12. regscale/core/utils/async_graphql_client.py +363 -0
  13. regscale/core/utils/date.py +77 -1
  14. regscale/dev/cli.py +26 -0
  15. regscale/dev/code_gen.py +109 -24
  16. regscale/dev/version.py +72 -0
  17. regscale/integrations/commercial/__init__.py +30 -2
  18. regscale/integrations/commercial/aws/audit_manager_compliance.py +3908 -0
  19. regscale/integrations/commercial/aws/cli.py +3107 -54
  20. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  21. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  22. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  23. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  24. regscale/integrations/commercial/{amazon → aws}/common.py +71 -19
  25. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  26. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  27. regscale/integrations/commercial/aws/control_compliance_analyzer.py +439 -0
  28. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  29. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  30. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  31. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  32. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  33. regscale/integrations/commercial/aws/inventory/__init__.py +338 -22
  34. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  35. regscale/integrations/commercial/aws/inventory/resources/analytics.py +390 -0
  36. regscale/integrations/commercial/aws/inventory/resources/applications.py +234 -0
  37. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  38. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  39. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  40. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  41. regscale/integrations/commercial/aws/inventory/resources/compute.py +328 -9
  42. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  43. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  44. regscale/integrations/commercial/aws/inventory/resources/database.py +481 -31
  45. regscale/integrations/commercial/aws/inventory/resources/developer_tools.py +253 -0
  46. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  47. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  48. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  49. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  50. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  51. regscale/integrations/commercial/aws/inventory/resources/machine_learning.py +358 -0
  52. regscale/integrations/commercial/aws/inventory/resources/networking.py +390 -67
  53. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  54. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  55. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  56. regscale/integrations/commercial/aws/inventory/resources/storage.py +288 -29
  57. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  58. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  59. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  60. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  61. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  62. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  63. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  64. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  65. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  66. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  67. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  68. regscale/integrations/commercial/aws/scanner.py +1072 -205
  69. regscale/integrations/commercial/aws/security_hub.py +319 -0
  70. regscale/integrations/commercial/aws/session_manager.py +282 -0
  71. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  72. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  73. regscale/integrations/commercial/jira.py +489 -153
  74. regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
  75. regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
  76. regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
  77. regscale/integrations/commercial/qualys/__init__.py +167 -68
  78. regscale/integrations/commercial/qualys/scanner.py +305 -39
  79. regscale/integrations/commercial/sarif/sairf_importer.py +432 -0
  80. regscale/integrations/commercial/sarif/sarif_converter.py +67 -0
  81. regscale/integrations/commercial/sicura/api.py +79 -42
  82. regscale/integrations/commercial/sicura/commands.py +8 -2
  83. regscale/integrations/commercial/sicura/scanner.py +83 -44
  84. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  85. regscale/integrations/commercial/synqly/assets.py +133 -16
  86. regscale/integrations/commercial/synqly/edr.py +2 -8
  87. regscale/integrations/commercial/synqly/query_builder.py +536 -0
  88. regscale/integrations/commercial/synqly/ticketing.py +27 -0
  89. regscale/integrations/commercial/synqly/vulnerabilities.py +165 -28
  90. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  91. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  92. regscale/integrations/commercial/tenablev2/commands.py +146 -5
  93. regscale/integrations/commercial/tenablev2/scanner.py +1 -3
  94. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  95. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  96. regscale/integrations/commercial/wizv2/click.py +191 -76
  97. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  98. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  99. regscale/integrations/commercial/wizv2/compliance_report.py +1592 -0
  100. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  101. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +7 -3
  102. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +92 -89
  103. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  104. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  105. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +66 -9
  106. regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
  107. regscale/integrations/commercial/wizv2/issue.py +776 -28
  108. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  109. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  110. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  111. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  112. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  113. regscale/integrations/commercial/wizv2/reports.py +243 -0
  114. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  115. regscale/integrations/commercial/wizv2/scanner.py +1031 -441
  116. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  117. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  118. regscale/integrations/commercial/wizv2/variables.py +89 -3
  119. regscale/integrations/compliance_integration.py +1036 -151
  120. regscale/integrations/control_matcher.py +432 -0
  121. regscale/integrations/due_date_handler.py +333 -0
  122. regscale/integrations/milestone_manager.py +291 -0
  123. regscale/integrations/public/__init__.py +14 -0
  124. regscale/integrations/public/cci_importer.py +834 -0
  125. regscale/integrations/public/csam/__init__.py +0 -0
  126. regscale/integrations/public/csam/csam.py +938 -0
  127. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  128. regscale/integrations/public/csam/csam_common.py +154 -0
  129. regscale/integrations/public/csam/csam_controls.py +432 -0
  130. regscale/integrations/public/csam/csam_poam.py +124 -0
  131. regscale/integrations/public/fedramp/click.py +77 -6
  132. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  133. regscale/integrations/public/fedramp/fedramp_cis_crm.py +675 -289
  134. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  135. regscale/integrations/public/fedramp/poam/scanner.py +75 -7
  136. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  137. regscale/integrations/scanner_integration.py +1961 -430
  138. regscale/models/integration_models/CCI_List.xml +1 -0
  139. regscale/models/integration_models/aqua.py +2 -2
  140. regscale/models/integration_models/cisa_kev_data.json +805 -11
  141. regscale/models/integration_models/flat_file_importer/__init__.py +5 -8
  142. regscale/models/integration_models/nexpose.py +36 -10
  143. regscale/models/integration_models/qualys.py +3 -4
  144. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  145. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +87 -18
  146. regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
  147. regscale/models/integration_models/synqly_models/ocsf_mapper.py +124 -25
  148. regscale/models/integration_models/synqly_models/synqly_model.py +89 -16
  149. regscale/models/locking.py +12 -8
  150. regscale/models/platform.py +4 -2
  151. regscale/models/regscale_models/__init__.py +7 -0
  152. regscale/models/regscale_models/assessment.py +2 -1
  153. regscale/models/regscale_models/catalog.py +1 -1
  154. regscale/models/regscale_models/compliance_settings.py +251 -1
  155. regscale/models/regscale_models/component.py +1 -0
  156. regscale/models/regscale_models/control_implementation.py +236 -41
  157. regscale/models/regscale_models/control_objective.py +74 -5
  158. regscale/models/regscale_models/file.py +2 -0
  159. regscale/models/regscale_models/form_field_value.py +5 -3
  160. regscale/models/regscale_models/inheritance.py +44 -0
  161. regscale/models/regscale_models/issue.py +301 -102
  162. regscale/models/regscale_models/milestone.py +33 -14
  163. regscale/models/regscale_models/organization.py +3 -0
  164. regscale/models/regscale_models/regscale_model.py +310 -73
  165. regscale/models/regscale_models/security_plan.py +4 -2
  166. regscale/models/regscale_models/vulnerability.py +3 -3
  167. regscale/regscale.py +25 -4
  168. regscale/templates/__init__.py +0 -0
  169. regscale/utils/threading/threadhandler.py +20 -15
  170. regscale/validation/record.py +23 -1
  171. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/METADATA +17 -33
  172. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/RECORD +310 -111
  173. tests/core/__init__.py +0 -0
  174. tests/core/utils/__init__.py +0 -0
  175. tests/core/utils/test_async_graphql_client.py +472 -0
  176. tests/fixtures/test_fixture.py +13 -8
  177. tests/regscale/core/test_login.py +171 -4
  178. tests/regscale/integrations/commercial/__init__.py +0 -0
  179. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  180. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  181. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  182. tests/regscale/integrations/commercial/aws/test_aws_analytics_collector.py +260 -0
  183. tests/regscale/integrations/commercial/aws/test_aws_applications_collector.py +242 -0
  184. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  185. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  186. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  187. tests/regscale/integrations/commercial/aws/test_aws_developer_tools_collector.py +203 -0
  188. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  189. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  190. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  191. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  192. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  193. tests/regscale/integrations/commercial/aws/test_aws_machine_learning_collector.py +237 -0
  194. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  195. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  196. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  197. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  198. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  199. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  200. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  201. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  202. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  203. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  204. tests/regscale/integrations/commercial/aws/test_control_compliance_analyzer.py +375 -0
  205. tests/regscale/integrations/commercial/aws/test_datetime_parsing.py +223 -0
  206. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  207. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  208. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  209. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  210. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  211. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  212. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  213. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  214. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  215. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  216. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  217. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  218. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  219. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  220. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  221. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  222. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  223. tests/regscale/integrations/commercial/conftest.py +28 -0
  224. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  225. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  226. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  227. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  228. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  229. tests/regscale/integrations/commercial/test_aws.py +3742 -0
  230. tests/regscale/integrations/commercial/test_burp.py +48 -0
  231. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  232. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  233. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  234. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  235. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  236. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  237. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  238. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  239. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  240. tests/regscale/integrations/commercial/test_sicura.py +349 -0
  241. tests/regscale/integrations/commercial/test_snow.py +423 -0
  242. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  243. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  244. tests/regscale/integrations/commercial/test_stig.py +33 -0
  245. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  246. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  247. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  248. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  249. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  250. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  251. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  252. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  253. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  254. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  255. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  256. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  257. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  258. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  259. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  260. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  261. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  262. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  263. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  264. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  265. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  266. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  267. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  268. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  269. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  270. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  271. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  272. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  273. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  274. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  275. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  276. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  277. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  278. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  279. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  280. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1218 -0
  281. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  282. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  283. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  284. tests/regscale/integrations/public/__init__.py +0 -0
  285. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  286. tests/regscale/integrations/public/fedramp/test_gen_asset_list.py +150 -0
  287. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  288. tests/regscale/integrations/public/test_alienvault.py +220 -0
  289. tests/regscale/integrations/public/test_cci.py +1053 -0
  290. tests/regscale/integrations/public/test_cisa.py +1021 -0
  291. tests/regscale/integrations/public/test_emass.py +518 -0
  292. tests/regscale/integrations/public/test_fedramp.py +1152 -0
  293. tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
  294. tests/regscale/integrations/public/test_file_uploads.py +506 -0
  295. tests/regscale/integrations/public/test_oscal.py +453 -0
  296. tests/regscale/integrations/test_compliance_status_mapping.py +406 -0
  297. tests/regscale/integrations/test_control_matcher.py +1421 -0
  298. tests/regscale/integrations/test_control_matching.py +155 -0
  299. tests/regscale/integrations/test_milestone_manager.py +408 -0
  300. tests/regscale/models/test_control_implementation.py +118 -3
  301. tests/regscale/models/test_form_field_value_integration.py +304 -0
  302. tests/regscale/models/test_issue.py +378 -1
  303. tests/regscale/models/test_module_integration.py +582 -0
  304. tests/regscale/models/test_tenable_integrations.py +811 -105
  305. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3057
  306. regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +0 -7388
  307. regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +0 -9605
  308. regscale/integrations/public/fedramp/parts_mapper.py +0 -107
  309. /regscale/integrations/commercial/{amazon → sarif}/__init__.py +0 -0
  310. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  311. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/LICENSE +0 -0
  312. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/WHEEL +0 -0
  313. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/entry_points.txt +0 -0
  314. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1517 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Tests for Microsoft Defender integration in RegScale CLI"""
4
+ # standard python imports
5
+ import contextlib
6
+ import os
7
+ import shutil
8
+ from datetime import datetime, timedelta
9
+ from unittest.mock import MagicMock, patch
10
+
11
+ import pytest
12
+ from pathlib import Path
13
+ from regscale.integrations.commercial.microsoft_defender.defender import (
14
+ authenticate,
15
+ change_issue_status,
16
+ collect_and_upload_entra_evidence,
17
+ collect_specific_evidence_type,
18
+ compare_defender_and_regscale,
19
+ create_html_table,
20
+ create_payload,
21
+ evaluate_open_issues,
22
+ export_resources,
23
+ fetch_save_and_upload_query,
24
+ format_description,
25
+ get_control_implementations_map,
26
+ get_defender_url,
27
+ get_due_date,
28
+ import_defender_alerts,
29
+ map_365_alert_to_issue,
30
+ map_365_recommendation_to_issue,
31
+ map_cloud_alert_to_issue,
32
+ map_cloud_recommendation_to_issue,
33
+ prep_issues_for_creation,
34
+ process_list_value,
35
+ prompt_user_for_query_selection,
36
+ show_entra_mappings,
37
+ sync_defender_and_regscale,
38
+ upload_evidence_files,
39
+ upload_evidence_to_controls,
40
+ )
41
+ from regscale.integrations.commercial.microsoft_defender.defender_api import DefenderApi
42
+
43
+ from regscale.core.app.application import Application
44
+ from regscale.models import Issue, IssueSeverity
45
+ from regscale.models.integration_models.defender_data import DefenderData
46
+ from tests import CLITestFixture
47
+
48
+ PATH = "regscale.integrations.commercial.microsoft_defender.defender"
49
+
50
+
51
+ @pytest.mark.no_parallel
52
+ class TestDefender(CLITestFixture):
53
+ security_plan = None
54
+
55
+ @pytest.fixture(scope="class")
56
+ def create_security_plan(self, request, generate_uuid):
57
+ """Mock create_security_plan fixture to avoid real API calls in parallel tests"""
58
+ # Create a mock security plan that doesn't require API calls
59
+ mock_security_plan = MagicMock()
60
+ mock_security_plan.id = 12345 # Mock ID
61
+ mock_security_plan.get_module_string.return_value = "security_plans" # Mock module
62
+ yield mock_security_plan
63
+
64
+ @pytest.fixture(autouse=True)
65
+ def setup_ssp(self, create_security_plan):
66
+ self.security_plan = create_security_plan
67
+
68
+ @property
69
+ def PARENT_ID(self):
70
+ """Get the parent ID from the existing SSP"""
71
+ return self.security_plan.id
72
+
73
+ @property
74
+ def PARENT_MODULE(self):
75
+ """Get the parent module from the existing SSP"""
76
+ return self.security_plan.get_module_string()
77
+
78
+ def test_init(self):
79
+ """Test init file and config"""
80
+ self.verify_config(
81
+ [
82
+ "azure365TenantId",
83
+ "azure365ClientId",
84
+ "azure365Secret",
85
+ "azureCloudTenantId",
86
+ "azureCloudClientId",
87
+ "azureCloudSecret",
88
+ "azureCloudSubscriptionId",
89
+ ]
90
+ )
91
+
92
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
93
+ @patch(f"{PATH}.DefenderApi")
94
+ def test_authenticate_365(self, mock_defender_api, mock_check_license):
95
+ """Test authenticating with Microsoft Defender 365"""
96
+ mock_api_instance = MagicMock()
97
+ mock_defender_api.return_value = mock_api_instance
98
+
99
+ authenticate(system="365")
100
+
101
+ mock_check_license.assert_called_once()
102
+ mock_defender_api.assert_called_once_with(system="365")
103
+ mock_api_instance.get_token.assert_called_once()
104
+
105
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
106
+ @patch(f"{PATH}.DefenderApi")
107
+ def test_authenticate_cloud(self, mock_defender_api, mock_check_license):
108
+ """Test authenticating with Microsoft Defender for Cloud"""
109
+ mock_api_instance = MagicMock()
110
+ mock_defender_api.return_value = mock_api_instance
111
+
112
+ authenticate(system="cloud")
113
+
114
+ mock_check_license.assert_called_once()
115
+ mock_defender_api.assert_called_once_with(system="cloud")
116
+ mock_api_instance.get_token.assert_called_once()
117
+
118
+ @pytest.mark.parametrize(
119
+ "score,expected_days",
120
+ [
121
+ (9, 7), # high score
122
+ (5, 14), # moderate score
123
+ (2, 30), # low score
124
+ (None, 30), # None score
125
+ ("high", 7), # string high
126
+ ("medium", 14), # string medium
127
+ ("low", 30), # string low
128
+ ("unknown", 30), # unknown string
129
+ ],
130
+ )
131
+ def test_get_due_date(self, score, expected_days):
132
+ """Test getting due date based on severity score"""
133
+ config = {"issues": {"defender365": {"high": 7, "moderate": 14, "low": 30}}}
134
+
135
+ result = get_due_date(score=score, config=config, key="defender365")
136
+
137
+ # Parse the result date and calculate expected date
138
+ today = datetime.now().strftime("%m/%d/%y")
139
+ expected_date = datetime.strptime(today, "%m/%d/%y") + timedelta(days=expected_days)
140
+ expected_date_str = expected_date.strftime("%Y-%m-%dT%H:%M:%S")
141
+
142
+ assert result == expected_date_str
143
+
144
+ def test_format_description(self):
145
+ """Test formatting description with HTML table"""
146
+ defender_data = {
147
+ "id": "test-id",
148
+ "properties": {"alertUri": "https://example.com/alert", "name": "Test Alert", "severity": "High"},
149
+ }
150
+ tenant_id = "test-tenant-id"
151
+
152
+ result = format_description(defender_data=defender_data, tenant_id=tenant_id)
153
+
154
+ assert '<table style="border: 1px solid;">' in result
155
+ assert "test-id" in result
156
+ assert "Test Alert" in result
157
+ assert "High" in result
158
+ assert "https://example.com/alert" in result
159
+
160
+ def test_get_defender_url_with_alert_uri(self):
161
+ """Test getting defender URL when alertUri is present"""
162
+ rec = {"properties": {"alertUri": "https://security.microsoft.com/alerts/test-alert"}}
163
+ tenant_id = "test-tenant-id"
164
+
165
+ result = get_defender_url(rec=rec, tenant_id=tenant_id)
166
+
167
+ expected = '<a href="https://security.microsoft.com/alerts/test-alert">https://security.microsoft.com/alerts/test-alert</a>'
168
+ assert result == expected
169
+
170
+ def test_get_defender_url_without_alert_uri(self):
171
+ """Test getting defender URL when alertUri is not present"""
172
+ rec = {"properties": {}}
173
+ tenant_id = "test-tenant-id"
174
+
175
+ result = get_defender_url(rec=rec, tenant_id=tenant_id)
176
+
177
+ expected_url = f"https://security.microsoft.com/security-recommendations?tid={tenant_id}"
178
+ expected = f'<a href="{expected_url}">{expected_url}</a>'
179
+ assert result == expected
180
+
181
+ def test_create_payload(self):
182
+ """Test creating payload from defender data"""
183
+ rec = {
184
+ "name": "Test Alert",
185
+ "severity": "High",
186
+ "alertUri": "https://example.com",
187
+ "associatedThreats": ["threat1"], # should be skipped
188
+ "propertiesExtendedPropertiesCustomField": {"customField": "customValue"},
189
+ }
190
+
191
+ result = create_payload(rec=rec)
192
+
193
+ assert "Name" in result
194
+ assert "Severity" in result
195
+ assert "Custom Field" in result # uncamel_case applied
196
+ assert "customField" in result["Custom Field"]
197
+ assert "Associated Threats" not in result # should be skipped
198
+ assert "Alert Uri" not in result # should be skipped
199
+
200
+ def test_process_list_value_with_dict_list(self):
201
+ """Test processing list value with dictionary items"""
202
+ value = [{"key1": "value1", "key2": "value2"}, {"key3": "value3"}]
203
+
204
+ result = process_list_value(value=value)
205
+
206
+ assert "</br>key1: value1</br>key2: value2</br>key3: value3" == result
207
+
208
+ def test_process_list_value_with_nested_list(self):
209
+ """Test processing list value with nested lists"""
210
+ value = [["item1", "item2"], ["item3", "item4"]]
211
+
212
+ result = process_list_value(value=value)
213
+
214
+ assert "item1</br>item2item3</br>item4" == result
215
+
216
+ def test_process_list_value_with_string_list(self):
217
+ """Test processing list value with string items"""
218
+ value = ["item1", "item2", "item3"]
219
+
220
+ result = process_list_value(value=value)
221
+
222
+ assert "item1</br>item2</br>item3" == result
223
+
224
+ def test_create_html_table(self):
225
+ """Test creating HTML table"""
226
+ payload = {
227
+ "name": "Test Alert",
228
+ "severity": "High",
229
+ "created_time": "2023-01-01T12:00:00Z",
230
+ "empty_field": None,
231
+ }
232
+ url = '<a href="https://example.com">https://example.com</a>'
233
+
234
+ result = create_html_table(payload=payload, url=url)
235
+
236
+ assert '<table style="border: 1px solid;">' in result
237
+ assert "Test Alert" in result
238
+ assert "High" in result
239
+ assert "Jan 01, 2023" in result # time formatted
240
+ assert "empty_field" not in result # empty fields excluded
241
+ assert "View in Defender" in result
242
+ assert "</table>" in result
243
+
244
+ def test_compare_defender_and_regscale_new_recommendation(self):
245
+ """Test comparing when defender has new recommendation"""
246
+ def_data = DefenderData(
247
+ id="test-id", data={"id": "test-id", "name": "Test Rec"}, system="365", object="recommendations"
248
+ )
249
+ issues = []
250
+
251
+ # Mock global variables
252
+ with patch(f"{PATH}.unique_recs", []) as mock_unique_recs, patch(f"{PATH}.job_progress") as mock_job_progress:
253
+
254
+ mock_task = MagicMock()
255
+ args = (MagicMock(), issues, "id", mock_task)
256
+
257
+ compare_defender_and_regscale(def_data=def_data, args=args)
258
+
259
+ assert def_data.analyzed is True
260
+ assert len(mock_unique_recs) == 1
261
+ mock_job_progress.update.assert_called_once_with(mock_task, advance=1)
262
+
263
+ def test_compare_defender_and_regscale_existing_open_issue(self):
264
+ """Test comparing when issue exists and is open"""
265
+ def_data = DefenderData(
266
+ id="test-id", data={"id": "test-id", "name": "Test Rec"}, system="365", object="recommendations"
267
+ )
268
+ mock_issue = DefenderData(
269
+ id="test-id",
270
+ data={"status": "Open", def_data.integration_field: "test-id"},
271
+ system="365",
272
+ object="recommendations",
273
+ )
274
+ issues = [mock_issue]
275
+
276
+ with patch(f"{PATH}.unique_recs", []) as mock_unique_recs, patch(f"{PATH}.job_progress") as mock_job_progress:
277
+
278
+ mock_task = MagicMock()
279
+ args = (MagicMock(), issues, "id", mock_task)
280
+
281
+ compare_defender_and_regscale(def_data=def_data, args=args)
282
+
283
+ assert def_data.analyzed is True
284
+ assert len(mock_unique_recs) == 0 # Should be empty as it's a duplicate
285
+ mock_job_progress.update.assert_called_once_with(mock_task, advance=1)
286
+
287
+ @patch(f"{PATH}.change_issue_status")
288
+ def test_compare_defender_and_regscale_closed_issue_reopen(self, mock_change_status):
289
+ """Test comparing when issue exists but is closed - should reopen"""
290
+ def_data = DefenderData(
291
+ id="test-id", data={"id": "test-id", "name": "Test Rec"}, system="365", object="recommendations"
292
+ )
293
+ mock_issue = DefenderData(
294
+ id="test-id",
295
+ data={"status": "Closed", def_data.integration_field: "test-id"},
296
+ system="365",
297
+ object="recommendations",
298
+ )
299
+ issues = [mock_issue]
300
+ api = MagicMock()
301
+ api.config = {"issues": {mock_issue.init_key: {"status": "Open"}}}
302
+
303
+ with patch(f"{PATH}.unique_recs", []), patch(f"{PATH}.job_progress"):
304
+
305
+ mock_task = MagicMock()
306
+ args = (api, issues, "id", mock_task)
307
+
308
+ compare_defender_and_regscale(def_data=def_data, args=args)
309
+
310
+ mock_change_status.assert_called_once_with(
311
+ api=api, status="Open", issue=mock_issue.data, rec=def_data, rec_type=mock_issue.init_key
312
+ )
313
+
314
+ @patch(f"{PATH}.change_issue_status")
315
+ def test_evaluate_open_issues_close_outdated(self, mock_change_status):
316
+ """Test evaluating open issues - close when no longer in defender"""
317
+ issue = DefenderData(
318
+ id="outdated-id",
319
+ data={"status": "Open", "defender365Id": "outdated-id"},
320
+ system="365",
321
+ object="recommendations",
322
+ )
323
+ defender_data = [] # Empty list means issue not in current recommendations
324
+
325
+ with patch(f"{PATH}.job_progress") as mock_job_progress:
326
+ mock_task = MagicMock()
327
+ args = (MagicMock(), defender_data, mock_task)
328
+
329
+ evaluate_open_issues(issue=issue, args=args)
330
+
331
+ assert issue.analyzed is True
332
+ mock_change_status.assert_called_once_with(
333
+ api=args[0], status="Closed", issue=issue.data, rec=None, rec_type=issue.init_key
334
+ )
335
+ mock_job_progress.update.assert_called_once_with(mock_task, advance=1)
336
+
337
+ def test_evaluate_open_issues_skip_closed(self):
338
+ """Test evaluating issues that are already closed"""
339
+ issue = DefenderData(
340
+ id="closed-id",
341
+ data={"status": "Closed", "defender365Id": "closed-id"},
342
+ system="365",
343
+ object="recommendations",
344
+ )
345
+ defender_data = []
346
+
347
+ with patch(f"{PATH}.job_progress"), patch(f"{PATH}.change_issue_status") as mock_change_status:
348
+
349
+ mock_task = MagicMock()
350
+ args = (MagicMock(), defender_data, mock_task)
351
+
352
+ evaluate_open_issues(issue=issue, args=args)
353
+
354
+ # Should not call change_issue_status for already closed issues
355
+ mock_change_status.assert_not_called()
356
+
357
+ @patch(f"{PATH}.Issue")
358
+ def test_change_issue_status_close_365(self, mock_issue_class):
359
+ """Test changing issue status to closed for Defender 365"""
360
+ api = MagicMock()
361
+ api.config = {**self.config, **{"userId": "test-user"}}
362
+
363
+ issue = {"id": 1, "status": "Open"}
364
+ rec = DefenderData(
365
+ id="test-id",
366
+ data={"recommendationName": "Test Recommendation", "severityScore": 8},
367
+ system="365",
368
+ object="recommendations",
369
+ )
370
+
371
+ mock_issue_instance = MagicMock()
372
+ mock_issue_class.return_value = mock_issue_instance
373
+
374
+ with patch(f"{PATH}.get_current_datetime") as mock_datetime, patch(
375
+ f"{PATH}.format_description"
376
+ ) as mock_format_desc, patch(f"{PATH}.closed", []) as mock_closed:
377
+
378
+ mock_datetime.return_value = "2023-01-01 12:00:00"
379
+ mock_format_desc.return_value = "Test description"
380
+
381
+ change_issue_status(api=api, status="Closed", issue=issue, rec=rec, rec_type="defender365")
382
+
383
+ assert issue["status"] == "Closed"
384
+ assert issue["lastUpdatedById"] == "test-user"
385
+ assert "No longer reported via Microsoft 365 Defender" in issue["description"]
386
+ assert len(mock_closed) == 1
387
+ mock_issue_instance.save.assert_called_once()
388
+
389
+ @patch(f"{PATH}.Issue")
390
+ def test_change_issue_status_reopen(self, mock_issue_class):
391
+ """Test changing issue status to reopen"""
392
+ api = MagicMock()
393
+ api.config = {**self.config, **{"userId": "test-user"}}
394
+
395
+ issue = {"id": 1, "status": "Closed"}
396
+ rec = DefenderData(
397
+ id="test-id",
398
+ data={"recommendationName": "Test Recommendation", "severityScore": 8},
399
+ system="365",
400
+ object="recommendations",
401
+ )
402
+
403
+ mock_issue_instance = MagicMock()
404
+ mock_issue_class.return_value = mock_issue_instance
405
+
406
+ with patch(f"{PATH}.get_current_datetime") as mock_datetime, patch(
407
+ f"{PATH}.format_description"
408
+ ) as mock_format_desc, patch(f"{PATH}.updated", []) as mock_updated:
409
+
410
+ mock_datetime.return_value = "2023-01-01 12:00:00"
411
+ mock_format_desc.return_value = "Test description"
412
+
413
+ change_issue_status(api=api, status="Open", issue=issue, rec=rec, rec_type="defender365")
414
+
415
+ assert issue["status"] == "Open"
416
+ assert issue["dateCompleted"] == ""
417
+ assert len(mock_updated) == 1
418
+ mock_issue_instance.save.assert_called_once()
419
+
420
+ def test_change_issue_status_no_rec(self):
421
+ """Test changing issue status with no recommendation data"""
422
+ api = MagicMock()
423
+ api.config = {"userId": "test-user"}
424
+
425
+ issue = {"id": 1, "status": "Open"}
426
+
427
+ with patch(f"{PATH}.get_current_datetime") as mock_datetime:
428
+ mock_datetime.return_value = "2023-01-01 12:00:00"
429
+
430
+ result = change_issue_status(api=api, status="Closed", issue=issue, rec=None, rec_type="defender365")
431
+
432
+ # Should return early when rec is None
433
+ assert result is None
434
+ assert issue["lastUpdatedById"] == "test-user"
435
+ assert issue["status"] == "Closed"
436
+
437
+ @patch(f"{PATH}.issues_to_create")
438
+ def test_prep_issues_for_creation(self, mock_issues_to_create):
439
+ """Test preparing issues for creation"""
440
+ mock_issues_to_create.return_value = []
441
+ def_data = DefenderData(
442
+ id="test-id", data={"id": "test-id", "name": "Test Alert"}, system="365", object="alerts"
443
+ )
444
+
445
+ mapping_func = MagicMock()
446
+ mock_issue = MagicMock()
447
+ mapping_func.return_value = mock_issue
448
+
449
+ with patch(f"{PATH}.job_progress") as mock_job_progress, patch(
450
+ f"{PATH}.format_description"
451
+ ) as mock_format_desc:
452
+
453
+ mock_format_desc.return_value = "Test description"
454
+ mock_task = MagicMock()
455
+ args = (mapping_func, self.config, "id", 1, "issues", mock_task)
456
+
457
+ prep_issues_for_creation(def_data=def_data, args=args)
458
+
459
+ assert def_data.created is True
460
+ mapping_func.assert_called_once_with(data=def_data, config=self.config, description="Test description")
461
+ assert mock_issue.parentId == 1
462
+ assert mock_issue.parentModule == "issues"
463
+ mock_job_progress.update.assert_called_once_with(mock_task, advance=1)
464
+
465
+ def test_map_365_alert_to_issue(self):
466
+ """Test mapping 365 alert to RegScale issue"""
467
+ data = DefenderData(
468
+ id="test-id",
469
+ data={
470
+ "title": "Test Alert",
471
+ "severity": "High",
472
+ "machineId": "machine-123",
473
+ "computerDnsName": "test.example.com",
474
+ },
475
+ system="365",
476
+ object="alerts",
477
+ )
478
+ description = "Test alert description"
479
+
480
+ result = map_365_alert_to_issue(data=data, config=self.config, description=description)
481
+
482
+ assert isinstance(result, Issue)
483
+ assert result.title == "Test Alert"
484
+ assert result.description == description
485
+ assert result.severityLevel == IssueSeverity.High
486
+ assert result.status == "Open"
487
+ assert "Machine ID:machine-123" in result.assetIdentifier
488
+ assert "test.example.com" in result.assetIdentifier
489
+ assert result.sourceReport == "Microsoft Defender 365 Alert"
490
+
491
+ def test_map_365_recommendation_to_issue(self):
492
+ """Test mapping 365 recommendation to RegScale issue"""
493
+ data = DefenderData(
494
+ id="test-id",
495
+ data={"recommendationName": "Test Recommendation", "severityScore": 8, "vendor": "Microsoft"},
496
+ system="365",
497
+ object="recommendations",
498
+ )
499
+ description = "Test recommendation description"
500
+
501
+ result = map_365_recommendation_to_issue(data=data, config=self.config, description=description)
502
+
503
+ assert isinstance(result, Issue)
504
+ assert result.title == "Test Recommendation"
505
+ assert result.description == description
506
+ assert result.vendorName == "Microsoft"
507
+ assert result.sourceReport == "Microsoft Defender 365 Recommendation"
508
+
509
+ def test_map_cloud_alert_to_issue(self):
510
+ """Test mapping cloud alert to RegScale issue"""
511
+ data = DefenderData(
512
+ id="test-id",
513
+ data={
514
+ "id": "alert-id-123",
515
+ "properties": {
516
+ "productName": "Microsoft Defender",
517
+ "compromisedEntity": "test-server",
518
+ "severity": "High",
519
+ "vendorName": "Microsoft",
520
+ "resourceIdentifiers": [
521
+ {"azureResourceId": "/subscriptions/test/resource1"},
522
+ {"azureResourceId": "/subscriptions/test/resource2"},
523
+ ],
524
+ "remediationSteps": ["Step 1", "Step 2"],
525
+ },
526
+ },
527
+ system="cloud",
528
+ object="alerts",
529
+ )
530
+ description = "Test alert description"
531
+
532
+ result = map_cloud_alert_to_issue(data=data, config=self.config, description=description)
533
+
534
+ assert isinstance(result, Issue)
535
+ assert result.title == "Microsoft Defender Alert - test-server"
536
+ assert result.description == description
537
+ assert result.vendorName == "Microsoft"
538
+ assert "/subscriptions/test/resource1" in result.assetIdentifier
539
+ assert "Step 1" in result.recommendedActions
540
+ assert result.otherIdentifier == "alert-id-123"
541
+ assert result.sourceReport == "Microsoft Defender for Cloud Alert"
542
+
543
+ def test_map_cloud_recommendation_to_issue(self):
544
+ """Test mapping cloud recommendation to RegScale issue"""
545
+ data = DefenderData(
546
+ id="test-id",
547
+ data={
548
+ "id": "rec-id-123",
549
+ "properties": {
550
+ "metadata": {
551
+ "displayName": "Test Recommendation",
552
+ "severity": "Medium",
553
+ "remediationDescription": "Fix this issue",
554
+ },
555
+ "resourceDetails": {
556
+ "ResourceProvider": "Microsoft.Compute",
557
+ "ResourceType": "virtualMachines",
558
+ "ResourceName": "test-vm",
559
+ "Id": "/subscriptions/test/vm1",
560
+ },
561
+ },
562
+ },
563
+ system="cloud",
564
+ object="recommendations",
565
+ )
566
+ description = "Test recommendation description"
567
+
568
+ result = map_cloud_recommendation_to_issue(data=data, config=self.config, description=description)
569
+
570
+ assert isinstance(result, Issue)
571
+ assert result.title == "Test Recommendation on Microsoft.Compute/virtualMachines/test-vm"
572
+ assert result.description == description
573
+ assert result.recommendedActions == "Fix this issue"
574
+ assert result.assetIdentifier == "/subscriptions/test/vm1"
575
+ assert result.otherIdentifier == "rec-id-123"
576
+ assert result.manualDetectionId == data.id
577
+ assert "Microsoft Defender for Cloud Recommendation" in result.sourceReport
578
+
579
+ @patch(f"{PATH}.File.upload_file_to_regscale")
580
+ @patch(f"{PATH}.save_data_to")
581
+ def test_fetch_save_and_upload_query(self, mock_save_data, mock_upload_file):
582
+ """Test fetching, saving and uploading query results"""
583
+ mock_defender_api = MagicMock(spec=DefenderApi)
584
+ mock_defender_api.fetch_and_run_query.return_value = [{"data": "test"}]
585
+ mock_defender_api.api = MagicMock()
586
+
587
+ query = {"name": "test-query"}
588
+ parent_id = 1
589
+ parent_module = "issues"
590
+ no_upload = False
591
+
592
+ mock_upload_file.return_value = True
593
+
594
+ with patch(f"{PATH}.get_current_datetime") as mock_datetime:
595
+ mock_datetime.return_value = "20230101"
596
+
597
+ fetch_save_and_upload_query(
598
+ defender_api=mock_defender_api,
599
+ query=query,
600
+ parent_id=parent_id,
601
+ parent_module=parent_module,
602
+ no_upload=no_upload,
603
+ )
604
+
605
+ mock_defender_api.fetch_and_run_query.assert_called_once_with(query=query)
606
+ mock_save_data.assert_called_once()
607
+ mock_upload_file.assert_called_once()
608
+
609
+ @patch(f"{PATH}.File.upload_file_to_regscale")
610
+ @patch(f"{PATH}.save_data_to")
611
+ def test_fetch_save_and_upload_query_no_upload(self, mock_save_data, mock_upload_file):
612
+ """Test fetching and saving query results without uploading"""
613
+ mock_defender_api = MagicMock(spec=DefenderApi)
614
+ mock_defender_api.fetch_and_run_query.return_value = [{"data": "test"}]
615
+
616
+ query = {"name": "test-query"}
617
+ parent_id = 1
618
+ parent_module = "issues"
619
+ no_upload = True
620
+
621
+ with patch(f"{PATH}.get_current_datetime") as mock_datetime:
622
+ mock_datetime.return_value = "20230101"
623
+
624
+ fetch_save_and_upload_query(
625
+ defender_api=mock_defender_api,
626
+ query=query,
627
+ parent_id=parent_id,
628
+ parent_module=parent_module,
629
+ no_upload=no_upload,
630
+ )
631
+
632
+ mock_defender_api.fetch_and_run_query.assert_called_once_with(query=query)
633
+ mock_save_data.assert_called_once()
634
+ mock_upload_file.assert_not_called()
635
+
636
+ def test_prompt_user_for_query_selection_by_name(self):
637
+ """Test prompting user for query selection when name matches"""
638
+ queries = [{"name": "Query1", "id": "1"}, {"name": "Query2", "id": "2"}, {"name": "Query3", "id": "3"}]
639
+ query_name = "query2" # Case insensitive
640
+
641
+ result = prompt_user_for_query_selection(queries=queries, query_name=query_name)
642
+
643
+ assert result["name"] == "Query2"
644
+ assert result["id"] == "2"
645
+
646
+ @patch(f"{PATH}.click.prompt")
647
+ def test_prompt_user_for_query_selection_interactive(self, mock_click_prompt):
648
+ """Test prompting user for query selection interactively"""
649
+ queries = [{"name": "Query1", "id": "1"}, {"name": "Query2", "id": "2"}]
650
+ mock_click_prompt.return_value = "Query1"
651
+
652
+ result = prompt_user_for_query_selection(queries=queries)
653
+
654
+ assert result["name"] == "Query1"
655
+ assert result["id"] == "1"
656
+ mock_click_prompt.assert_called_once()
657
+
658
+ @patch(f"{PATH}.FlatFileImporter.import_files")
659
+ def test_import_defender_alerts(self, mock_import_files):
660
+ """Test importing defender alerts from CSV"""
661
+ from regscale.models.integration_models.defenderimport import DefenderImport
662
+
663
+ folder_path = "/path/to/files"
664
+ regscale_ssp_id = 1
665
+ scan_date = datetime(2023, 1, 1)
666
+ mappings_path = Path("/path/to/mappings")
667
+ disable_mapping = False
668
+ s3_bucket = "test-bucket"
669
+ s3_prefix = "test-prefix"
670
+ aws_profile = "test-profile"
671
+ upload_file = True
672
+
673
+ import_defender_alerts(
674
+ folder_path=folder_path,
675
+ regscale_ssp_id=regscale_ssp_id,
676
+ scan_date=scan_date,
677
+ mappings_path=mappings_path,
678
+ disable_mapping=disable_mapping,
679
+ s3_bucket=s3_bucket,
680
+ s3_prefix=s3_prefix,
681
+ aws_profile=aws_profile,
682
+ upload_file=upload_file,
683
+ )
684
+
685
+ mock_import_files.assert_called_once_with(
686
+ import_type=DefenderImport,
687
+ import_name="Defender",
688
+ file_types=".csv",
689
+ folder_path=folder_path,
690
+ object_id=regscale_ssp_id,
691
+ scan_date=scan_date,
692
+ mappings_path=mappings_path,
693
+ disable_mapping=disable_mapping,
694
+ s3_bucket=s3_bucket,
695
+ s3_prefix=s3_prefix,
696
+ aws_profile=aws_profile,
697
+ upload_file=upload_file,
698
+ )
699
+
700
+ @patch(f"{PATH}.fetch_save_and_upload_query")
701
+ @patch(f"{PATH}.DefenderApi")
702
+ @patch(f"{PATH}.is_valid")
703
+ @patch(f"{PATH}.check_license")
704
+ def test_export_resources_all_queries(self, mock_check_license, mock_is_valid, mock_defender_api, mock_fetch_save):
705
+ """Test exporting all queries from Defender for Cloud"""
706
+ mock_app = MagicMock(spec=Application)
707
+ mock_check_license.return_value = mock_app
708
+ mock_is_valid.return_value = True
709
+
710
+ mock_api_instance = MagicMock()
711
+ mock_defender_api.return_value = mock_api_instance
712
+ mock_api_instance.fetch_queries_from_azure.return_value = [{"name": "Query1"}, {"name": "Query2"}]
713
+
714
+ export_resources(
715
+ parent_id=1, parent_module="issues", query_name="Test Query", no_upload=False, all_queries=True
716
+ )
717
+
718
+ mock_defender_api.assert_called_once_with(system="cloud")
719
+ mock_api_instance.fetch_queries_from_azure.assert_called_once()
720
+ assert mock_fetch_save.call_count == 2
721
+
722
+ @patch(f"{PATH}.fetch_save_and_upload_query")
723
+ @patch(f"{PATH}.prompt_user_for_query_selection")
724
+ @patch(f"{PATH}.DefenderApi")
725
+ @patch(f"{PATH}.is_valid")
726
+ @patch(f"{PATH}.check_license")
727
+ def test_export_resources_single_query(
728
+ self, mock_check_license, mock_is_valid, mock_defender_api, mock_prompt, mock_fetch_save
729
+ ):
730
+ """Test exporting single query from Defender for Cloud"""
731
+ mock_app = MagicMock(spec=Application)
732
+ mock_check_license.return_value = mock_app
733
+ mock_is_valid.return_value = True
734
+
735
+ mock_api_instance = MagicMock()
736
+ mock_defender_api.return_value = mock_api_instance
737
+ mock_api_instance.fetch_queries_from_azure.return_value = [{"name": "Query1"}]
738
+
739
+ mock_prompt.return_value = {"name": "Query1"}
740
+
741
+ export_resources(parent_id=1, parent_module="issues", query_name="Query1", no_upload=False, all_queries=False)
742
+
743
+ mock_prompt.assert_called_once_with(queries=[{"name": "Query1"}], query_name="Query1")
744
+ mock_fetch_save.assert_called_once()
745
+
746
+ @patch(f"{PATH}.logger")
747
+ @patch(f"{PATH}.DefenderApi")
748
+ @patch(f"{PATH}.is_valid")
749
+ @patch(f"{PATH}.check_license")
750
+ def test_export_resources_no_queries(self, mock_check_license, mock_is_valid, mock_defender_api, mock_logger):
751
+ """Test exporting when no queries exist"""
752
+ mock_app = MagicMock(spec=Application)
753
+ mock_check_license.return_value = mock_app
754
+ mock_is_valid.return_value = True
755
+
756
+ mock_api_instance = MagicMock()
757
+ mock_defender_api.return_value = mock_api_instance
758
+ mock_api_instance.fetch_queries_from_azure.return_value = []
759
+
760
+ export_resources(parent_id=1, parent_module="issues", query_name="None", no_upload=False, all_queries=True)
761
+
762
+ mock_logger.warning.assert_called_once_with(
763
+ "No saved queries found in Azure. Please create at least one query to use this export function."
764
+ )
765
+
766
+ @patch(f"{PATH}.error_and_exit")
767
+ @patch(f"{PATH}.is_valid")
768
+ @patch(f"{PATH}.check_license")
769
+ def test_export_resources_invalid_login(self, mock_check_license, mock_is_valid, mock_error_exit):
770
+ """Test exporting with invalid RegScale login"""
771
+ mock_app = MagicMock(spec=Application)
772
+ mock_check_license.return_value = mock_app
773
+ mock_is_valid.return_value = False
774
+
775
+ export_resources(
776
+ parent_id=1, parent_module="issues", query_name="Invalid Login", no_upload=False, all_queries=True
777
+ )
778
+
779
+ mock_error_exit.assert_called_once_with("Login Invalid RegScale Credentials, please login for a new token.")
780
+
781
+ # ==============================
782
+ # NEW TESTS FOR ENTRA FUNCTIONALITY
783
+ # ==============================
784
+
785
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
786
+ @patch(f"{PATH}.DefenderApi")
787
+ def test_authenticate_entra(self, mock_defender_api, mock_check_license):
788
+ """Test authenticating with Azure Entra"""
789
+ mock_api_instance = MagicMock()
790
+ mock_defender_api.return_value = mock_api_instance
791
+
792
+ authenticate(system="entra")
793
+
794
+ mock_check_license.assert_called_once()
795
+ mock_defender_api.assert_called_once_with(system="entra")
796
+ mock_api_instance.get_token.assert_called_once()
797
+
798
+ @patch(f"{PATH}.upload_evidence_files")
799
+ @patch(f"{PATH}.collect_specific_evidence_type")
800
+ @patch(f"{PATH}.DefenderApi")
801
+ @patch(f"{PATH}.is_valid")
802
+ @patch(f"{PATH}.Api")
803
+ @patch(f"{PATH}.check_license")
804
+ def test_collect_and_upload_entra_evidence_all(
805
+ self,
806
+ mock_check_license,
807
+ mock_api_class,
808
+ mock_is_valid,
809
+ mock_defender_api,
810
+ mock_collect_specific,
811
+ mock_upload_evidence,
812
+ ):
813
+ """Test collect_and_upload_entra_evidence with all evidence types"""
814
+ mock_app = MagicMock(spec=Application)
815
+ mock_check_license.return_value = mock_app
816
+ mock_is_valid.return_value = True
817
+
818
+ mock_api = MagicMock()
819
+ mock_api_class.return_value = mock_api
820
+
821
+ mock_defender_api_instance = MagicMock()
822
+ mock_defender_api.return_value = mock_defender_api_instance
823
+ mock_defender_api_instance.collect_all_entra_evidence.return_value = {
824
+ "users": [Path("/test/users.csv")],
825
+ "sign_in_logs": [Path("/test/logs.csv")],
826
+ }
827
+
828
+ collect_and_upload_entra_evidence(parent_id=1, parent_module="securityplans", days_back=30, evidence_type="all")
829
+
830
+ mock_defender_api.assert_called_once_with(system="entra")
831
+ mock_defender_api_instance.collect_all_entra_evidence.assert_called_once_with(days_back=30)
832
+ mock_upload_evidence.assert_called_once()
833
+
834
+ @patch(f"{PATH}.upload_evidence_files")
835
+ @patch(f"{PATH}.collect_specific_evidence_type")
836
+ @patch(f"{PATH}.DefenderApi")
837
+ @patch(f"{PATH}.is_valid")
838
+ @patch(f"{PATH}.Api")
839
+ @patch(f"{PATH}.check_license")
840
+ def test_collect_and_upload_entra_evidence_specific_type(
841
+ self,
842
+ mock_check_license,
843
+ mock_api_class,
844
+ mock_is_valid,
845
+ mock_defender_api,
846
+ mock_collect_specific,
847
+ mock_upload_evidence,
848
+ ):
849
+ """Test collect_and_upload_entra_evidence with specific evidence type"""
850
+ mock_app = MagicMock(spec=Application)
851
+ mock_check_license.return_value = mock_app
852
+ mock_is_valid.return_value = True
853
+
854
+ mock_api = MagicMock()
855
+ mock_api_class.return_value = mock_api
856
+
857
+ mock_defender_api_instance = MagicMock()
858
+ mock_defender_api.return_value = mock_defender_api_instance
859
+
860
+ mock_collect_specific.return_value = {"users": [Path("/test/users.csv")]}
861
+
862
+ collect_and_upload_entra_evidence(
863
+ parent_id=1, parent_module="securityplans", days_back=30, evidence_type="users_groups"
864
+ )
865
+
866
+ mock_defender_api.assert_called_once_with(system="entra")
867
+ mock_collect_specific.assert_called_once_with(mock_defender_api_instance, "users_groups", 30)
868
+ mock_upload_evidence.assert_called_once()
869
+
870
+ @patch(f"{PATH}.error_and_exit")
871
+ @patch(f"{PATH}.is_valid")
872
+ @patch(f"{PATH}.Api")
873
+ @patch(f"{PATH}.check_license")
874
+ def test_collect_and_upload_entra_evidence_invalid_login(
875
+ self, mock_check_license, mock_api_class, mock_is_valid, mock_error_exit
876
+ ):
877
+ """Test collect_and_upload_entra_evidence with invalid login"""
878
+ mock_error_exit.side_effect = SystemExit(1)
879
+ mock_app = MagicMock(spec=Application)
880
+ mock_check_license.return_value = mock_app
881
+ mock_is_valid.return_value = False
882
+
883
+ with pytest.raises(SystemExit):
884
+ collect_and_upload_entra_evidence(
885
+ parent_id=1, parent_module="securityplans", days_back=30, evidence_type="all"
886
+ )
887
+
888
+ mock_error_exit.assert_called_once_with("Login Invalid RegScale Credentials, please login for a new token.")
889
+
890
+ @patch(f"{PATH}.error_and_exit")
891
+ @patch(f"{PATH}.DefenderApi")
892
+ @patch(f"{PATH}.is_valid")
893
+ @patch(f"{PATH}.Api")
894
+ @patch(f"{PATH}.check_license")
895
+ def test_collect_and_upload_entra_evidence_with_exception(
896
+ self, mock_check_license, mock_api_class, mock_is_valid, mock_defender_api, mock_error_exit
897
+ ):
898
+ """Test collect_and_upload_entra_evidence with exception"""
899
+ mock_error_exit.side_effect = SystemExit(1)
900
+ mock_app = MagicMock(spec=Application)
901
+ mock_check_license.return_value = mock_app
902
+ mock_is_valid.return_value = True
903
+
904
+ mock_api = MagicMock()
905
+ mock_api_class.return_value = mock_api
906
+
907
+ mock_defender_api_instance = MagicMock()
908
+ mock_defender_api.return_value = mock_defender_api_instance
909
+ mock_defender_api_instance.collect_all_entra_evidence.side_effect = Exception("API Error")
910
+
911
+ with pytest.raises(SystemExit):
912
+ collect_and_upload_entra_evidence(
913
+ parent_id=1, parent_module="securityplans", days_back=30, evidence_type="all"
914
+ )
915
+
916
+ mock_error_exit.assert_called_once_with("Error collecting Azure Entra evidence: API Error")
917
+
918
+ def test_collect_specific_evidence_type_users_groups(self):
919
+ """Test collect_specific_evidence_type for users_groups"""
920
+ mock_defender_api = MagicMock()
921
+ mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
922
+
923
+ result = collect_specific_evidence_type(mock_defender_api, "users_groups", 30)
924
+
925
+ expected_calls = [
926
+ ("users",),
927
+ ("guest_users",),
928
+ ("groups_and_members",),
929
+ ("security_groups",),
930
+ ]
931
+ actual_calls = [call[0] for call in mock_defender_api.get_and_save_entra_evidence.call_args_list]
932
+ for expected_call in expected_calls:
933
+ assert expected_call in actual_calls
934
+
935
+ assert "users" in result
936
+ assert "guest_users" in result
937
+ assert "security_groups" in result
938
+ assert "groups_and_members" in result
939
+
940
+ def test_collect_specific_evidence_type_rbac_pim(self):
941
+ """Test collect_specific_evidence_type for rbac_pim"""
942
+ mock_defender_api = MagicMock()
943
+ mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
944
+
945
+ result = collect_specific_evidence_type(mock_defender_api, "rbac_pim", 30)
946
+
947
+ expected_calls = [
948
+ ("role_assignments",),
949
+ ("role_definitions",),
950
+ ("pim_assignments",),
951
+ ("pim_eligibility",),
952
+ ]
953
+ actual_calls = [call[0] for call in mock_defender_api.get_and_save_entra_evidence.call_args_list]
954
+ for expected_call in expected_calls:
955
+ assert expected_call in actual_calls
956
+
957
+ assert "role_assignments" in result
958
+ assert "role_definitions" in result
959
+ assert "pim_assignments" in result
960
+ assert "pim_eligibility" in result
961
+
962
+ def test_collect_specific_evidence_type_audit_logs(self):
963
+ """Test collect_specific_evidence_type for audit_logs"""
964
+ mock_defender_api = MagicMock()
965
+ mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
966
+
967
+ result = collect_specific_evidence_type(mock_defender_api, "audit_logs", 30)
968
+
969
+ # Verify start_date parameter is passed for audit log endpoints
970
+ expected_calls_with_start_date = [
971
+ "sign_in_logs",
972
+ "directory_audits",
973
+ "provisioning_logs",
974
+ ]
975
+ for call in mock_defender_api.get_and_save_entra_evidence.call_args_list:
976
+ endpoint_key = call[0][0]
977
+ if endpoint_key in expected_calls_with_start_date:
978
+ assert "start_date" in call[1]
979
+
980
+ assert "sign_in_logs" in result
981
+ assert "directory_audits" in result
982
+ assert "provisioning_logs" in result
983
+
984
+ def test_collect_specific_evidence_type_access_reviews(self):
985
+ """Test collect_specific_evidence_type for access_reviews"""
986
+ mock_defender_api = MagicMock()
987
+ mock_defender_api.collect_entra_access_reviews.return_value = [Path("/test/file.csv")]
988
+
989
+ result = collect_specific_evidence_type(mock_defender_api, "access_reviews", 30)
990
+
991
+ mock_defender_api.collect_entra_access_reviews.assert_called_once()
992
+ assert "access_review_definitions" in result
993
+
994
+ @patch("regscale.models.ControlImplementation.get_list_by_parent")
995
+ def test_get_control_implementations_map_success(self, mock_get_list):
996
+ """Test get_control_implementations_map with successful result"""
997
+ from regscale.integrations.commercial.microsoft_defender.defender import get_control_implementations_map
998
+
999
+ # Mock control implementations
1000
+ mock_controls = [
1001
+ {"id": 1, "controlId": "AC-1"},
1002
+ {"id": 2, "controlId": "AC-2"},
1003
+ {"id": 3, "controlId": "IA-2"},
1004
+ ]
1005
+ mock_get_list.return_value = mock_controls
1006
+
1007
+ result = get_control_implementations_map(parent_id=1, parent_module="securityplans")
1008
+
1009
+ mock_get_list.assert_called_once_with(1, "securityplans")
1010
+ expected = {"AC-1": 1, "AC-2": 2, "IA-2": 3}
1011
+ assert result == expected
1012
+
1013
+ @patch("regscale.models.ControlImplementation.get_list_by_parent")
1014
+ def test_get_control_implementations_map_empty(self, mock_get_list):
1015
+ """Test get_control_implementations_map with empty result"""
1016
+ from regscale.integrations.commercial.microsoft_defender.defender import get_control_implementations_map
1017
+
1018
+ mock_get_list.return_value = []
1019
+
1020
+ result = get_control_implementations_map(parent_id=1, parent_module="securityplans")
1021
+
1022
+ assert result == {}
1023
+
1024
+ @patch("regscale.models.ControlImplementation.get_list_by_parent")
1025
+ def test_get_control_implementations_map_with_exception(self, mock_get_list):
1026
+ """Test get_control_implementations_map with exception"""
1027
+ from regscale.integrations.commercial.microsoft_defender.defender import get_control_implementations_map
1028
+
1029
+ mock_get_list.side_effect = Exception("Database error")
1030
+
1031
+ result = get_control_implementations_map(parent_id=1, parent_module="securityplans")
1032
+
1033
+ assert result == {}
1034
+
1035
+ @patch(f"{PATH}.File.upload_file_to_regscale")
1036
+ def test_upload_evidence_to_controls_success(self, mock_upload_file):
1037
+ """Test upload_evidence_to_controls with successful uploads"""
1038
+ from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_to_controls
1039
+
1040
+ mock_upload_file.return_value = True
1041
+ evidence_files = [Path("/test/users.csv"), Path("/test/logs.csv")]
1042
+ control_map = {"AC-1": 1, "AC-2": 2, "IA-2": 3}
1043
+ mock_api = MagicMock()
1044
+
1045
+ result = upload_evidence_to_controls(
1046
+ evidence_key="users",
1047
+ evidence_file_list=evidence_files,
1048
+ control_implementations_map=control_map,
1049
+ api=mock_api,
1050
+ )
1051
+
1052
+ # For users evidence, it should upload to AC-1, AC-2, and other user-related controls
1053
+ # Each file should be uploaded to multiple controls
1054
+ assert mock_upload_file.call_count > 0
1055
+ assert result > 0
1056
+
1057
+ @patch(f"{PATH}.File.upload_file_to_regscale")
1058
+ def test_upload_evidence_to_controls_no_mapping(self, mock_upload_file):
1059
+ """Test upload_evidence_to_controls with unknown evidence key"""
1060
+ from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_to_controls
1061
+
1062
+ evidence_files = [Path("/test/unknown.csv")]
1063
+ control_map = {"AC-1": 1}
1064
+ mock_api = MagicMock()
1065
+
1066
+ result = upload_evidence_to_controls(
1067
+ evidence_key="unknown_evidence",
1068
+ evidence_file_list=evidence_files,
1069
+ control_implementations_map=control_map,
1070
+ api=mock_api,
1071
+ )
1072
+
1073
+ mock_upload_file.assert_not_called()
1074
+ assert result == 0
1075
+
1076
+ @patch(f"{PATH}.File.upload_file_to_regscale")
1077
+ def test_upload_evidence_to_controls_upload_failure(self, mock_upload_file):
1078
+ """Test upload_evidence_to_controls with upload failures"""
1079
+ from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_to_controls
1080
+
1081
+ mock_upload_file.return_value = False
1082
+ evidence_files = [Path("/test/users.csv")]
1083
+ control_map = {"AC-1": 1, "AC-2": 2}
1084
+ mock_api = MagicMock()
1085
+
1086
+ result = upload_evidence_to_controls(
1087
+ evidence_key="users",
1088
+ evidence_file_list=evidence_files,
1089
+ control_implementations_map=control_map,
1090
+ api=mock_api,
1091
+ )
1092
+
1093
+ assert mock_upload_file.call_count > 0
1094
+ assert result == 0
1095
+
1096
+ @patch(f"{PATH}.get_control_implementations_map")
1097
+ @patch(f"{PATH}.upload_evidence_to_controls")
1098
+ def test_upload_evidence_files_success(self, mock_upload_to_controls, mock_get_control_map):
1099
+ """Test upload_evidence_files with successful uploads"""
1100
+ from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_files
1101
+
1102
+ mock_get_control_map.return_value = {"AC-1": 1, "AC-2": 2}
1103
+ mock_upload_to_controls.return_value = 3
1104
+
1105
+ evidence_data = {
1106
+ "users": [Path("/test/users.csv")],
1107
+ "sign_in_logs": [Path("/test/logs.csv")],
1108
+ }
1109
+ mock_api = MagicMock()
1110
+
1111
+ upload_evidence_files(
1112
+ evidence_data=evidence_data, parent_id=1, parent_module="securityplans", api=mock_api, evidence_type="all"
1113
+ )
1114
+
1115
+ mock_get_control_map.assert_called_once_with(1, "securityplans")
1116
+ assert mock_upload_to_controls.call_count == 2 # Called for each evidence type
1117
+
1118
+ @patch(f"{PATH}.get_control_implementations_map")
1119
+ def test_upload_evidence_files_no_controls(self, mock_get_control_map):
1120
+ """Test upload_evidence_files with no control implementations"""
1121
+ from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_files
1122
+
1123
+ mock_get_control_map.return_value = {}
1124
+
1125
+ evidence_data = {"users": [Path("/test/users.csv")]}
1126
+ mock_api = MagicMock()
1127
+
1128
+ # Should return early when no control implementations are found
1129
+ upload_evidence_files(
1130
+ evidence_data=evidence_data, parent_id=1, parent_module="securityplans", api=mock_api, evidence_type="all"
1131
+ )
1132
+
1133
+ mock_get_control_map.assert_called_once()
1134
+
1135
+ @patch(f"{PATH}.console")
1136
+ def test_show_entra_mappings_all(self, mock_console):
1137
+ """Test show_entra_mappings displaying all mappings"""
1138
+ from click.testing import CliRunner
1139
+ from regscale.integrations.commercial.microsoft_defender.defender import show_entra_mappings
1140
+
1141
+ # Mock the console.print calls
1142
+ mock_console.print.return_value = None
1143
+
1144
+ # Use Click's test runner to invoke the command
1145
+ runner = CliRunner()
1146
+ result = runner.invoke(show_entra_mappings, ["--evidence_type", "all"])
1147
+
1148
+ # Verify the command executed successfully
1149
+ assert result.exit_code == 0
1150
+ # Verify console.print was called (table creation and final message)
1151
+ assert mock_console.print.call_count == 2
1152
+
1153
+ @patch(f"{PATH}.console")
1154
+ def test_show_entra_mappings_specific_type(self, mock_console):
1155
+ """Test show_entra_mappings displaying specific type"""
1156
+ from click.testing import CliRunner
1157
+ from regscale.integrations.commercial.microsoft_defender.defender import show_entra_mappings
1158
+
1159
+ mock_console.print.return_value = None
1160
+
1161
+ # Use Click's test runner to invoke the command
1162
+ runner = CliRunner()
1163
+ result = runner.invoke(show_entra_mappings, ["--evidence_type", "users_groups"])
1164
+
1165
+ # Verify the command executed successfully
1166
+ assert result.exit_code == 0
1167
+ # Verify console.print was called
1168
+ assert mock_console.print.call_count == 2
1169
+
1170
+ # ==============================
1171
+ # COMPREHENSIVE ENTRA FUNCTIONALITY TESTS
1172
+ # ==============================
1173
+
1174
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
1175
+ @patch(f"{PATH}.DefenderApi")
1176
+ def test_authenticate_entra(self, mock_defender_api, mock_check_license):
1177
+ """Test authenticating with Azure Entra"""
1178
+ mock_api_instance = MagicMock()
1179
+ mock_defender_api.return_value = mock_api_instance
1180
+
1181
+ authenticate(system="entra")
1182
+
1183
+ mock_check_license.assert_called_once()
1184
+ mock_defender_api.assert_called_once_with(system="entra")
1185
+ mock_api_instance.get_token.assert_called_once()
1186
+
1187
+ @patch(f"{PATH}.error_and_exit")
1188
+ @patch(f"{PATH}.is_valid")
1189
+ @patch(f"{PATH}.check_license")
1190
+ @patch(f"{PATH}.Api")
1191
+ @patch(f"{PATH}.DefenderApi")
1192
+ def test_collect_and_upload_entra_evidence_invalid_auth(
1193
+ self, mock_defender_api, mock_api, mock_check_license, mock_is_valid, mock_error_exit
1194
+ ):
1195
+ """Test collect_and_upload_entra_evidence with invalid authentication"""
1196
+ mock_error_exit.side_effect = SystemExit(1)
1197
+ mock_check_license.return_value = MagicMock()
1198
+ mock_is_valid.return_value = False
1199
+
1200
+ with pytest.raises(SystemExit):
1201
+ collect_and_upload_entra_evidence(parent_id=1, parent_module="securityplans")
1202
+
1203
+ mock_error_exit.assert_called_once()
1204
+
1205
+ @patch(f"{PATH}.upload_evidence_files")
1206
+ @patch(f"{PATH}.collect_specific_evidence_type")
1207
+ @patch(f"{PATH}.is_valid")
1208
+ @patch(f"{PATH}.check_license")
1209
+ @patch(f"{PATH}.Api")
1210
+ @patch(f"{PATH}.DefenderApi")
1211
+ def test_collect_and_upload_entra_evidence_specific_type(
1212
+ self, mock_defender_api, mock_api, mock_check_license, mock_is_valid, mock_collect_specific, mock_upload
1213
+ ):
1214
+ """Test collect_and_upload_entra_evidence with specific evidence type"""
1215
+ # Setup mocks
1216
+ mock_check_license.return_value = MagicMock()
1217
+ mock_is_valid.return_value = True
1218
+ mock_collect_specific.return_value = {"users": [Path("/test/users.csv")]}
1219
+ mock_upload.return_value = None
1220
+
1221
+ # Call function with specific evidence type
1222
+ collect_and_upload_entra_evidence(
1223
+ parent_id=1, parent_module="securityplans", days_back=30, evidence_type="users_groups"
1224
+ )
1225
+
1226
+ # Verify specific evidence collection was called
1227
+ mock_collect_specific.assert_called_once_with(mock_defender_api.return_value, "users_groups", 30)
1228
+ mock_upload.assert_called_once()
1229
+
1230
+ @patch(f"{PATH}.upload_evidence_files")
1231
+ @patch(f"{PATH}.is_valid")
1232
+ @patch(f"{PATH}.check_license")
1233
+ @patch(f"{PATH}.Api")
1234
+ @patch(f"{PATH}.DefenderApi")
1235
+ def test_collect_and_upload_entra_evidence_all(
1236
+ self, mock_defender_api, mock_api, mock_check_license, mock_is_valid, mock_upload
1237
+ ):
1238
+ """Test collect_and_upload_entra_evidence collecting all evidence"""
1239
+ # Setup mocks
1240
+ mock_check_license.return_value = MagicMock()
1241
+ mock_is_valid.return_value = True
1242
+ mock_api_instance = MagicMock()
1243
+ mock_defender_api.return_value = mock_api_instance
1244
+ mock_api_instance.collect_all_entra_evidence.return_value = {
1245
+ "users": [Path("/test/users.csv")],
1246
+ "sign_in_logs": [Path("/test/signin.csv")],
1247
+ }
1248
+ mock_upload.return_value = None
1249
+
1250
+ # Call function with evidence_type="all"
1251
+ collect_and_upload_entra_evidence(parent_id=1, parent_module="securityplans", evidence_type="all")
1252
+
1253
+ # Verify all evidence collection was called
1254
+ mock_api_instance.collect_all_entra_evidence.assert_called_once_with(days_back=30)
1255
+ mock_upload.assert_called_once()
1256
+
1257
+ @patch(f"{PATH}.error_and_exit")
1258
+ @patch(f"{PATH}.is_valid")
1259
+ @patch(f"{PATH}.check_license")
1260
+ @patch(f"{PATH}.Api")
1261
+ @patch(f"{PATH}.DefenderApi")
1262
+ def test_collect_and_upload_entra_evidence_exception(
1263
+ self, mock_defender_api, mock_api, mock_check_license, mock_is_valid, mock_error_exit
1264
+ ):
1265
+ """Test collect_and_upload_entra_evidence handles exceptions"""
1266
+ mock_error_exit.side_effect = SystemExit(1)
1267
+ mock_check_license.return_value = MagicMock()
1268
+ mock_is_valid.return_value = True
1269
+ mock_api_instance = MagicMock()
1270
+ mock_defender_api.return_value = mock_api_instance
1271
+ mock_api_instance.collect_all_entra_evidence.side_effect = Exception("API Error")
1272
+
1273
+ with pytest.raises(SystemExit):
1274
+ collect_and_upload_entra_evidence(parent_id=1, parent_module="securityplans")
1275
+
1276
+ mock_error_exit.assert_called_once()
1277
+ error_message = mock_error_exit.call_args[0][0]
1278
+ assert "Error collecting Azure Entra evidence" in error_message
1279
+
1280
+ def test_collect_specific_evidence_type_users_groups(self):
1281
+ """Test collect_specific_evidence_type for users_groups"""
1282
+ mock_defender_api = MagicMock()
1283
+ mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
1284
+
1285
+ result = collect_specific_evidence_type(mock_defender_api, "users_groups", 30)
1286
+
1287
+ # Verify all expected user/group endpoints were called
1288
+ expected_calls = ["users", "guest_users", "groups_and_members", "security_groups"]
1289
+ assert len(mock_defender_api.get_and_save_entra_evidence.call_args_list) == len(expected_calls)
1290
+
1291
+ for call in mock_defender_api.get_and_save_entra_evidence.call_args_list:
1292
+ endpoint = call[0][0] # First positional argument
1293
+ assert endpoint in expected_calls
1294
+
1295
+ # Verify all evidence types are in result
1296
+ for expected_type in expected_calls:
1297
+ assert expected_type in result
1298
+
1299
+ def test_collect_specific_evidence_type_rbac_pim(self):
1300
+ """Test collect_specific_evidence_type for rbac_pim"""
1301
+ mock_defender_api = MagicMock()
1302
+ mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
1303
+
1304
+ result = collect_specific_evidence_type(mock_defender_api, "rbac_pim", 30)
1305
+
1306
+ expected_calls = ["role_assignments", "role_definitions", "pim_assignments", "pim_eligibility"]
1307
+ assert len(mock_defender_api.get_and_save_entra_evidence.call_args_list) == len(expected_calls)
1308
+
1309
+ for expected_type in expected_calls:
1310
+ assert expected_type in result
1311
+
1312
+ def test_collect_specific_evidence_type_conditional_access(self):
1313
+ """Test collect_specific_evidence_type for conditional_access"""
1314
+ mock_defender_api = MagicMock()
1315
+ mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
1316
+
1317
+ result = collect_specific_evidence_type(mock_defender_api, "conditional_access", 30)
1318
+
1319
+ mock_defender_api.get_and_save_entra_evidence.assert_called_once_with("conditional_access")
1320
+ assert "conditional_access" in result
1321
+
1322
+ def test_collect_specific_evidence_type_authentication(self):
1323
+ """Test collect_specific_evidence_type for authentication"""
1324
+ mock_defender_api = MagicMock()
1325
+ mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
1326
+
1327
+ result = collect_specific_evidence_type(mock_defender_api, "authentication", 30)
1328
+
1329
+ expected_calls = ["auth_methods_policy", "user_mfa_registration", "mfa_registered_users"]
1330
+ assert len(mock_defender_api.get_and_save_entra_evidence.call_args_list) == len(expected_calls)
1331
+
1332
+ for expected_type in expected_calls:
1333
+ assert expected_type in result
1334
+
1335
+ def test_collect_specific_evidence_type_audit_logs(self):
1336
+ """Test collect_specific_evidence_type for audit_logs with start_date"""
1337
+ mock_defender_api = MagicMock()
1338
+ mock_defender_api.get_and_save_entra_evidence.return_value = [Path("/test/file.csv")]
1339
+
1340
+ result = collect_specific_evidence_type(mock_defender_api, "audit_logs", 60)
1341
+
1342
+ expected_calls = ["sign_in_logs", "directory_audits", "provisioning_logs"]
1343
+ assert len(mock_defender_api.get_and_save_entra_evidence.call_args_list) == len(expected_calls)
1344
+
1345
+ # Verify start_date parameter was passed for audit logs
1346
+ for call in mock_defender_api.get_and_save_entra_evidence.call_args_list:
1347
+ kwargs = call[1] # Keyword arguments
1348
+ assert "start_date" in kwargs
1349
+ # Verify it's a valid date string for 60 days back
1350
+ start_date = kwargs["start_date"]
1351
+ assert start_date.endswith("T00:00:00Z")
1352
+
1353
+ for expected_type in expected_calls:
1354
+ assert expected_type in result
1355
+
1356
+ def test_collect_specific_evidence_type_access_reviews(self):
1357
+ """Test collect_specific_evidence_type for access_reviews"""
1358
+ mock_defender_api = MagicMock()
1359
+ mock_defender_api.collect_entra_access_reviews.return_value = [Path("/test/file.csv")]
1360
+
1361
+ result = collect_specific_evidence_type(mock_defender_api, "access_reviews", 30)
1362
+
1363
+ mock_defender_api.collect_entra_access_reviews.assert_called_once()
1364
+ assert "access_review_definitions" in result
1365
+
1366
+ @patch("regscale.models.ControlImplementation")
1367
+ def test_get_control_implementations_map_success(self, mock_control_impl):
1368
+ """Test get_control_implementations_map with successful retrieval"""
1369
+ from regscale.integrations.commercial.microsoft_defender.defender import get_control_implementations_map
1370
+
1371
+ # Mock control implementations
1372
+ mock_controls = [
1373
+ {"id": 1, "controlId": "AC-2"},
1374
+ {"id": 2, "controlId": "AU-3"},
1375
+ {"id": 3, "controlId": "IA-2"},
1376
+ ]
1377
+ mock_control_impl.get_list_by_parent.return_value = mock_controls
1378
+
1379
+ result = get_control_implementations_map(parent_id=1, parent_module="securityplans")
1380
+
1381
+ expected = {"AC-2": 1, "AU-3": 2, "IA-2": 3}
1382
+ assert result == expected
1383
+ mock_control_impl.get_list_by_parent.assert_called_once_with(1, "securityplans")
1384
+
1385
+ @patch("regscale.models.ControlImplementation")
1386
+ def test_get_control_implementations_map_empty(self, mock_control_impl):
1387
+ """Test get_control_implementations_map with no control implementations"""
1388
+ from regscale.integrations.commercial.microsoft_defender.defender import get_control_implementations_map
1389
+
1390
+ mock_control_impl.get_list_by_parent.return_value = []
1391
+
1392
+ result = get_control_implementations_map(parent_id=1, parent_module="securityplans")
1393
+
1394
+ assert result == {}
1395
+
1396
+ @patch("regscale.models.ControlImplementation")
1397
+ def test_get_control_implementations_map_exception(self, mock_control_impl):
1398
+ """Test get_control_implementations_map handles exceptions"""
1399
+ from regscale.integrations.commercial.microsoft_defender.defender import get_control_implementations_map
1400
+
1401
+ mock_control_impl.get_list_by_parent.side_effect = Exception("API Error")
1402
+
1403
+ result = get_control_implementations_map(parent_id=1, parent_module="securityplans")
1404
+
1405
+ assert result == {}
1406
+
1407
+ @patch(f"{PATH}.File.upload_file_to_regscale")
1408
+ def test_upload_evidence_to_controls_success(self, mock_upload):
1409
+ """Test upload_evidence_to_controls with successful uploads"""
1410
+ from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_to_controls
1411
+
1412
+ mock_upload.return_value = True
1413
+ evidence_files = [Path("/test/users.csv"), Path("/test/users2.csv")]
1414
+ # users evidence maps to: [AC_1, AC_2, AC_2_1, AC_2_3, AC_2_5, AC_2_7, AC_2_12]
1415
+ control_map = {"AC-1": 1, "AC-2": 2, "AC-2(1)": 3, "AC-2(3)": 4, "AC-2(5)": 5, "AC-2(7)": 6, "AC-2(12)": 7}
1416
+ api = MagicMock()
1417
+
1418
+ result = upload_evidence_to_controls("users", evidence_files, control_map, api)
1419
+
1420
+ # users evidence maps to 7 controls × 2 files = 14 expected uploads
1421
+ assert result == 14
1422
+ assert mock_upload.call_count == 14
1423
+
1424
+ @patch(f"{PATH}.File.upload_file_to_regscale")
1425
+ def test_upload_evidence_to_controls_no_mapping(self, mock_upload):
1426
+ """Test upload_evidence_to_controls with no control mapping"""
1427
+ from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_to_controls
1428
+
1429
+ evidence_files = [Path("/test/unknown.csv")]
1430
+ control_map = {"AC-2": 1}
1431
+ api = MagicMock()
1432
+
1433
+ result = upload_evidence_to_controls("unknown_evidence", evidence_files, control_map, api)
1434
+
1435
+ assert result == 0
1436
+ mock_upload.assert_not_called()
1437
+
1438
+ @patch(f"{PATH}.File.upload_file_to_regscale")
1439
+ def test_upload_evidence_to_controls_partial_failure(self, mock_upload):
1440
+ """Test upload_evidence_to_controls with partial upload failures"""
1441
+ from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_to_controls
1442
+
1443
+ # Mock alternating success/failure
1444
+ mock_upload.side_effect = [True, False, True, False, True, False, True]
1445
+ evidence_files = [Path("/test/users.csv")]
1446
+ control_map = {"AC-2": 1, "AC-2(1)": 2, "AC-2(3)": 3, "AC-2(5)": 4, "AC-2(7)": 5, "AC-2(12)": 6, "AC-1": 7}
1447
+ api = MagicMock()
1448
+
1449
+ result = upload_evidence_to_controls("users", evidence_files, control_map, api)
1450
+
1451
+ # Should return count of successful uploads (4 out of 7)
1452
+ assert result == 4
1453
+
1454
+ @patch(f"{PATH}.upload_evidence_to_controls")
1455
+ @patch(f"{PATH}.get_control_implementations_map")
1456
+ @patch(f"{PATH}.Path")
1457
+ def test_upload_evidence_files_success(self, mock_path, mock_get_control_map, mock_upload_evidence):
1458
+ """Test upload_evidence_files with successful uploads"""
1459
+ from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_files
1460
+
1461
+ mock_get_control_map.return_value = {"AC-2": 1, "AU-3": 2}
1462
+ mock_upload_evidence.return_value = 5
1463
+ mock_path.return_value.mkdir.return_value = None
1464
+
1465
+ evidence_data = {
1466
+ "users": [Path("/test/users.csv")],
1467
+ "sign_in_logs": [Path("/test/signin.csv")],
1468
+ }
1469
+ api = MagicMock()
1470
+
1471
+ upload_evidence_files(evidence_data, 1, "securityplans", api, "all")
1472
+
1473
+ # Verify evidence upload was called for each evidence type
1474
+ assert mock_upload_evidence.call_count == 2
1475
+ mock_get_control_map.assert_called_once_with(1, "securityplans")
1476
+
1477
+ @patch(f"{PATH}.get_control_implementations_map")
1478
+ def test_upload_evidence_files_no_control_implementations(self, mock_get_control_map):
1479
+ """Test upload_evidence_files when no control implementations exist"""
1480
+ from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_files
1481
+
1482
+ mock_get_control_map.return_value = {}
1483
+
1484
+ evidence_data = {"users": [Path("/test/users.csv")]}
1485
+ api = MagicMock()
1486
+
1487
+ # Should return early when no control implementations are found
1488
+ upload_evidence_files(evidence_data, 1, "securityplans", api, "all")
1489
+
1490
+ mock_get_control_map.assert_called_once()
1491
+
1492
+ @patch(f"{PATH}.upload_evidence_to_controls")
1493
+ @patch(f"{PATH}.get_control_implementations_map")
1494
+ @patch(f"{PATH}.Path")
1495
+ def test_upload_evidence_files_empty_evidence_lists(self, mock_path, mock_get_control_map, mock_upload_evidence):
1496
+ """Test upload_evidence_files handles empty evidence lists"""
1497
+ from regscale.integrations.commercial.microsoft_defender.defender import upload_evidence_files
1498
+
1499
+ mock_get_control_map.return_value = {"AC-2": 1}
1500
+ mock_path.return_value.mkdir.return_value = None
1501
+
1502
+ evidence_data = {
1503
+ "users": [], # Empty list
1504
+ "guest_users": [Path("/test/guests.csv")], # Non-empty list
1505
+ }
1506
+ api = MagicMock()
1507
+
1508
+ upload_evidence_files(evidence_data, 1, "securityplans", api, "users_groups")
1509
+
1510
+ # Should only call upload_evidence_to_controls for non-empty evidence lists
1511
+ mock_upload_evidence.assert_called_once()
1512
+
1513
+ @staticmethod
1514
+ def teardown_class(cls):
1515
+ """Remove test data"""
1516
+ with contextlib.suppress(FileNotFoundError):
1517
+ shutil.rmtree("./artifacts")