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,722 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for AWS S3 Collector in RegScale CLI."""
4
+
5
+ from datetime import datetime
6
+ from unittest.mock import MagicMock, patch
7
+
8
+ import pytest
9
+ from botocore.exceptions import ClientError
10
+
11
+ from regscale.integrations.commercial.aws.inventory.resources.s3 import S3Collector
12
+
13
+
14
+ class TestS3Collector:
15
+ """Test suite for AWS S3 Collector."""
16
+
17
+ @pytest.fixture
18
+ def mock_session(self):
19
+ """Create a mock AWS session."""
20
+ session = MagicMock()
21
+ return session
22
+
23
+ @pytest.fixture
24
+ def mock_s3_client(self):
25
+ """Create a mock S3 client."""
26
+ client = MagicMock()
27
+ return client
28
+
29
+ @pytest.fixture
30
+ def s3_collector(self, mock_session):
31
+ """Create an S3Collector instance for testing."""
32
+ return S3Collector(session=mock_session, region="us-east-1", account_id="123456789012")
33
+
34
+ @pytest.fixture
35
+ def s3_collector_no_account(self, mock_session):
36
+ """Create an S3Collector instance without account_id."""
37
+ return S3Collector(session=mock_session, region="us-west-2")
38
+
39
+ # Test 1: Initialization with account_id
40
+ def test_initialization_with_account_id(self, s3_collector):
41
+ """Should initialize S3Collector with account_id."""
42
+ assert s3_collector.region == "us-east-1"
43
+ assert s3_collector.account_id == "123456789012"
44
+
45
+ # Test 2: Initialization without account_id
46
+ def test_initialization_without_account_id(self, s3_collector_no_account):
47
+ """Should initialize S3Collector without account_id."""
48
+ assert s3_collector_no_account.region == "us-west-2"
49
+ assert s3_collector_no_account.account_id is None
50
+
51
+ # Test 3: Successful collection of buckets with full details
52
+ def test_collect_buckets_successfully(self, s3_collector, mock_s3_client):
53
+ """Should successfully collect S3 buckets with full details."""
54
+ # Mock the _get_client method
55
+ s3_collector._get_client = MagicMock(return_value=mock_s3_client)
56
+
57
+ # Mock list_buckets response
58
+ mock_s3_client.list_buckets.return_value = {
59
+ "Buckets": [
60
+ {"Name": "test-bucket-1", "CreationDate": datetime(2023, 1, 1, 12, 0, 0)},
61
+ {"Name": "test-bucket-2", "CreationDate": datetime(2023, 2, 1, 12, 0, 0)},
62
+ ]
63
+ }
64
+
65
+ # Mock bucket location (both in us-east-1)
66
+ mock_s3_client.get_bucket_location.side_effect = [
67
+ {"LocationConstraint": None}, # us-east-1 returns None
68
+ {"LocationConstraint": None},
69
+ ]
70
+
71
+ # Mock encryption configuration
72
+ mock_s3_client.get_bucket_encryption.side_effect = [
73
+ {
74
+ "ServerSideEncryptionConfiguration": {
75
+ "Rules": [
76
+ {
77
+ "ApplyServerSideEncryptionByDefault": {
78
+ "SSEAlgorithm": "AES256",
79
+ }
80
+ }
81
+ ]
82
+ }
83
+ },
84
+ {
85
+ "ServerSideEncryptionConfiguration": {
86
+ "Rules": [
87
+ {
88
+ "ApplyServerSideEncryptionByDefault": {
89
+ "SSEAlgorithm": "aws:kms",
90
+ "KMSMasterKeyID": "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012",
91
+ }
92
+ }
93
+ ]
94
+ }
95
+ },
96
+ ]
97
+
98
+ # Mock versioning configuration
99
+ mock_s3_client.get_bucket_versioning.side_effect = [
100
+ {"Status": "Enabled", "MFADelete": "Disabled"},
101
+ {"Status": "Disabled", "MFADelete": "Disabled"},
102
+ ]
103
+
104
+ # Mock public access block
105
+ mock_s3_client.get_public_access_block.side_effect = [
106
+ {
107
+ "PublicAccessBlockConfiguration": {
108
+ "BlockPublicAcls": True,
109
+ "IgnorePublicAcls": True,
110
+ "BlockPublicPolicy": True,
111
+ "RestrictPublicBuckets": True,
112
+ }
113
+ },
114
+ {
115
+ "PublicAccessBlockConfiguration": {
116
+ "BlockPublicAcls": False,
117
+ "IgnorePublicAcls": False,
118
+ "BlockPublicPolicy": False,
119
+ "RestrictPublicBuckets": False,
120
+ }
121
+ },
122
+ ]
123
+
124
+ # Mock bucket policy status
125
+ mock_s3_client.get_bucket_policy_status.side_effect = [
126
+ {"PolicyStatus": {"IsPublic": False}},
127
+ {"PolicyStatus": {"IsPublic": True}},
128
+ ]
129
+
130
+ # Mock bucket ACL
131
+ mock_s3_client.get_bucket_acl.side_effect = [
132
+ {
133
+ "Owner": {"DisplayName": "owner1", "ID": "owner-id-1"},
134
+ "Grants": [{"Grantee": {"Type": "CanonicalUser"}, "Permission": "FULL_CONTROL"}],
135
+ },
136
+ {
137
+ "Owner": {"DisplayName": "owner2", "ID": "owner-id-2"},
138
+ "Grants": [
139
+ {"Grantee": {"Type": "CanonicalUser"}, "Permission": "FULL_CONTROL"},
140
+ {"Grantee": {"Type": "Group"}, "Permission": "READ"},
141
+ ],
142
+ },
143
+ ]
144
+
145
+ # Mock bucket tagging
146
+ mock_s3_client.get_bucket_tagging.side_effect = [
147
+ {"TagSet": [{"Key": "Environment", "Value": "Production"}, {"Key": "Owner", "Value": "TeamA"}]},
148
+ {"TagSet": [{"Key": "Environment", "Value": "Development"}]},
149
+ ]
150
+
151
+ # Mock bucket logging
152
+ mock_s3_client.get_bucket_logging.side_effect = [
153
+ {"LoggingEnabled": {"TargetBucket": "log-bucket", "TargetPrefix": "logs/"}},
154
+ {},
155
+ ]
156
+
157
+ result = s3_collector.collect()
158
+
159
+ assert "Buckets" in result
160
+ assert len(result["Buckets"]) == 2
161
+
162
+ # Verify first bucket
163
+ bucket1 = result["Buckets"][0]
164
+ assert bucket1["Name"] == "test-bucket-1"
165
+ assert bucket1["Region"] == "us-east-1"
166
+ assert bucket1["Location"] == "us-east-1"
167
+ assert bucket1["Encryption"]["Enabled"] is True
168
+ assert bucket1["Encryption"]["Algorithm"] == "AES256"
169
+ assert bucket1["Versioning"]["Status"] == "Enabled"
170
+ assert bucket1["PublicAccessBlock"]["BlockPublicAcls"] is True
171
+ assert bucket1["PolicyStatus"]["IsPublic"] is False
172
+ assert bucket1["ACL"]["GrantCount"] == 1
173
+ assert len(bucket1["Tags"]) == 2
174
+ assert bucket1["Logging"]["Enabled"] is True
175
+
176
+ # Verify second bucket
177
+ bucket2 = result["Buckets"][1]
178
+ assert bucket2["Name"] == "test-bucket-2"
179
+ assert bucket2["Encryption"]["Algorithm"] == "aws:kms"
180
+ assert bucket2["PublicAccessBlock"]["BlockPublicAcls"] is False
181
+ assert bucket2["PolicyStatus"]["IsPublic"] is True
182
+ assert bucket2["ACL"]["GrantCount"] == 2
183
+ assert len(bucket2["Tags"]) == 1
184
+ assert bucket2["Logging"]["Enabled"] is False
185
+
186
+ # Test 4: Bucket location retrieval
187
+ def test_get_bucket_location_us_east_1(self, s3_collector, mock_s3_client):
188
+ """Should return us-east-1 when LocationConstraint is None."""
189
+ mock_s3_client.get_bucket_location.return_value = {"LocationConstraint": None}
190
+
191
+ location = s3_collector._get_bucket_location(mock_s3_client, "test-bucket")
192
+
193
+ assert location == "us-east-1"
194
+
195
+ def test_get_bucket_location_other_region(self, s3_collector, mock_s3_client):
196
+ """Should return correct region when LocationConstraint is set."""
197
+ mock_s3_client.get_bucket_location.return_value = {"LocationConstraint": "us-west-2"}
198
+
199
+ location = s3_collector._get_bucket_location(mock_s3_client, "test-bucket")
200
+
201
+ assert location == "us-west-2"
202
+
203
+ def test_get_bucket_location_error(self, s3_collector, mock_s3_client):
204
+ """Should return unknown when ClientError occurs."""
205
+ mock_s3_client.get_bucket_location.side_effect = ClientError(
206
+ {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_location"
207
+ )
208
+
209
+ location = s3_collector._get_bucket_location(mock_s3_client, "test-bucket")
210
+
211
+ assert location == "unknown"
212
+
213
+ # Test 5: Bucket encryption configuration (enabled)
214
+ def test_get_bucket_encryption_aes256(self, s3_collector, mock_s3_client):
215
+ """Should return encryption configuration with AES256 algorithm."""
216
+ mock_s3_client.get_bucket_encryption.return_value = {
217
+ "ServerSideEncryptionConfiguration": {
218
+ "Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]
219
+ }
220
+ }
221
+
222
+ encryption = s3_collector._get_bucket_encryption(mock_s3_client, "test-bucket")
223
+
224
+ assert encryption["Enabled"] is True
225
+ assert encryption["Algorithm"] == "AES256"
226
+ assert encryption["KMSMasterKeyID"] is None
227
+
228
+ def test_get_bucket_encryption_kms(self, s3_collector, mock_s3_client):
229
+ """Should return encryption configuration with KMS algorithm."""
230
+ kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
231
+ mock_s3_client.get_bucket_encryption.return_value = {
232
+ "ServerSideEncryptionConfiguration": {
233
+ "Rules": [
234
+ {
235
+ "ApplyServerSideEncryptionByDefault": {
236
+ "SSEAlgorithm": "aws:kms",
237
+ "KMSMasterKeyID": kms_key_id,
238
+ }
239
+ }
240
+ ]
241
+ }
242
+ }
243
+
244
+ encryption = s3_collector._get_bucket_encryption(mock_s3_client, "test-bucket")
245
+
246
+ assert encryption["Enabled"] is True
247
+ assert encryption["Algorithm"] == "aws:kms"
248
+ assert encryption["KMSMasterKeyID"] == kms_key_id
249
+
250
+ # Test 6: Bucket encryption configuration (disabled)
251
+ def test_get_bucket_encryption_not_found(self, s3_collector, mock_s3_client):
252
+ """Should return Enabled False when ServerSideEncryptionConfigurationNotFoundError occurs."""
253
+ mock_s3_client.get_bucket_encryption.side_effect = ClientError(
254
+ {"Error": {"Code": "ServerSideEncryptionConfigurationNotFoundError", "Message": "Not found"}},
255
+ "get_bucket_encryption",
256
+ )
257
+
258
+ encryption = s3_collector._get_bucket_encryption(mock_s3_client, "test-bucket")
259
+
260
+ assert encryption["Enabled"] is False
261
+
262
+ def test_get_bucket_encryption_empty_rules(self, s3_collector, mock_s3_client):
263
+ """Should return Enabled False when encryption rules are empty."""
264
+ mock_s3_client.get_bucket_encryption.return_value = {"ServerSideEncryptionConfiguration": {"Rules": []}}
265
+
266
+ encryption = s3_collector._get_bucket_encryption(mock_s3_client, "test-bucket")
267
+
268
+ assert encryption["Enabled"] is False
269
+
270
+ def test_get_bucket_encryption_other_error(self, s3_collector, mock_s3_client):
271
+ """Should return empty dict for other ClientError."""
272
+ mock_s3_client.get_bucket_encryption.side_effect = ClientError(
273
+ {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_encryption"
274
+ )
275
+
276
+ encryption = s3_collector._get_bucket_encryption(mock_s3_client, "test-bucket")
277
+
278
+ assert encryption == {}
279
+
280
+ # Test 7: Bucket versioning configuration
281
+ def test_get_bucket_versioning_enabled(self, s3_collector, mock_s3_client):
282
+ """Should return versioning status as Enabled."""
283
+ mock_s3_client.get_bucket_versioning.return_value = {"Status": "Enabled", "MFADelete": "Enabled"}
284
+
285
+ versioning = s3_collector._get_bucket_versioning(mock_s3_client, "test-bucket")
286
+
287
+ assert versioning["Status"] == "Enabled"
288
+ assert versioning["MFADelete"] == "Enabled"
289
+
290
+ def test_get_bucket_versioning_disabled(self, s3_collector, mock_s3_client):
291
+ """Should return versioning status as Disabled when not set."""
292
+ mock_s3_client.get_bucket_versioning.return_value = {}
293
+
294
+ versioning = s3_collector._get_bucket_versioning(mock_s3_client, "test-bucket")
295
+
296
+ assert versioning["Status"] == "Disabled"
297
+ assert versioning["MFADelete"] == "Disabled"
298
+
299
+ def test_get_bucket_versioning_error(self, s3_collector, mock_s3_client):
300
+ """Should return empty dict when ClientError occurs."""
301
+ mock_s3_client.get_bucket_versioning.side_effect = ClientError(
302
+ {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_versioning"
303
+ )
304
+
305
+ versioning = s3_collector._get_bucket_versioning(mock_s3_client, "test-bucket")
306
+
307
+ assert versioning == {}
308
+
309
+ # Test 8: Public access block configuration (enabled)
310
+ def test_get_public_access_block_enabled(self, s3_collector, mock_s3_client):
311
+ """Should return public access block configuration when enabled."""
312
+ mock_s3_client.get_public_access_block.return_value = {
313
+ "PublicAccessBlockConfiguration": {
314
+ "BlockPublicAcls": True,
315
+ "IgnorePublicAcls": True,
316
+ "BlockPublicPolicy": True,
317
+ "RestrictPublicBuckets": True,
318
+ }
319
+ }
320
+
321
+ public_access_block = s3_collector._get_public_access_block(mock_s3_client, "test-bucket")
322
+
323
+ assert public_access_block["BlockPublicAcls"] is True
324
+ assert public_access_block["IgnorePublicAcls"] is True
325
+ assert public_access_block["BlockPublicPolicy"] is True
326
+ assert public_access_block["RestrictPublicBuckets"] is True
327
+
328
+ # Test 9: Public access block configuration (disabled)
329
+ def test_get_public_access_block_disabled(self, s3_collector, mock_s3_client):
330
+ """Should return all False when NoSuchPublicAccessBlockConfiguration occurs."""
331
+ mock_s3_client.get_public_access_block.side_effect = ClientError(
332
+ {"Error": {"Code": "NoSuchPublicAccessBlockConfiguration", "Message": "Not found"}},
333
+ "get_public_access_block",
334
+ )
335
+
336
+ public_access_block = s3_collector._get_public_access_block(mock_s3_client, "test-bucket")
337
+
338
+ assert public_access_block["BlockPublicAcls"] is False
339
+ assert public_access_block["IgnorePublicAcls"] is False
340
+ assert public_access_block["BlockPublicPolicy"] is False
341
+ assert public_access_block["RestrictPublicBuckets"] is False
342
+
343
+ def test_get_public_access_block_other_error(self, s3_collector, mock_s3_client):
344
+ """Should return empty dict for other ClientError."""
345
+ mock_s3_client.get_public_access_block.side_effect = ClientError(
346
+ {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_public_access_block"
347
+ )
348
+
349
+ public_access_block = s3_collector._get_public_access_block(mock_s3_client, "test-bucket")
350
+
351
+ assert public_access_block == {}
352
+
353
+ # Test 10: Bucket policy status (public and private)
354
+ def test_get_bucket_policy_status_public(self, s3_collector, mock_s3_client):
355
+ """Should return IsPublic True when bucket policy is public."""
356
+ mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": True}}
357
+
358
+ policy_status = s3_collector._get_bucket_policy_status(mock_s3_client, "test-bucket")
359
+
360
+ assert policy_status["IsPublic"] is True
361
+
362
+ def test_get_bucket_policy_status_private(self, s3_collector, mock_s3_client):
363
+ """Should return IsPublic False when bucket policy is private."""
364
+ mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": False}}
365
+
366
+ policy_status = s3_collector._get_bucket_policy_status(mock_s3_client, "test-bucket")
367
+
368
+ assert policy_status["IsPublic"] is False
369
+
370
+ def test_get_bucket_policy_status_no_policy(self, s3_collector, mock_s3_client):
371
+ """Should return IsPublic False when NoSuchBucketPolicy error occurs."""
372
+ mock_s3_client.get_bucket_policy_status.side_effect = ClientError(
373
+ {"Error": {"Code": "NoSuchBucketPolicy", "Message": "Not found"}}, "get_bucket_policy_status"
374
+ )
375
+
376
+ policy_status = s3_collector._get_bucket_policy_status(mock_s3_client, "test-bucket")
377
+
378
+ assert policy_status["IsPublic"] is False
379
+
380
+ def test_get_bucket_policy_status_other_error(self, s3_collector, mock_s3_client):
381
+ """Should return empty dict for other ClientError."""
382
+ mock_s3_client.get_bucket_policy_status.side_effect = ClientError(
383
+ {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_policy_status"
384
+ )
385
+
386
+ policy_status = s3_collector._get_bucket_policy_status(mock_s3_client, "test-bucket")
387
+
388
+ assert policy_status == {}
389
+
390
+ # Test 11: Bucket ACL retrieval
391
+ def test_get_bucket_acl_single_grant(self, s3_collector, mock_s3_client):
392
+ """Should return ACL information with single grant."""
393
+ mock_s3_client.get_bucket_acl.return_value = {
394
+ "Owner": {"DisplayName": "owner1", "ID": "owner-id-1"},
395
+ "Grants": [{"Grantee": {"Type": "CanonicalUser"}, "Permission": "FULL_CONTROL"}],
396
+ }
397
+
398
+ acl = s3_collector._get_bucket_acl(mock_s3_client, "test-bucket")
399
+
400
+ assert acl["Owner"]["DisplayName"] == "owner1"
401
+ assert acl["Owner"]["ID"] == "owner-id-1"
402
+ assert acl["GrantCount"] == 1
403
+
404
+ def test_get_bucket_acl_multiple_grants(self, s3_collector, mock_s3_client):
405
+ """Should return ACL information with multiple grants."""
406
+ mock_s3_client.get_bucket_acl.return_value = {
407
+ "Owner": {"DisplayName": "owner1", "ID": "owner-id-1"},
408
+ "Grants": [
409
+ {"Grantee": {"Type": "CanonicalUser"}, "Permission": "FULL_CONTROL"},
410
+ {"Grantee": {"Type": "Group"}, "Permission": "READ"},
411
+ {"Grantee": {"Type": "Group"}, "Permission": "WRITE"},
412
+ ],
413
+ }
414
+
415
+ acl = s3_collector._get_bucket_acl(mock_s3_client, "test-bucket")
416
+
417
+ assert acl["GrantCount"] == 3
418
+
419
+ def test_get_bucket_acl_error(self, s3_collector, mock_s3_client):
420
+ """Should return empty dict when ClientError occurs."""
421
+ mock_s3_client.get_bucket_acl.side_effect = ClientError(
422
+ {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_acl"
423
+ )
424
+
425
+ acl = s3_collector._get_bucket_acl(mock_s3_client, "test-bucket")
426
+
427
+ assert acl == {}
428
+
429
+ # Test 12: Bucket tagging (with tags)
430
+ def test_get_bucket_tagging_with_tags(self, s3_collector, mock_s3_client):
431
+ """Should return bucket tags when tags exist."""
432
+ mock_s3_client.get_bucket_tagging.return_value = {
433
+ "TagSet": [
434
+ {"Key": "Environment", "Value": "Production"},
435
+ {"Key": "Owner", "Value": "TeamA"},
436
+ {"Key": "CostCenter", "Value": "Engineering"},
437
+ ]
438
+ }
439
+
440
+ tags = s3_collector._get_bucket_tagging(mock_s3_client, "test-bucket")
441
+
442
+ assert len(tags) == 3
443
+ assert tags[0]["Key"] == "Environment"
444
+ assert tags[0]["Value"] == "Production"
445
+ assert tags[1]["Key"] == "Owner"
446
+ assert tags[1]["Value"] == "TeamA"
447
+
448
+ # Test 13: Bucket tagging (without tags)
449
+ def test_get_bucket_tagging_no_tags(self, s3_collector, mock_s3_client):
450
+ """Should return empty list when NoSuchTagSet error occurs."""
451
+ mock_s3_client.get_bucket_tagging.side_effect = ClientError(
452
+ {"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"
453
+ )
454
+
455
+ tags = s3_collector._get_bucket_tagging(mock_s3_client, "test-bucket")
456
+
457
+ assert tags == []
458
+
459
+ def test_get_bucket_tagging_other_error(self, s3_collector, mock_s3_client):
460
+ """Should return empty list for other ClientError."""
461
+ mock_s3_client.get_bucket_tagging.side_effect = ClientError(
462
+ {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_tagging"
463
+ )
464
+
465
+ tags = s3_collector._get_bucket_tagging(mock_s3_client, "test-bucket")
466
+
467
+ assert tags == []
468
+
469
+ # Test 14: Bucket logging configuration
470
+ def test_get_bucket_logging_enabled(self, s3_collector, mock_s3_client):
471
+ """Should return logging configuration when logging is enabled."""
472
+ mock_s3_client.get_bucket_logging.return_value = {
473
+ "LoggingEnabled": {
474
+ "TargetBucket": "log-bucket",
475
+ "TargetPrefix": "logs/",
476
+ }
477
+ }
478
+
479
+ logging_config = s3_collector._get_bucket_logging(mock_s3_client, "test-bucket")
480
+
481
+ assert logging_config["Enabled"] is True
482
+ assert logging_config["TargetBucket"] == "log-bucket"
483
+ assert logging_config["TargetPrefix"] == "logs/"
484
+
485
+ def test_get_bucket_logging_disabled(self, s3_collector, mock_s3_client):
486
+ """Should return Enabled False when logging is not configured."""
487
+ mock_s3_client.get_bucket_logging.return_value = {}
488
+
489
+ logging_config = s3_collector._get_bucket_logging(mock_s3_client, "test-bucket")
490
+
491
+ assert logging_config["Enabled"] is False
492
+
493
+ def test_get_bucket_logging_error(self, s3_collector, mock_s3_client):
494
+ """Should return empty dict when ClientError occurs."""
495
+ mock_s3_client.get_bucket_logging.side_effect = ClientError(
496
+ {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_logging"
497
+ )
498
+
499
+ logging_config = s3_collector._get_bucket_logging(mock_s3_client, "test-bucket")
500
+
501
+ assert logging_config == {}
502
+
503
+ # Test 15: Region filtering
504
+ def test_region_filtering_excludes_other_regions(self, s3_collector, mock_s3_client):
505
+ """Should only include buckets in the target region."""
506
+ s3_collector._get_client = MagicMock(return_value=mock_s3_client)
507
+
508
+ mock_s3_client.list_buckets.return_value = {
509
+ "Buckets": [
510
+ {"Name": "bucket-us-east-1", "CreationDate": datetime(2023, 1, 1, 12, 0, 0)},
511
+ {"Name": "bucket-us-west-2", "CreationDate": datetime(2023, 2, 1, 12, 0, 0)},
512
+ {"Name": "bucket-eu-west-1", "CreationDate": datetime(2023, 3, 1, 12, 0, 0)},
513
+ ]
514
+ }
515
+
516
+ # First bucket is in us-east-1 (target region)
517
+ # Second bucket is in us-west-2
518
+ # Third bucket is in eu-west-1
519
+ mock_s3_client.get_bucket_location.side_effect = [
520
+ {"LocationConstraint": None}, # us-east-1
521
+ {"LocationConstraint": "us-west-2"},
522
+ {"LocationConstraint": "eu-west-1"},
523
+ ]
524
+
525
+ # Mock other required calls for the bucket in target region
526
+ mock_s3_client.get_bucket_encryption.return_value = {"ServerSideEncryptionConfiguration": {"Rules": []}}
527
+ mock_s3_client.get_bucket_versioning.return_value = {"Status": "Disabled"}
528
+ mock_s3_client.get_public_access_block.return_value = {"PublicAccessBlockConfiguration": {}}
529
+ mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": False}}
530
+ mock_s3_client.get_bucket_acl.return_value = {"Owner": {}, "Grants": []}
531
+ mock_s3_client.get_bucket_tagging.side_effect = ClientError(
532
+ {"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"
533
+ )
534
+ mock_s3_client.get_bucket_logging.return_value = {}
535
+
536
+ result = s3_collector.collect()
537
+
538
+ # Only the bucket in us-east-1 should be included
539
+ assert len(result["Buckets"]) == 1
540
+ assert result["Buckets"][0]["Name"] == "bucket-us-east-1"
541
+ assert result["Buckets"][0]["Region"] == "us-east-1"
542
+
543
+ def test_region_filtering_verifies_region_tag(self, s3_collector, mock_s3_client):
544
+ """Should verify Region tag is set for all buckets in target region."""
545
+ s3_collector._get_client = MagicMock(return_value=mock_s3_client)
546
+
547
+ mock_s3_client.list_buckets.return_value = {
548
+ "Buckets": [
549
+ {"Name": "test-bucket-1", "CreationDate": datetime(2023, 1, 1, 12, 0, 0)},
550
+ {"Name": "test-bucket-2", "CreationDate": datetime(2023, 2, 1, 12, 0, 0)},
551
+ ]
552
+ }
553
+
554
+ mock_s3_client.get_bucket_location.side_effect = [
555
+ {"LocationConstraint": None}, # us-east-1
556
+ {"LocationConstraint": None}, # us-east-1
557
+ ]
558
+
559
+ # Mock other required calls
560
+ mock_s3_client.get_bucket_encryption.return_value = {"ServerSideEncryptionConfiguration": {"Rules": []}}
561
+ mock_s3_client.get_bucket_versioning.return_value = {"Status": "Disabled"}
562
+ mock_s3_client.get_public_access_block.return_value = {"PublicAccessBlockConfiguration": {}}
563
+ mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": False}}
564
+ mock_s3_client.get_bucket_acl.return_value = {"Owner": {}, "Grants": []}
565
+ mock_s3_client.get_bucket_tagging.side_effect = [
566
+ ClientError({"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"),
567
+ ClientError({"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"),
568
+ ]
569
+ mock_s3_client.get_bucket_logging.return_value = {}
570
+
571
+ result = s3_collector.collect()
572
+
573
+ # Verify all buckets have Region tag
574
+ assert len(result["Buckets"]) == 2
575
+ for bucket in result["Buckets"]:
576
+ assert "Region" in bucket
577
+ assert bucket["Region"] == "us-east-1"
578
+
579
+ # Test 16: Error handling - AccessDenied during list_buckets
580
+ def test_collect_handles_access_denied_list_buckets(self, s3_collector, mock_s3_client):
581
+ """Should handle AccessDenied error when listing buckets."""
582
+ s3_collector._get_client = MagicMock(return_value=mock_s3_client)
583
+
584
+ mock_s3_client.list_buckets.side_effect = ClientError(
585
+ {"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "list_buckets"
586
+ )
587
+
588
+ with patch("regscale.integrations.commercial.aws.inventory.resources.s3.logger") as mock_logger:
589
+ result = s3_collector.collect()
590
+
591
+ assert result["Buckets"] == []
592
+ mock_logger.warning.assert_called_once()
593
+
594
+ # Test 17: Error handling - NoSuchBucket during bucket details retrieval
595
+ def test_list_buckets_handles_nosuchbucket_error(self, s3_collector, mock_s3_client):
596
+ """Should skip bucket when NoSuchBucket error occurs during details retrieval."""
597
+ s3_collector._get_client = MagicMock(return_value=mock_s3_client)
598
+
599
+ mock_s3_client.list_buckets.return_value = {
600
+ "Buckets": [
601
+ {"Name": "bucket-exists", "CreationDate": datetime(2023, 1, 1, 12, 0, 0)},
602
+ {"Name": "bucket-deleted", "CreationDate": datetime(2023, 2, 1, 12, 0, 0)},
603
+ ]
604
+ }
605
+
606
+ # First bucket succeeds, second bucket throws NoSuchBucket
607
+ mock_s3_client.get_bucket_location.side_effect = [
608
+ {"LocationConstraint": None}, # us-east-1
609
+ ClientError({"Error": {"Code": "NoSuchBucket", "Message": "Bucket not found"}}, "get_bucket_location"),
610
+ ]
611
+
612
+ # Mock other required calls for the first bucket
613
+ mock_s3_client.get_bucket_encryption.return_value = {"ServerSideEncryptionConfiguration": {"Rules": []}}
614
+ mock_s3_client.get_bucket_versioning.return_value = {"Status": "Disabled"}
615
+ mock_s3_client.get_public_access_block.return_value = {"PublicAccessBlockConfiguration": {}}
616
+ mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": False}}
617
+ mock_s3_client.get_bucket_acl.return_value = {"Owner": {}, "Grants": []}
618
+ mock_s3_client.get_bucket_tagging.side_effect = ClientError(
619
+ {"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"
620
+ )
621
+ mock_s3_client.get_bucket_logging.return_value = {}
622
+
623
+ result = s3_collector.collect()
624
+
625
+ # Only the first bucket should be included
626
+ assert len(result["Buckets"]) == 1
627
+ assert result["Buckets"][0]["Name"] == "bucket-exists"
628
+
629
+ # Test 18: Error handling - AccessDenied during bucket details retrieval
630
+ def test_list_buckets_handles_access_denied_details(self, s3_collector, mock_s3_client):
631
+ """Should skip bucket when AccessDenied error occurs during details retrieval."""
632
+ s3_collector._get_client = MagicMock(return_value=mock_s3_client)
633
+
634
+ mock_s3_client.list_buckets.return_value = {
635
+ "Buckets": [
636
+ {"Name": "bucket-accessible", "CreationDate": datetime(2023, 1, 1, 12, 0, 0)},
637
+ {"Name": "bucket-restricted", "CreationDate": datetime(2023, 2, 1, 12, 0, 0)},
638
+ ]
639
+ }
640
+
641
+ # First bucket succeeds, second bucket throws AccessDenied
642
+ mock_s3_client.get_bucket_location.side_effect = [
643
+ {"LocationConstraint": None}, # us-east-1
644
+ ClientError({"Error": {"Code": "AccessDenied", "Message": "Access Denied"}}, "get_bucket_location"),
645
+ ]
646
+
647
+ # Mock other required calls for the first bucket
648
+ mock_s3_client.get_bucket_encryption.return_value = {"ServerSideEncryptionConfiguration": {"Rules": []}}
649
+ mock_s3_client.get_bucket_versioning.return_value = {"Status": "Disabled"}
650
+ mock_s3_client.get_public_access_block.return_value = {"PublicAccessBlockConfiguration": {}}
651
+ mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": False}}
652
+ mock_s3_client.get_bucket_acl.return_value = {"Owner": {}, "Grants": []}
653
+ mock_s3_client.get_bucket_tagging.side_effect = ClientError(
654
+ {"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"
655
+ )
656
+ mock_s3_client.get_bucket_logging.return_value = {}
657
+
658
+ result = s3_collector.collect()
659
+
660
+ # Only the first bucket should be included
661
+ assert len(result["Buckets"]) == 1
662
+ assert result["Buckets"][0]["Name"] == "bucket-accessible"
663
+
664
+ # Test 19: Error handling - Unexpected error during collection
665
+ def test_collect_handles_unexpected_error(self, s3_collector, mock_s3_client):
666
+ """Should handle unexpected errors during collection."""
667
+ s3_collector._get_client = MagicMock(return_value=mock_s3_client)
668
+
669
+ mock_s3_client.list_buckets.side_effect = Exception("Unexpected error")
670
+
671
+ with patch("regscale.integrations.commercial.aws.inventory.resources.s3.logger") as mock_logger:
672
+ result = s3_collector.collect()
673
+
674
+ assert result["Buckets"] == []
675
+ mock_logger.error.assert_called_once()
676
+
677
+ # Test 20: Collect with ClientError during main operation
678
+ def test_collect_handles_client_error(self, s3_collector, mock_s3_client):
679
+ """Should handle ClientError during main collection operation."""
680
+ s3_collector._get_client = MagicMock(
681
+ side_effect=ClientError({"Error": {"Code": "UnauthorizedOperation", "Message": "Unauthorized"}}, "client")
682
+ )
683
+
684
+ result = s3_collector.collect()
685
+
686
+ # The error should be handled by _handle_error method
687
+ assert result["Buckets"] == []
688
+
689
+ # Test 21: Error handling for other exceptions during bucket details retrieval
690
+ def test_list_buckets_handles_other_errors(self, s3_collector, mock_s3_client):
691
+ """Should skip bucket when non-AccessDenied/NoSuchBucket error occurs during details retrieval."""
692
+ s3_collector._get_client = MagicMock(return_value=mock_s3_client)
693
+
694
+ mock_s3_client.list_buckets.return_value = {
695
+ "Buckets": [
696
+ {"Name": "bucket-ok", "CreationDate": datetime(2023, 1, 1, 12, 0, 0)},
697
+ {"Name": "bucket-error", "CreationDate": datetime(2023, 2, 1, 12, 0, 0)},
698
+ ]
699
+ }
700
+
701
+ # First bucket succeeds, second bucket throws unexpected error
702
+ mock_s3_client.get_bucket_location.side_effect = [
703
+ {"LocationConstraint": None}, # us-east-1
704
+ ClientError({"Error": {"Code": "InternalError", "Message": "Internal Error"}}, "get_bucket_location"),
705
+ ]
706
+
707
+ # Mock other required calls for the first bucket
708
+ mock_s3_client.get_bucket_encryption.return_value = {"ServerSideEncryptionConfiguration": {"Rules": []}}
709
+ mock_s3_client.get_bucket_versioning.return_value = {"Status": "Disabled"}
710
+ mock_s3_client.get_public_access_block.return_value = {"PublicAccessBlockConfiguration": {}}
711
+ mock_s3_client.get_bucket_policy_status.return_value = {"PolicyStatus": {"IsPublic": False}}
712
+ mock_s3_client.get_bucket_acl.return_value = {"Owner": {}, "Grants": []}
713
+ mock_s3_client.get_bucket_tagging.side_effect = ClientError(
714
+ {"Error": {"Code": "NoSuchTagSet", "Message": "Not found"}}, "get_bucket_tagging"
715
+ )
716
+ mock_s3_client.get_bucket_logging.return_value = {}
717
+
718
+ result = s3_collector.collect()
719
+
720
+ # Only the first bucket should be included (second bucket skipped due to error)
721
+ assert len(result["Buckets"]) == 1
722
+ assert result["Buckets"][0]["Name"] == "bucket-ok"