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,987 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for AWS S3 Evidence Integration."""
4
+
5
+ import gzip
6
+ import json
7
+ import os
8
+ from datetime import datetime, timedelta
9
+ from pathlib import Path
10
+ from unittest.mock import MagicMock, Mock, patch
11
+
12
+ import pytest
13
+
14
+ from regscale.integrations.commercial.aws.s3_evidence import (
15
+ S3ComplianceItem,
16
+ AWSS3EvidenceIntegration,
17
+ )
18
+
19
+ PATH = "regscale.integrations.commercial.aws.s3_evidence"
20
+
21
+
22
+ class TestS3ComplianceItem:
23
+ """Test S3ComplianceItem class."""
24
+
25
+ def test_init_with_complete_data(self):
26
+ """Test initialization with complete bucket data."""
27
+ bucket_data = {
28
+ "Name": "test-bucket",
29
+ "Region": "us-east-1",
30
+ "CreationDate": "2023-01-01T00:00:00.000Z",
31
+ "Encryption": {"Enabled": True, "SSEAlgorithm": "AES256"},
32
+ "Versioning": {"Status": "Enabled"},
33
+ "PublicAccessBlock": {
34
+ "BlockPublicAcls": True,
35
+ "IgnorePublicAcls": True,
36
+ "BlockPublicPolicy": True,
37
+ "RestrictPublicBuckets": True,
38
+ },
39
+ "PolicyStatus": {"IsPublic": False},
40
+ "ACL": {"Grants": []},
41
+ "Logging": {"Enabled": True, "TargetBucket": "log-bucket"},
42
+ "Tags": [{"Key": "Environment", "Value": "Production"}],
43
+ }
44
+
45
+ item = S3ComplianceItem(bucket_data)
46
+
47
+ assert item.bucket_name == "test-bucket"
48
+ assert item.region == "us-east-1"
49
+ assert item.creation_date == "2023-01-01T00:00:00.000Z"
50
+ assert item.encryption == {"Enabled": True, "SSEAlgorithm": "AES256"}
51
+ assert item.versioning == {"Status": "Enabled"}
52
+ assert item.public_access_block["BlockPublicAcls"] is True
53
+ assert item.policy_status == {"IsPublic": False}
54
+ assert item.acl == {"Grants": []}
55
+ assert item.logging == {"Enabled": True, "TargetBucket": "log-bucket"}
56
+ assert item.tags == [{"Key": "Environment", "Value": "Production"}]
57
+
58
+ def test_init_with_minimal_data(self):
59
+ """Test initialization with minimal bucket data."""
60
+ bucket_data = {}
61
+
62
+ item = S3ComplianceItem(bucket_data)
63
+
64
+ assert item.bucket_name == ""
65
+ assert item.region == ""
66
+ assert item.creation_date == ""
67
+ assert item.encryption == {}
68
+ assert item.versioning == {}
69
+ assert item.public_access_block == {}
70
+ assert item.policy_status == {}
71
+ assert item.acl == {}
72
+ assert item.logging == {}
73
+ assert item.tags == []
74
+
75
+ def test_to_dict(self):
76
+ """Test conversion to dictionary."""
77
+ bucket_data = {
78
+ "Name": "test-bucket",
79
+ "Region": "us-west-2",
80
+ "Encryption": {"Enabled": False},
81
+ }
82
+
83
+ item = S3ComplianceItem(bucket_data)
84
+ result = item.to_dict()
85
+
86
+ assert result == bucket_data
87
+ assert result is item.raw_data
88
+
89
+
90
+ class TestAWSS3EvidenceIntegration:
91
+ """Test AWSS3EvidenceIntegration class."""
92
+
93
+ @patch(f"{PATH}.S3ControlMapper")
94
+ @patch(f"{PATH}.Api")
95
+ def test_init_with_defaults(self, mock_api, mock_mapper):
96
+ """Test initialization with default parameters."""
97
+ integration = AWSS3EvidenceIntegration(plan_id=123)
98
+
99
+ assert integration.plan_id == 123
100
+ assert integration.region == "us-east-1"
101
+ assert integration.account_id is None
102
+ assert integration.tags == {}
103
+ assert integration.bucket_name_filter is None
104
+ assert integration.create_evidence is False
105
+ assert integration.create_ssp_attachment is True
106
+ assert integration.evidence_control_ids == []
107
+ assert integration.force_refresh is False
108
+ assert integration.aws_profile is None
109
+ assert integration.aws_access_key_id is None
110
+ assert integration.aws_secret_access_key is None
111
+ assert integration.aws_session_token is None
112
+ assert integration.session is None
113
+ assert integration.collector is None
114
+ assert integration.cache_ttl_hours == 4
115
+ assert integration.raw_s3_data == {}
116
+ assert integration.buckets == []
117
+ mock_api.assert_called_once()
118
+ mock_mapper.assert_called_once_with(framework="NIST800-53R5")
119
+
120
+ @patch(f"{PATH}.S3ControlMapper")
121
+ @patch(f"{PATH}.Api")
122
+ def test_init_with_custom_parameters(self, mock_api, mock_mapper):
123
+ """Test initialization with custom parameters."""
124
+ integration = AWSS3EvidenceIntegration(
125
+ plan_id=456,
126
+ region="us-west-2",
127
+ account_id="123456789012",
128
+ tags={"Environment": "Production"},
129
+ bucket_name_filter="prod",
130
+ create_evidence=True,
131
+ create_ssp_attachment=False,
132
+ evidence_control_ids=["AC-3", "SC-13"],
133
+ force_refresh=True,
134
+ aws_profile="test-profile",
135
+ aws_access_key_id="AKIATEST",
136
+ aws_secret_access_key="secret",
137
+ aws_session_token="token",
138
+ )
139
+
140
+ assert integration.plan_id == 456
141
+ assert integration.region == "us-west-2"
142
+ assert integration.account_id == "123456789012"
143
+ assert integration.tags == {"Environment": "Production"}
144
+ assert integration.bucket_name_filter == "prod"
145
+ assert integration.create_evidence is True
146
+ assert integration.create_ssp_attachment is False
147
+ assert integration.evidence_control_ids == ["AC-3", "SC-13"]
148
+ assert integration.force_refresh is True
149
+ assert integration.aws_profile == "test-profile"
150
+ assert integration.aws_access_key_id == "AKIATEST"
151
+ assert integration.aws_secret_access_key == "secret"
152
+ assert integration.aws_session_token == "token"
153
+
154
+ @patch(f"{PATH}.S3ControlMapper")
155
+ @patch(f"{PATH}.Api")
156
+ def test_get_cache_file_path(self, mock_api, mock_mapper):
157
+ """Test cache file path generation."""
158
+ integration = AWSS3EvidenceIntegration(plan_id=123, region="us-east-1", account_id="123456789012")
159
+
160
+ cache_path = integration._get_cache_file_path()
161
+
162
+ assert isinstance(cache_path, Path)
163
+ assert cache_path.name == "s3_buckets_us-east-1_123456789012.json"
164
+ assert "regscale" in str(cache_path)
165
+ assert "aws_s3_cache" in str(cache_path)
166
+
167
+ @patch(f"{PATH}.S3ControlMapper")
168
+ @patch(f"{PATH}.Api")
169
+ def test_get_cache_file_path_without_account_id(self, mock_api, mock_mapper):
170
+ """Test cache file path generation without account ID."""
171
+ integration = AWSS3EvidenceIntegration(plan_id=123, region="us-west-2")
172
+
173
+ cache_path = integration._get_cache_file_path()
174
+
175
+ assert cache_path.name == "s3_buckets_us-west-2_default.json"
176
+
177
+ @patch(f"{PATH}.S3ControlMapper")
178
+ @patch(f"{PATH}.Api")
179
+ def test_is_cache_valid_no_cache(self, mock_api, mock_mapper):
180
+ """Test cache validation when cache file doesn't exist."""
181
+ integration = AWSS3EvidenceIntegration(plan_id=123)
182
+
183
+ # Ensure cache directory exists (it gets created during init)
184
+ cache_file = integration._get_cache_file_path()
185
+ if cache_file.exists():
186
+ cache_file.unlink()
187
+
188
+ is_valid = integration._is_cache_valid()
189
+
190
+ assert is_valid is False
191
+
192
+ @patch(f"{PATH}.S3ControlMapper")
193
+ @patch(f"{PATH}.Api")
194
+ def test_is_cache_valid_expired(self, mock_api, mock_mapper):
195
+ """Test cache validation when cache is expired."""
196
+ integration = AWSS3EvidenceIntegration(plan_id=123)
197
+ cache_file = integration._get_cache_file_path()
198
+
199
+ # Create cache directory and file
200
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
201
+ cache_file.write_text("[]")
202
+
203
+ # Set modification time to 5 hours ago (expired)
204
+ five_hours_ago = datetime.now() - timedelta(hours=5)
205
+ os.utime(cache_file, (five_hours_ago.timestamp(), five_hours_ago.timestamp()))
206
+
207
+ is_valid = integration._is_cache_valid()
208
+
209
+ assert is_valid is False
210
+
211
+ # Cleanup
212
+ cache_file.unlink()
213
+
214
+ @patch(f"{PATH}.S3ControlMapper")
215
+ @patch(f"{PATH}.Api")
216
+ def test_is_cache_valid_fresh(self, mock_api, mock_mapper):
217
+ """Test cache validation when cache is fresh."""
218
+ integration = AWSS3EvidenceIntegration(plan_id=123)
219
+ cache_file = integration._get_cache_file_path()
220
+
221
+ # Create cache directory and file
222
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
223
+ cache_file.write_text("[]")
224
+
225
+ is_valid = integration._is_cache_valid()
226
+
227
+ assert is_valid is True
228
+
229
+ # Cleanup
230
+ cache_file.unlink()
231
+
232
+ @patch(f"{PATH}.S3ControlMapper")
233
+ @patch(f"{PATH}.Api")
234
+ def test_save_cache(self, mock_api, mock_mapper):
235
+ """Test saving data to cache."""
236
+ integration = AWSS3EvidenceIntegration(plan_id=123)
237
+ cache_file = integration._get_cache_file_path()
238
+
239
+ test_data = [{"Name": "test-bucket"}]
240
+
241
+ integration._save_cache(test_data)
242
+
243
+ assert cache_file.exists()
244
+ with open(cache_file) as f:
245
+ loaded_data = json.load(f)
246
+ assert loaded_data == test_data
247
+
248
+ # Cleanup
249
+ cache_file.unlink()
250
+
251
+ @patch(f"{PATH}.S3ControlMapper")
252
+ @patch(f"{PATH}.Api")
253
+ def test_save_cache_error(self, mock_api, mock_mapper, caplog):
254
+ """Test saving cache with error."""
255
+ integration = AWSS3EvidenceIntegration(plan_id=123)
256
+
257
+ with patch("builtins.open", side_effect=OSError("Permission denied")):
258
+ integration._save_cache({"test": "data"})
259
+
260
+ assert "Failed to save cache" in caplog.text
261
+
262
+ @patch(f"{PATH}.S3ControlMapper")
263
+ @patch(f"{PATH}.Api")
264
+ def test_load_cached_data(self, mock_api, mock_mapper):
265
+ """Test loading data from cache."""
266
+ integration = AWSS3EvidenceIntegration(plan_id=123)
267
+ cache_file = integration._get_cache_file_path()
268
+
269
+ test_data = [{"Name": "test-bucket"}]
270
+
271
+ # Create cache
272
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
273
+ with open(cache_file, "w") as f:
274
+ json.dump(test_data, f)
275
+
276
+ loaded_data = integration._load_cached_data()
277
+
278
+ assert loaded_data == test_data
279
+
280
+ # Cleanup
281
+ cache_file.unlink()
282
+
283
+ @patch(f"{PATH}.S3ControlMapper")
284
+ @patch(f"{PATH}.Api")
285
+ def test_load_cached_data_error(self, mock_api, mock_mapper, caplog):
286
+ """Test loading cache with error."""
287
+ integration = AWSS3EvidenceIntegration(plan_id=123)
288
+
289
+ result = integration._load_cached_data()
290
+
291
+ assert result is None
292
+ # Cache loading can fail either due to file error or invalid format
293
+ assert "Failed to load cache" in caplog.text or "Invalid cache format" in caplog.text
294
+
295
+ @patch(f"{PATH}.S3ControlMapper")
296
+ @patch(f"{PATH}.Api")
297
+ def test_get_cache_age_hours_no_cache(self, mock_api, mock_mapper):
298
+ """Test getting cache age when no cache exists."""
299
+ integration = AWSS3EvidenceIntegration(plan_id=123)
300
+
301
+ age = integration._get_cache_age_hours()
302
+
303
+ assert age == float("inf")
304
+
305
+ @patch(f"{PATH}.S3ControlMapper")
306
+ @patch(f"{PATH}.Api")
307
+ def test_get_cache_age_hours_with_cache(self, mock_api, mock_mapper):
308
+ """Test getting cache age with existing cache."""
309
+ integration = AWSS3EvidenceIntegration(plan_id=123)
310
+ cache_file = integration._get_cache_file_path()
311
+
312
+ # Create cache
313
+ cache_file.parent.mkdir(parents=True, exist_ok=True)
314
+ cache_file.write_text("[]")
315
+
316
+ # Set modification time to 2 hours ago
317
+ two_hours_ago = datetime.now() - timedelta(hours=2)
318
+ os.utime(cache_file, (two_hours_ago.timestamp(), two_hours_ago.timestamp()))
319
+
320
+ age = integration._get_cache_age_hours()
321
+
322
+ assert 1.9 < age < 2.1 # Allow small variance
323
+
324
+ # Cleanup
325
+ cache_file.unlink()
326
+
327
+ @patch(f"{PATH}.boto3.Session")
328
+ @patch(f"{PATH}.S3ControlMapper")
329
+ @patch(f"{PATH}.Api")
330
+ def test_initialize_aws_session_with_keys(self, mock_api, mock_mapper, mock_boto_session):
331
+ """Test AWS session initialization with access keys."""
332
+ mock_session = MagicMock()
333
+ mock_boto_session.return_value = mock_session
334
+
335
+ integration = AWSS3EvidenceIntegration(
336
+ plan_id=123,
337
+ region="us-west-2",
338
+ aws_access_key_id="AKIATEST",
339
+ aws_secret_access_key="secret",
340
+ aws_session_token="token",
341
+ )
342
+
343
+ integration._initialize_aws_session()
344
+
345
+ mock_boto_session.assert_called_once_with(
346
+ aws_access_key_id="AKIATEST",
347
+ aws_secret_access_key="secret",
348
+ aws_session_token="token",
349
+ region_name="us-west-2",
350
+ )
351
+ assert integration.session == mock_session
352
+
353
+ @patch(f"{PATH}.boto3.Session")
354
+ @patch(f"{PATH}.S3ControlMapper")
355
+ @patch(f"{PATH}.Api")
356
+ def test_initialize_aws_session_with_profile(self, mock_api, mock_mapper, mock_boto_session):
357
+ """Test AWS session initialization with profile."""
358
+ mock_session = MagicMock()
359
+ mock_boto_session.return_value = mock_session
360
+
361
+ integration = AWSS3EvidenceIntegration(plan_id=123, region="us-east-1", aws_profile="test-profile")
362
+
363
+ integration._initialize_aws_session()
364
+
365
+ mock_boto_session.assert_called_once_with(profile_name="test-profile", region_name="us-east-1")
366
+ assert integration.session == mock_session
367
+
368
+ @patch(f"{PATH}.boto3.Session")
369
+ @patch(f"{PATH}.S3ControlMapper")
370
+ @patch(f"{PATH}.Api")
371
+ def test_initialize_aws_session_default(self, mock_api, mock_mapper, mock_boto_session):
372
+ """Test AWS session initialization with default credentials."""
373
+ mock_session = MagicMock()
374
+ mock_boto_session.return_value = mock_session
375
+
376
+ integration = AWSS3EvidenceIntegration(plan_id=123, region="us-east-1")
377
+
378
+ integration._initialize_aws_session()
379
+
380
+ mock_boto_session.assert_called_once_with(region_name="us-east-1")
381
+ assert integration.session == mock_session
382
+
383
+ @patch(f"{PATH}.S3ControlMapper")
384
+ @patch(f"{PATH}.Api")
385
+ def test_fetch_compliance_data_from_cache(self, mock_api, mock_mapper):
386
+ """Test fetching compliance data from cache."""
387
+ integration = AWSS3EvidenceIntegration(plan_id=123)
388
+
389
+ cached_data = [{"Name": "test-bucket"}]
390
+
391
+ with patch.object(integration, "_is_cache_valid", return_value=True):
392
+ with patch.object(integration, "_load_cached_data", return_value=cached_data):
393
+ result = integration.fetch_compliance_data()
394
+
395
+ assert result == cached_data
396
+
397
+ @patch(f"{PATH}.S3ControlMapper")
398
+ @patch(f"{PATH}.Api")
399
+ def test_fetch_compliance_data_force_refresh(self, mock_api, mock_mapper):
400
+ """Test fetching compliance data with force refresh."""
401
+ integration = AWSS3EvidenceIntegration(plan_id=123, force_refresh=True)
402
+
403
+ fresh_data = [{"Name": "fresh-bucket"}]
404
+
405
+ with patch.object(integration, "_fetch_fresh_s3_data", return_value=fresh_data):
406
+ result = integration.fetch_compliance_data()
407
+
408
+ assert result == fresh_data
409
+
410
+ @patch(f"{PATH}.S3ControlMapper")
411
+ @patch(f"{PATH}.Api")
412
+ def test_fetch_compliance_data_cache_empty(self, mock_api, mock_mapper):
413
+ """Test fetching compliance data when cache returns None."""
414
+ integration = AWSS3EvidenceIntegration(plan_id=123)
415
+
416
+ fresh_data = [{"Name": "fresh-bucket"}]
417
+
418
+ with patch.object(integration, "_is_cache_valid", return_value=True):
419
+ with patch.object(integration, "_load_cached_data", return_value=None):
420
+ with patch.object(integration, "_fetch_fresh_s3_data", return_value=fresh_data):
421
+ result = integration.fetch_compliance_data()
422
+
423
+ assert result == fresh_data
424
+
425
+ @patch(f"{PATH}.S3Collector")
426
+ @patch(f"{PATH}.S3ControlMapper")
427
+ @patch(f"{PATH}.Api")
428
+ def test_fetch_fresh_s3_data(self, mock_api, mock_mapper, mock_collector_class):
429
+ """Test fetching fresh S3 data."""
430
+ mock_collector = MagicMock()
431
+ mock_collector_class.return_value = mock_collector
432
+ mock_collector.collect.return_value = {
433
+ "Buckets": [{"Name": "bucket-1"}, {"Name": "bucket-2"}],
434
+ }
435
+
436
+ integration = AWSS3EvidenceIntegration(plan_id=123, region="us-east-1")
437
+
438
+ mock_session = MagicMock()
439
+ integration.session = mock_session
440
+
441
+ with patch.object(integration, "_save_cache"):
442
+ result = integration._fetch_fresh_s3_data()
443
+
444
+ assert len(result) == 2
445
+ assert result[0]["Name"] == "bucket-1"
446
+ mock_collector_class.assert_called_once_with(session=mock_session, region="us-east-1", account_id=None, tags={})
447
+
448
+ @patch(f"{PATH}.S3Collector")
449
+ @patch(f"{PATH}.S3ControlMapper")
450
+ @patch(f"{PATH}.Api")
451
+ def test_fetch_fresh_s3_data_with_filter(self, mock_api, mock_mapper, mock_collector_class):
452
+ """Test fetching fresh S3 data with bucket name filter."""
453
+ mock_collector = MagicMock()
454
+ mock_collector_class.return_value = mock_collector
455
+ mock_collector.collect.return_value = {
456
+ "Buckets": [
457
+ {"Name": "prod-bucket"},
458
+ {"Name": "dev-bucket"},
459
+ {"Name": "prod-bucket-2"},
460
+ ],
461
+ }
462
+
463
+ integration = AWSS3EvidenceIntegration(plan_id=123, bucket_name_filter="prod")
464
+
465
+ mock_session = MagicMock()
466
+ integration.session = mock_session
467
+
468
+ with patch.object(integration, "_save_cache"):
469
+ result = integration._fetch_fresh_s3_data()
470
+
471
+ assert len(result) == 2
472
+ assert all("prod" in bucket["Name"] for bucket in result)
473
+
474
+ @patch(f"{PATH}.S3ControlMapper")
475
+ @patch(f"{PATH}.Api")
476
+ def test_fetch_fresh_s3_data_initializes_session(self, mock_api, mock_mapper):
477
+ """Test that fetch_fresh_s3_data initializes session if needed."""
478
+ integration = AWSS3EvidenceIntegration(plan_id=123)
479
+
480
+ with patch.object(integration, "_initialize_aws_session") as mock_init:
481
+ with patch(f"{PATH}.S3Collector"):
482
+ integration._fetch_fresh_s3_data()
483
+
484
+ mock_init.assert_called_once()
485
+
486
+ @patch(f"{PATH}.S3ControlMapper")
487
+ @patch(f"{PATH}.Api")
488
+ def test_create_compliance_item(self, mock_api, mock_mapper):
489
+ """Test creating a compliance item from raw data."""
490
+ integration = AWSS3EvidenceIntegration(plan_id=123)
491
+
492
+ raw_data = {
493
+ "Name": "test-bucket",
494
+ "Region": "us-east-1",
495
+ }
496
+
497
+ result = integration.create_compliance_item(raw_data)
498
+
499
+ assert isinstance(result, S3ComplianceItem)
500
+ assert result.bucket_name == "test-bucket"
501
+
502
+ @patch(f"{PATH}.S3ControlMapper")
503
+ @patch(f"{PATH}.Api")
504
+ def test_assess_compliance(self, mock_api, mock_mapper):
505
+ """Test assessing compliance."""
506
+ mock_mapper_instance = MagicMock()
507
+ mock_mapper.return_value = mock_mapper_instance
508
+
509
+ mock_mapper_instance.assess_bucket_compliance.return_value = {
510
+ "SC-13": "PASS",
511
+ "SC-28": "FAIL",
512
+ }
513
+ mock_mapper_instance.assess_all_buckets_compliance.return_value = {
514
+ "SC-13": "PASS",
515
+ "SC-28": "FAIL",
516
+ "AC-3": "FAIL",
517
+ }
518
+
519
+ integration = AWSS3EvidenceIntegration(plan_id=123)
520
+ integration.buckets = [
521
+ S3ComplianceItem({"Name": "bucket-1"}),
522
+ S3ComplianceItem({"Name": "bucket-2"}),
523
+ ]
524
+
525
+ result = integration._assess_compliance()
526
+
527
+ assert "overall" in result
528
+ assert "buckets" in result
529
+ assert len(result["buckets"]) == 2
530
+ assert result["overall"] == {"SC-13": "PASS", "SC-28": "FAIL", "AC-3": "FAIL"}
531
+
532
+ @patch(f"{PATH}.S3ControlMapper")
533
+ @patch(f"{PATH}.Api")
534
+ def test_sync_compliance_data_no_buckets(self, mock_api, mock_mapper, caplog):
535
+ """Test sync_compliance_data with no buckets."""
536
+ integration = AWSS3EvidenceIntegration(plan_id=123)
537
+
538
+ with patch.object(integration, "fetch_compliance_data", return_value=[]):
539
+ integration.sync_compliance_data()
540
+
541
+ assert "No S3 bucket data to sync" in caplog.text
542
+
543
+ @patch(f"{PATH}.S3ControlMapper")
544
+ @patch(f"{PATH}.Api")
545
+ def test_sync_compliance_data_with_buckets(self, mock_api, mock_mapper):
546
+ """Test sync_compliance_data with buckets."""
547
+ mock_mapper_instance = MagicMock()
548
+ mock_mapper.return_value = mock_mapper_instance
549
+ mock_mapper_instance.assess_bucket_compliance.return_value = {"SC-13": "PASS"}
550
+ mock_mapper_instance.assess_all_buckets_compliance.return_value = {"SC-13": "PASS"}
551
+
552
+ integration = AWSS3EvidenceIntegration(plan_id=123, create_evidence=False, create_ssp_attachment=False)
553
+
554
+ bucket_data = [{"Name": "test-bucket"}]
555
+
556
+ with patch.object(integration, "fetch_compliance_data", return_value=bucket_data):
557
+ with patch.object(integration, "_create_evidence_artifacts") as mock_create_evidence:
558
+ integration.sync_compliance_data()
559
+
560
+ assert len(integration.buckets) == 1
561
+ mock_create_evidence.assert_not_called()
562
+
563
+ @patch(f"{PATH}.S3ControlMapper")
564
+ @patch(f"{PATH}.Api")
565
+ def test_sync_compliance_data_with_evidence(self, mock_api, mock_mapper):
566
+ """Test sync_compliance_data with evidence creation."""
567
+ mock_mapper_instance = MagicMock()
568
+ mock_mapper.return_value = mock_mapper_instance
569
+ mock_mapper_instance.assess_bucket_compliance.return_value = {"SC-13": "PASS"}
570
+ mock_mapper_instance.assess_all_buckets_compliance.return_value = {"SC-13": "PASS"}
571
+
572
+ integration = AWSS3EvidenceIntegration(plan_id=123, create_evidence=True)
573
+
574
+ bucket_data = [{"Name": "test-bucket"}]
575
+
576
+ with patch.object(integration, "fetch_compliance_data", return_value=bucket_data):
577
+ with patch.object(integration, "_create_evidence_artifacts") as mock_create_evidence:
578
+ integration.sync_compliance_data()
579
+
580
+ mock_create_evidence.assert_called_once()
581
+
582
+ @patch(f"{PATH}.S3ControlMapper")
583
+ @patch(f"{PATH}.Api")
584
+ def test_create_evidence_file(self, mock_api, mock_mapper):
585
+ """Test creating evidence file."""
586
+ integration = AWSS3EvidenceIntegration(plan_id=123, region="us-east-1", account_id="123456789012")
587
+ integration.buckets = [
588
+ S3ComplianceItem(
589
+ {
590
+ "Name": "test-bucket",
591
+ "Region": "us-east-1",
592
+ "Encryption": {"Enabled": True, "SSEAlgorithm": "AES256"},
593
+ "Versioning": {"Status": "Enabled"},
594
+ "PublicAccessBlock": {
595
+ "BlockPublicAcls": True,
596
+ "IgnorePublicAcls": True,
597
+ "BlockPublicPolicy": True,
598
+ "RestrictPublicBuckets": True,
599
+ },
600
+ "PolicyStatus": {"IsPublic": False},
601
+ "ACL": {"Grants": []},
602
+ "Logging": {"Enabled": True},
603
+ "Tags": [{"Key": "Environment", "Value": "Test"}],
604
+ }
605
+ )
606
+ ]
607
+
608
+ compliance_results = {
609
+ "overall": {"SC-13": "PASS", "SC-28": "FAIL"},
610
+ "buckets": [{"bucket_name": "test-bucket", "controls": {"SC-13": "PASS"}}],
611
+ }
612
+
613
+ evidence_file = integration._create_evidence_file(compliance_results)
614
+
615
+ assert os.path.exists(evidence_file)
616
+ assert evidence_file.endswith(".jsonl.gz")
617
+
618
+ # Verify file contents
619
+ with gzip.open(evidence_file, "rt", encoding="utf-8") as f:
620
+ lines = f.readlines()
621
+
622
+ assert len(lines) == 3 # metadata, summary, bucket record
623
+
624
+ # Parse and verify metadata
625
+ metadata = json.loads(lines[0])
626
+ assert metadata["type"] == "metadata"
627
+ assert metadata["region"] == "us-east-1"
628
+ assert metadata["account_id"] == "123456789012"
629
+ assert metadata["bucket_count"] == 1
630
+
631
+ # Parse and verify summary
632
+ summary = json.loads(lines[1])
633
+ assert summary["type"] == "compliance_summary"
634
+ assert summary["results"]["SC-13"] == "PASS"
635
+
636
+ # Parse and verify bucket record
637
+ bucket_record = json.loads(lines[2])
638
+ assert bucket_record["type"] == "bucket_configuration"
639
+ assert bucket_record["bucket_name"] == "test-bucket"
640
+ assert bucket_record["encryption"]["Enabled"] is True
641
+
642
+ # Cleanup
643
+ os.remove(evidence_file)
644
+
645
+ @patch(f"{PATH}.S3ControlMapper")
646
+ @patch(f"{PATH}.Api")
647
+ def test_create_evidence_file_error(self, mock_api, mock_mapper):
648
+ """Test creating evidence file with error."""
649
+ integration = AWSS3EvidenceIntegration(plan_id=123)
650
+ integration.buckets = []
651
+
652
+ compliance_results = {"overall": {}, "buckets": []}
653
+
654
+ with patch("gzip.open", side_effect=OSError("Permission denied")):
655
+ with pytest.raises(OSError):
656
+ integration._create_evidence_file(compliance_results)
657
+
658
+ @patch(f"{PATH}.File")
659
+ @patch(f"{PATH}.S3ControlMapper")
660
+ @patch(f"{PATH}.Api")
661
+ def test_create_ssp_attachment_with_evidence(self, mock_api, mock_mapper, mock_file):
662
+ """Test creating SSP attachment with evidence."""
663
+ mock_file.upload_file_to_regscale.return_value = True
664
+
665
+ integration = AWSS3EvidenceIntegration(plan_id=123, region="us-east-1")
666
+
667
+ # Create temporary evidence file
668
+ import tempfile
669
+
670
+ with tempfile.NamedTemporaryFile(suffix=".jsonl.gz", delete=False) as tmp_file:
671
+ evidence_file_path = tmp_file.name
672
+ with gzip.open(tmp_file, "wt") as f:
673
+ f.write("{}\n")
674
+
675
+ compliance_results = {"overall": {"SC-13": "PASS"}}
676
+
677
+ with patch.object(integration, "check_for_existing_evidence", return_value=False):
678
+ integration._create_ssp_attachment_with_evidence(evidence_file_path, compliance_results)
679
+
680
+ mock_file.upload_file_to_regscale.assert_called_once()
681
+ call_kwargs = mock_file.upload_file_to_regscale.call_args[1]
682
+ assert call_kwargs["parent_id"] == 123
683
+ assert call_kwargs["parent_module"] == "securityplans"
684
+ assert "s3_evidence" in call_kwargs["file_name"]
685
+ assert "aws,s3,storage,compliance,automated" == call_kwargs["tags"]
686
+
687
+ # Cleanup
688
+ os.remove(evidence_file_path)
689
+
690
+ @patch(f"{PATH}.File")
691
+ @patch(f"{PATH}.S3ControlMapper")
692
+ @patch(f"{PATH}.Api")
693
+ def test_create_ssp_attachment_duplicate_check(self, mock_api, mock_mapper, mock_file, caplog):
694
+ """Test creating SSP attachment with duplicate check."""
695
+ integration = AWSS3EvidenceIntegration(plan_id=123, region="us-west-2")
696
+
697
+ evidence_file_path = "/tmp/test_evidence.jsonl.gz"
698
+ compliance_results = {"overall": {"SC-13": "PASS"}}
699
+
700
+ with patch.object(integration, "check_for_existing_evidence", return_value=True):
701
+ integration._create_ssp_attachment_with_evidence(evidence_file_path, compliance_results)
702
+
703
+ mock_file.upload_file_to_regscale.assert_not_called()
704
+ assert "already exists for today" in caplog.text
705
+
706
+ @patch(f"{PATH}.File")
707
+ @patch(f"{PATH}.S3ControlMapper")
708
+ @patch(f"{PATH}.Api")
709
+ def test_create_ssp_attachment_upload_failure(self, mock_api, mock_mapper, mock_file, caplog):
710
+ """Test creating SSP attachment with upload failure."""
711
+ mock_file.upload_file_to_regscale.return_value = False
712
+
713
+ integration = AWSS3EvidenceIntegration(plan_id=123, region="us-east-1")
714
+
715
+ # Create temporary evidence file
716
+ import tempfile
717
+
718
+ with tempfile.NamedTemporaryFile(suffix=".jsonl.gz", delete=False) as tmp_file:
719
+ evidence_file_path = tmp_file.name
720
+ with gzip.open(tmp_file, "wt") as f:
721
+ f.write("{}\n")
722
+
723
+ compliance_results = {"overall": {"SC-13": "PASS"}}
724
+
725
+ with patch.object(integration, "check_for_existing_evidence", return_value=False):
726
+ integration._create_ssp_attachment_with_evidence(evidence_file_path, compliance_results)
727
+
728
+ assert "Failed to upload S3 evidence file" in caplog.text
729
+
730
+ # Cleanup
731
+ os.remove(evidence_file_path)
732
+
733
+ @patch(f"{PATH}.File")
734
+ @patch(f"{PATH}.S3ControlMapper")
735
+ @patch(f"{PATH}.Api")
736
+ def test_create_ssp_attachment_error_handling(self, mock_api, mock_mapper, mock_file, caplog):
737
+ """Test creating SSP attachment with error."""
738
+ integration = AWSS3EvidenceIntegration(plan_id=123)
739
+
740
+ evidence_file_path = "/tmp/nonexistent.jsonl.gz"
741
+ compliance_results = {"overall": {}}
742
+
743
+ with patch.object(integration, "check_for_existing_evidence", return_value=False):
744
+ integration._create_ssp_attachment_with_evidence(evidence_file_path, compliance_results)
745
+
746
+ assert "Failed to create SSP attachment" in caplog.text
747
+
748
+ @patch(f"{PATH}.S3ControlMapper")
749
+ @patch(f"{PATH}.Api")
750
+ def test_create_evidence_artifacts(self, mock_api, mock_mapper):
751
+ """Test creating evidence artifacts."""
752
+ integration = AWSS3EvidenceIntegration(plan_id=123, create_ssp_attachment=True)
753
+ integration.buckets = []
754
+
755
+ compliance_results = {"overall": {}, "buckets": []}
756
+
757
+ with patch.object(integration, "_create_evidence_file", return_value="/tmp/test.jsonl.gz") as mock_create:
758
+ with patch.object(integration, "_create_ssp_attachment_with_evidence") as mock_upload:
759
+ with patch("os.path.exists", return_value=True):
760
+ with patch("os.remove") as mock_remove:
761
+ integration._create_evidence_artifacts(compliance_results)
762
+
763
+ mock_create.assert_called_once_with(compliance_results)
764
+ mock_upload.assert_called_once()
765
+ mock_remove.assert_called_once_with("/tmp/test.jsonl.gz")
766
+
767
+ @patch(f"{PATH}.S3ControlMapper")
768
+ @patch(f"{PATH}.Api")
769
+ def test_create_evidence_artifacts_no_ssp_attachment(self, mock_api, mock_mapper):
770
+ """Test creating evidence artifacts without SSP attachment."""
771
+ integration = AWSS3EvidenceIntegration(plan_id=123, create_ssp_attachment=False)
772
+ integration.buckets = []
773
+
774
+ compliance_results = {"overall": {}, "buckets": []}
775
+
776
+ with patch.object(integration, "_create_evidence_file", return_value="/tmp/test.jsonl.gz"):
777
+ with patch.object(integration, "_create_ssp_attachment_with_evidence") as mock_upload:
778
+ with patch("os.path.exists", return_value=True):
779
+ with patch("os.remove"):
780
+ integration._create_evidence_artifacts(compliance_results)
781
+
782
+ mock_upload.assert_not_called()
783
+
784
+ @patch(f"{PATH}.S3ControlMapper")
785
+ @patch(f"{PATH}.Api")
786
+ def test_create_evidence_artifacts_cleanup(self, mock_api, mock_mapper):
787
+ """Test evidence artifacts cleanup."""
788
+ integration = AWSS3EvidenceIntegration(plan_id=123, create_ssp_attachment=True)
789
+ integration.buckets = []
790
+
791
+ compliance_results = {"overall": {}, "buckets": []}
792
+
793
+ # Create actual temp file to test cleanup
794
+ import tempfile
795
+
796
+ with tempfile.NamedTemporaryFile(suffix=".jsonl.gz", delete=False) as tmp_file:
797
+ evidence_file_path = tmp_file.name
798
+
799
+ with patch.object(integration, "_create_evidence_file", return_value=evidence_file_path):
800
+ with patch.object(integration, "_create_ssp_attachment_with_evidence"):
801
+ integration._create_evidence_artifacts(compliance_results)
802
+
803
+ # Verify file was cleaned up
804
+ assert not os.path.exists(evidence_file_path)
805
+
806
+ @patch(f"{PATH}.S3ControlMapper")
807
+ @patch(f"{PATH}.Api")
808
+ def test_create_evidence_records(self, mock_api, mock_mapper):
809
+ """Test creating evidence records."""
810
+ mock_api_instance = MagicMock()
811
+ mock_api.return_value = mock_api_instance
812
+ mock_api_instance.create_evidence.return_value = {"id": 999}
813
+
814
+ integration = AWSS3EvidenceIntegration(plan_id=123, evidence_control_ids=["SC-13", "SC-28"])
815
+ integration.buckets = []
816
+
817
+ evidence_file_path = "/tmp/test.jsonl.gz"
818
+ compliance_results = {"overall": {"SC-13": "PASS"}}
819
+
820
+ with patch.object(integration, "_build_evidence_description", return_value="Test description"):
821
+ with patch.object(integration, "_upload_evidence_file") as mock_upload:
822
+ with patch.object(integration, "_link_evidence_to_controls") as mock_link:
823
+ integration._create_evidence_records(evidence_file_path, compliance_results)
824
+
825
+ mock_api_instance.create_evidence.assert_called_once()
826
+ mock_upload.assert_called_once_with(999, evidence_file_path)
827
+ mock_link.assert_called_once_with(999, is_attachment=False)
828
+
829
+ @patch(f"{PATH}.S3ControlMapper")
830
+ @patch(f"{PATH}.Api")
831
+ def test_upload_evidence_file(self, mock_api, mock_mapper):
832
+ """Test uploading evidence file."""
833
+ mock_api_instance = MagicMock()
834
+ mock_api.return_value = mock_api_instance
835
+
836
+ integration = AWSS3EvidenceIntegration(plan_id=123)
837
+
838
+ evidence_id = 999
839
+ file_path = "/tmp/test.jsonl.gz"
840
+
841
+ integration._upload_evidence_file(evidence_id, file_path)
842
+
843
+ mock_api_instance.upload_evidence_file.assert_called_once_with(evidence_id, file_path)
844
+
845
+ @patch(f"{PATH}.S3ControlMapper")
846
+ @patch(f"{PATH}.Api")
847
+ def test_upload_evidence_file_error(self, mock_api, mock_mapper, caplog):
848
+ """Test uploading evidence file with error."""
849
+ mock_api_instance = MagicMock()
850
+ mock_api.return_value = mock_api_instance
851
+ mock_api_instance.upload_evidence_file.side_effect = Exception("Upload failed")
852
+
853
+ integration = AWSS3EvidenceIntegration(plan_id=123)
854
+
855
+ integration._upload_evidence_file(999, "/tmp/test.jsonl.gz")
856
+
857
+ assert "Failed to upload evidence file" in caplog.text
858
+
859
+ @patch(f"{PATH}.S3ControlMapper")
860
+ @patch(f"{PATH}.Api")
861
+ def test_link_evidence_to_controls(self, mock_api, mock_mapper):
862
+ """Test linking evidence to controls."""
863
+ mock_api_instance = MagicMock()
864
+ mock_api.return_value = mock_api_instance
865
+
866
+ integration = AWSS3EvidenceIntegration(plan_id=123, evidence_control_ids=["SC-13", "SC-28"])
867
+
868
+ integration._link_evidence_to_controls(999, is_attachment=False)
869
+
870
+ assert mock_api_instance.link_evidence_to_control.call_count == 2
871
+
872
+ @patch(f"{PATH}.S3ControlMapper")
873
+ @patch(f"{PATH}.Api")
874
+ def test_link_evidence_to_controls_as_attachment(self, mock_api, mock_mapper):
875
+ """Test linking evidence to controls as attachment."""
876
+ mock_api_instance = MagicMock()
877
+ mock_api.return_value = mock_api_instance
878
+
879
+ integration = AWSS3EvidenceIntegration(plan_id=123, evidence_control_ids=["SC-13", "SC-28"])
880
+
881
+ integration._link_evidence_to_controls(999, is_attachment=True)
882
+
883
+ assert mock_api_instance.link_ssp_attachment_to_control.call_count == 2
884
+
885
+ @patch(f"{PATH}.S3ControlMapper")
886
+ @patch(f"{PATH}.Api")
887
+ def test_link_evidence_to_controls_error(self, mock_api, mock_mapper, caplog):
888
+ """Test linking evidence to controls with error."""
889
+ mock_api_instance = MagicMock()
890
+ mock_api.return_value = mock_api_instance
891
+ mock_api_instance.link_evidence_to_control.side_effect = Exception("Link failed")
892
+
893
+ integration = AWSS3EvidenceIntegration(plan_id=123, evidence_control_ids=["SC-13"])
894
+
895
+ integration._link_evidence_to_controls(999, is_attachment=False)
896
+
897
+ assert "Failed to link evidence to controls" in caplog.text
898
+
899
+ @patch(f"{PATH}.S3ControlMapper")
900
+ @patch(f"{PATH}.Api")
901
+ def test_build_evidence_description(self, mock_api, mock_mapper):
902
+ """Test building HTML-formatted evidence description."""
903
+ mock_mapper_instance = MagicMock()
904
+ mock_mapper.return_value = mock_mapper_instance
905
+ mock_mapper_instance.get_control_description.side_effect = lambda x: f"{x} description"
906
+
907
+ integration = AWSS3EvidenceIntegration(plan_id=123, region="us-east-1", account_id="123456789012")
908
+ integration.buckets = [
909
+ S3ComplianceItem(
910
+ {
911
+ "Name": "bucket-1",
912
+ "Encryption": {"Enabled": True},
913
+ "Versioning": {"Status": "Enabled"},
914
+ }
915
+ ),
916
+ S3ComplianceItem(
917
+ {
918
+ "Name": "bucket-2",
919
+ "Encryption": {"Enabled": False},
920
+ "Versioning": {"Status": "Disabled"},
921
+ }
922
+ ),
923
+ ]
924
+
925
+ compliance_results = {
926
+ "overall": {"SC-13": "PASS", "SC-28": "FAIL"},
927
+ }
928
+
929
+ description = integration._build_evidence_description(compliance_results)
930
+
931
+ assert "AWS S3 Storage Configuration Evidence" in description
932
+ assert "us-east-1" in description
933
+ assert "123456789012" in description
934
+ assert "Total Buckets:</strong> 2" in description
935
+ assert "Controls Passed:</strong> 1" in description
936
+ assert "SC-13" in description
937
+ assert "Controls Failed:</strong> 1" in description
938
+ assert "SC-28" in description
939
+ assert "bucket-1" in description
940
+ assert "Encryption=Enabled" in description
941
+ assert "Versioning=Enabled" in description
942
+
943
+ @patch(f"{PATH}.S3ControlMapper")
944
+ @patch(f"{PATH}.Api")
945
+ def test_build_evidence_description_many_buckets(self, mock_api, mock_mapper):
946
+ """Test building evidence description with many buckets (limit to 10)."""
947
+ mock_mapper_instance = MagicMock()
948
+ mock_mapper.return_value = mock_mapper_instance
949
+ mock_mapper_instance.get_control_description.return_value = "Test description"
950
+
951
+ integration = AWSS3EvidenceIntegration(plan_id=123, region="us-east-1")
952
+ integration.buckets = [
953
+ S3ComplianceItem({"Name": f"bucket-{i}", "Encryption": {}, "Versioning": {}}) for i in range(15)
954
+ ]
955
+
956
+ compliance_results = {"overall": {"SC-13": "PASS"}}
957
+
958
+ description = integration._build_evidence_description(compliance_results)
959
+
960
+ assert "bucket-0" in description
961
+ assert "bucket-9" in description
962
+ assert "... and 5 more buckets" in description
963
+
964
+ @patch(f"{PATH}.S3ControlMapper")
965
+ @patch(f"{PATH}.Api")
966
+ def test_build_evidence_description_with_filtering(self, mock_api, mock_mapper):
967
+ """Test building evidence description with control filtering."""
968
+ mock_mapper_instance = MagicMock()
969
+ mock_mapper.return_value = mock_mapper_instance
970
+ mock_mapper_instance.get_control_description.side_effect = lambda x: f"{x} description"
971
+
972
+ integration = AWSS3EvidenceIntegration(plan_id=123, region="us-east-1", evidence_control_ids=["SC-13"])
973
+ integration.buckets = []
974
+
975
+ compliance_results = {
976
+ "overall": {"SC-13": "PASS", "SC-28": "PASS"},
977
+ }
978
+
979
+ description = integration._build_evidence_description(compliance_results)
980
+
981
+ # Should include all controls in the description, filtering is for linking only
982
+ assert "SC-13" in description
983
+ assert "SC-28" in description
984
+
985
+
986
+ if __name__ == "__main__":
987
+ pytest.main([__file__, "-v"])