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,918 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for AWS Systems Manager resource collector."""
4
+
5
+ import logging
6
+ from datetime import datetime
7
+ from unittest.mock import MagicMock, patch
8
+
9
+ import pytest
10
+ from botocore.exceptions import ClientError
11
+
12
+ from regscale.integrations.commercial.aws.inventory.resources.systems_manager import SystemsManagerCollector
13
+
14
+ logger = logging.getLogger("regscale")
15
+
16
+ PATH = "regscale.integrations.commercial.aws.inventory.resources.systems_manager"
17
+
18
+
19
+ class TestSystemsManagerCollector:
20
+ """Test suite for AWS Systems Manager resource collector."""
21
+
22
+ @pytest.fixture
23
+ def mock_session(self):
24
+ """Create a mock boto3 session."""
25
+ session = MagicMock()
26
+ return session
27
+
28
+ @pytest.fixture
29
+ def mock_ssm_client(self):
30
+ """Create a mock SSM client."""
31
+ client = MagicMock()
32
+ return client
33
+
34
+ @pytest.fixture
35
+ def collector(self, mock_session):
36
+ """Create a SystemsManagerCollector instance."""
37
+ return SystemsManagerCollector(session=mock_session, region="us-east-1", account_id="123456789012")
38
+
39
+ @pytest.fixture
40
+ def collector_no_account(self, mock_session):
41
+ """Create a SystemsManagerCollector instance without account_id."""
42
+ return SystemsManagerCollector(session=mock_session, region="us-east-1", account_id=None)
43
+
44
+ def test_init_with_account_id(self, mock_session):
45
+ """Test initialization with account ID."""
46
+ collector = SystemsManagerCollector(session=mock_session, region="us-west-2", account_id="123456789012")
47
+
48
+ assert collector.session == mock_session
49
+ assert collector.region == "us-west-2"
50
+ assert collector.account_id == "123456789012"
51
+
52
+ def test_init_without_account_id(self, mock_session):
53
+ """Test initialization without account ID."""
54
+ collector = SystemsManagerCollector(session=mock_session, region="us-east-1")
55
+
56
+ assert collector.session == mock_session
57
+ assert collector.region == "us-east-1"
58
+ assert collector.account_id is None
59
+
60
+ @patch(f"{PATH}.SystemsManagerCollector._get_compliance_summary")
61
+ @patch(f"{PATH}.SystemsManagerCollector._list_associations")
62
+ @patch(f"{PATH}.SystemsManagerCollector._list_maintenance_windows")
63
+ @patch(f"{PATH}.SystemsManagerCollector._list_patch_baselines")
64
+ @patch(f"{PATH}.SystemsManagerCollector._list_documents")
65
+ @patch(f"{PATH}.SystemsManagerCollector._list_parameters")
66
+ @patch(f"{PATH}.SystemsManagerCollector._list_managed_instances")
67
+ def test_collect_success(
68
+ self,
69
+ mock_list_instances,
70
+ mock_list_params,
71
+ mock_list_docs,
72
+ mock_list_baselines,
73
+ mock_list_windows,
74
+ mock_list_assocs,
75
+ mock_get_compliance,
76
+ collector,
77
+ mock_ssm_client,
78
+ ):
79
+ """Test successful collection of all Systems Manager resources."""
80
+ collector.session.client.return_value = mock_ssm_client
81
+
82
+ mock_list_instances.return_value = [
83
+ {"InstanceId": "i-12345", "PingStatus": "Online"},
84
+ {"InstanceId": "i-67890", "PingStatus": "Online"},
85
+ ]
86
+ mock_list_params.return_value = [{"Name": "/test/param1"}]
87
+ mock_list_docs.return_value = [{"Name": "TestDocument"}]
88
+ mock_list_baselines.return_value = [{"BaselineId": "pb-12345"}]
89
+ mock_list_windows.return_value = [{"WindowId": "mw-12345"}]
90
+ mock_list_assocs.return_value = [{"AssociationId": "assoc-12345"}]
91
+ mock_get_compliance.return_value = {"TotalCompliant": 10, "TotalNonCompliant": 2}
92
+
93
+ result = collector.collect()
94
+
95
+ assert result["ManagedInstances"] == mock_list_instances.return_value
96
+ assert result["Parameters"] == mock_list_params.return_value
97
+ assert result["Documents"] == mock_list_docs.return_value
98
+ assert result["PatchBaselines"] == mock_list_baselines.return_value
99
+ assert result["MaintenanceWindows"] == mock_list_windows.return_value
100
+ assert result["Associations"] == mock_list_assocs.return_value
101
+ assert result["ComplianceSummary"] == mock_get_compliance.return_value
102
+ assert len(result["ManagedInstances"]) == 2
103
+
104
+ collector.session.client.assert_called_once_with("ssm", region_name="us-east-1")
105
+ mock_list_instances.assert_called_once_with(mock_ssm_client)
106
+ mock_list_params.assert_called_once_with(mock_ssm_client)
107
+ mock_list_docs.assert_called_once_with(mock_ssm_client)
108
+ mock_list_baselines.assert_called_once_with(mock_ssm_client)
109
+ mock_list_windows.assert_called_once_with(mock_ssm_client)
110
+ mock_list_assocs.assert_called_once_with(mock_ssm_client)
111
+ mock_get_compliance.assert_called_once_with(mock_ssm_client)
112
+
113
+ @patch(f"{PATH}.SystemsManagerCollector._handle_error")
114
+ def test_collect_client_error(self, mock_handle_error, collector, mock_ssm_client):
115
+ """Test collect handles ClientError from _get_client."""
116
+ error = ClientError({"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "CreateClient")
117
+ collector.session.client.side_effect = error
118
+
119
+ result = collector.collect()
120
+
121
+ assert result["ManagedInstances"] == []
122
+ assert result["Parameters"] == []
123
+ assert result["Documents"] == []
124
+ mock_handle_error.assert_called_once_with(error, "Systems Manager resources")
125
+
126
+ @patch(f"{PATH}.logger")
127
+ def test_collect_unexpected_error(self, mock_logger, collector, mock_ssm_client):
128
+ """Test collect handles unexpected errors."""
129
+ collector.session.client.return_value = mock_ssm_client
130
+ error = ValueError("Unexpected error")
131
+
132
+ mock_ssm_client.get_paginator.side_effect = error
133
+
134
+ result = collector.collect()
135
+
136
+ assert result["ManagedInstances"] == []
137
+ assert result["Parameters"] == []
138
+ mock_logger.error.assert_called()
139
+ assert "Unexpected error collecting Systems Manager resources" in str(mock_logger.error.call_args)
140
+
141
+ def test_list_managed_instances_success(self, collector, mock_ssm_client):
142
+ """Test successful listing of managed instances with pagination."""
143
+ mock_paginator = MagicMock()
144
+ mock_ssm_client.get_paginator.return_value = mock_paginator
145
+
146
+ test_datetime = datetime(2024, 1, 1, 12, 0, 0)
147
+ mock_paginator.paginate.return_value = [
148
+ {
149
+ "InstanceInformationList": [
150
+ {
151
+ "InstanceId": "i-12345",
152
+ "PingStatus": "Online",
153
+ "LastPingDateTime": test_datetime,
154
+ "AgentVersion": "3.1.0",
155
+ "IsLatestVersion": True,
156
+ "PlatformType": "Linux",
157
+ "PlatformName": "Amazon Linux",
158
+ "PlatformVersion": "2",
159
+ "ResourceType": "EC2Instance",
160
+ "IPAddress": "10.0.1.5",
161
+ "ComputerName": "test-instance",
162
+ "AssociationStatus": "Success",
163
+ "LastAssociationExecutionDate": test_datetime,
164
+ "LastSuccessfulAssociationExecutionDate": test_datetime,
165
+ }
166
+ ]
167
+ },
168
+ {
169
+ "InstanceInformationList": [
170
+ {
171
+ "InstanceId": "i-67890",
172
+ "PingStatus": "ConnectionLost",
173
+ "LastPingDateTime": test_datetime,
174
+ "AgentVersion": "3.0.0",
175
+ "PlatformType": "Windows",
176
+ "PlatformName": "Windows Server",
177
+ "PlatformVersion": "2019",
178
+ "ResourceType": "ManagedInstance",
179
+ "IPAddress": "10.0.1.6",
180
+ "ComputerName": "windows-server",
181
+ }
182
+ ]
183
+ },
184
+ ]
185
+
186
+ with patch.object(collector, "_get_instance_patches") as mock_get_patches:
187
+ mock_get_patches.side_effect = [
188
+ {"TotalPatches": 10, "Installed": 8, "Missing": 2},
189
+ {"TotalPatches": 15, "Installed": 15, "Missing": 0},
190
+ ]
191
+
192
+ result = collector._list_managed_instances(mock_ssm_client)
193
+
194
+ assert len(result) == 2
195
+ assert result[0]["InstanceId"] == "i-12345"
196
+ assert result[0]["Region"] == "us-east-1"
197
+ assert result[0]["PingStatus"] == "Online"
198
+ assert result[0]["AgentVersion"] == "3.1.0"
199
+ assert result[0]["IsLatestVersion"] is True
200
+ assert result[0]["PatchSummary"]["TotalPatches"] == 10
201
+ assert result[1]["InstanceId"] == "i-67890"
202
+ assert result[1]["PatchSummary"]["TotalPatches"] == 15
203
+
204
+ mock_ssm_client.get_paginator.assert_called_once_with("describe_instance_information")
205
+ assert mock_get_patches.call_count == 2
206
+
207
+ @patch(f"{PATH}.logger")
208
+ def test_list_managed_instances_access_denied(self, mock_logger, collector, mock_ssm_client):
209
+ """Test listing managed instances with access denied error."""
210
+ mock_paginator = MagicMock()
211
+ mock_ssm_client.get_paginator.return_value = mock_paginator
212
+ error = ClientError(
213
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "DescribeInstanceInformation"
214
+ )
215
+
216
+ mock_paginator.paginate.side_effect = error
217
+
218
+ result = collector._list_managed_instances(mock_ssm_client)
219
+
220
+ assert result == []
221
+ mock_logger.warning.assert_called_once()
222
+ assert "Access denied to list managed instances in us-east-1" in str(mock_logger.warning.call_args)
223
+
224
+ @patch(f"{PATH}.logger")
225
+ def test_list_managed_instances_other_error(self, mock_logger, collector, mock_ssm_client):
226
+ """Test listing managed instances with other ClientError."""
227
+ mock_paginator = MagicMock()
228
+ mock_ssm_client.get_paginator.return_value = mock_paginator
229
+ error = ClientError(
230
+ {"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "DescribeInstanceInformation"
231
+ )
232
+
233
+ mock_paginator.paginate.side_effect = error
234
+
235
+ result = collector._list_managed_instances(mock_ssm_client)
236
+
237
+ assert result == []
238
+ mock_logger.error.assert_called_once()
239
+ assert "Error listing managed instances" in str(mock_logger.error.call_args)
240
+
241
+ def test_get_instance_patches_success(self, collector, mock_ssm_client):
242
+ """Test successful retrieval of instance patches."""
243
+ mock_ssm_client.describe_instance_patches.return_value = {
244
+ "Patches": [
245
+ {"State": "Installed"},
246
+ {"State": "Installed"},
247
+ {"State": "InstalledOther"},
248
+ {"State": "Missing"},
249
+ {"State": "Missing"},
250
+ {"State": "Failed"},
251
+ {"State": "NotApplicable"},
252
+ ]
253
+ }
254
+
255
+ result = collector._get_instance_patches(mock_ssm_client, "i-12345")
256
+
257
+ assert result["TotalPatches"] == 7
258
+ assert result["Installed"] == 2
259
+ assert result["InstalledOther"] == 1
260
+ assert result["Missing"] == 2
261
+ assert result["Failed"] == 1
262
+ assert result["NotApplicable"] == 1
263
+
264
+ mock_ssm_client.describe_instance_patches.assert_called_once_with(InstanceId="i-12345", MaxResults=50)
265
+
266
+ def test_get_instance_patches_empty(self, collector, mock_ssm_client):
267
+ """Test instance patches with no patches returned."""
268
+ mock_ssm_client.describe_instance_patches.return_value = {"Patches": []}
269
+
270
+ result = collector._get_instance_patches(mock_ssm_client, "i-12345")
271
+
272
+ assert result["TotalPatches"] == 0
273
+ assert result["Installed"] == 0
274
+ assert result["Missing"] == 0
275
+
276
+ @patch(f"{PATH}.logger")
277
+ def test_get_instance_patches_access_denied(self, mock_logger, collector, mock_ssm_client):
278
+ """Test instance patches with access denied error."""
279
+ error = ClientError(
280
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "DescribeInstancePatches"
281
+ )
282
+
283
+ mock_ssm_client.describe_instance_patches.side_effect = error
284
+
285
+ result = collector._get_instance_patches(mock_ssm_client, "i-12345")
286
+
287
+ assert result == {}
288
+ mock_logger.debug.assert_not_called()
289
+
290
+ @patch(f"{PATH}.logger")
291
+ def test_get_instance_patches_invalid_instance(self, mock_logger, collector, mock_ssm_client):
292
+ """Test instance patches with invalid instance ID error."""
293
+ error = ClientError(
294
+ {"Error": {"Code": "InvalidInstanceId", "Message": "Invalid instance"}}, "DescribeInstancePatches"
295
+ )
296
+
297
+ mock_ssm_client.describe_instance_patches.side_effect = error
298
+
299
+ result = collector._get_instance_patches(mock_ssm_client, "i-invalid")
300
+
301
+ assert result == {}
302
+ mock_logger.debug.assert_not_called()
303
+
304
+ @patch(f"{PATH}.logger")
305
+ def test_get_instance_patches_other_error(self, mock_logger, collector, mock_ssm_client):
306
+ """Test instance patches with other ClientError."""
307
+ error = ClientError(
308
+ {"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "DescribeInstancePatches"
309
+ )
310
+
311
+ mock_ssm_client.describe_instance_patches.side_effect = error
312
+
313
+ result = collector._get_instance_patches(mock_ssm_client, "i-12345")
314
+
315
+ assert result == {}
316
+ mock_logger.debug.assert_called_once()
317
+ assert "Error getting patches for instance i-12345" in str(mock_logger.debug.call_args)
318
+
319
+ def test_list_parameters_success(self, collector, mock_ssm_client):
320
+ """Test successful listing of parameters with pagination."""
321
+ mock_paginator = MagicMock()
322
+ mock_ssm_client.get_paginator.return_value = mock_paginator
323
+
324
+ test_datetime = datetime(2024, 1, 1, 12, 0, 0)
325
+ mock_paginator.paginate.return_value = [
326
+ {
327
+ "Parameters": [
328
+ {
329
+ "Name": "/test/param1",
330
+ "Type": "String",
331
+ "KeyId": "key-12345",
332
+ "LastModifiedDate": test_datetime,
333
+ "Description": "Test parameter 1",
334
+ "Version": 1,
335
+ "Tier": "Standard",
336
+ "Policies": [],
337
+ "DataType": "text",
338
+ },
339
+ {
340
+ "Name": "/test/param2",
341
+ "Type": "SecureString",
342
+ "LastModifiedDate": test_datetime,
343
+ "Version": 2,
344
+ "Tier": "Advanced",
345
+ },
346
+ ]
347
+ }
348
+ ]
349
+
350
+ result = collector._list_parameters(mock_ssm_client)
351
+
352
+ assert len(result) == 2
353
+ assert result[0]["Name"] == "/test/param1"
354
+ assert result[0]["Type"] == "String"
355
+ assert result[0]["Region"] == "us-east-1"
356
+ assert result[0]["KeyId"] == "key-12345"
357
+ assert result[1]["Name"] == "/test/param2"
358
+ assert result[1]["Type"] == "SecureString"
359
+
360
+ mock_ssm_client.get_paginator.assert_called_once_with("describe_parameters")
361
+
362
+ @patch(f"{PATH}.logger")
363
+ def test_list_parameters_access_denied(self, mock_logger, collector, mock_ssm_client):
364
+ """Test listing parameters with access denied error."""
365
+ mock_paginator = MagicMock()
366
+ mock_ssm_client.get_paginator.return_value = mock_paginator
367
+ error = ClientError(
368
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "DescribeParameters"
369
+ )
370
+
371
+ mock_paginator.paginate.side_effect = error
372
+
373
+ result = collector._list_parameters(mock_ssm_client)
374
+
375
+ assert result == []
376
+ mock_logger.warning.assert_called_once()
377
+ assert "Access denied to list parameters in us-east-1" in str(mock_logger.warning.call_args)
378
+
379
+ @patch(f"{PATH}.logger")
380
+ def test_list_parameters_other_error(self, mock_logger, collector, mock_ssm_client):
381
+ """Test listing parameters with other ClientError."""
382
+ mock_paginator = MagicMock()
383
+ mock_ssm_client.get_paginator.return_value = mock_paginator
384
+ error = ClientError({"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "DescribeParameters")
385
+
386
+ mock_paginator.paginate.side_effect = error
387
+
388
+ result = collector._list_parameters(mock_ssm_client)
389
+
390
+ assert result == []
391
+ mock_logger.error.assert_called_once()
392
+ assert "Error listing parameters" in str(mock_logger.error.call_args)
393
+
394
+ def test_list_documents_with_account_filter(self, collector, mock_ssm_client):
395
+ """Test listing documents with account ID filtering."""
396
+ mock_paginator = MagicMock()
397
+ mock_ssm_client.get_paginator.return_value = mock_paginator
398
+
399
+ mock_paginator.paginate.return_value = [
400
+ {
401
+ "DocumentIdentifiers": [
402
+ {
403
+ "Name": "MyDocument",
404
+ "Owner": "123456789012",
405
+ "VersionName": "v1",
406
+ "PlatformTypes": ["Linux"],
407
+ "DocumentVersion": "1",
408
+ "DocumentType": "Command",
409
+ "SchemaVersion": "2.2",
410
+ "DocumentFormat": "JSON",
411
+ "TargetType": "/AWS::EC2::Instance",
412
+ "Tags": [{"Key": "Environment", "Value": "Test"}],
413
+ },
414
+ {
415
+ "Name": "AmazonDocument",
416
+ "Owner": "Amazon",
417
+ "PlatformTypes": ["Windows", "Linux"],
418
+ "DocumentType": "Automation",
419
+ "SchemaVersion": "0.3",
420
+ "DocumentFormat": "YAML",
421
+ },
422
+ {
423
+ "Name": "OtherAccountDoc",
424
+ "Owner": "999999999999",
425
+ "PlatformTypes": ["Linux"],
426
+ "DocumentType": "Command",
427
+ },
428
+ ]
429
+ }
430
+ ]
431
+
432
+ result = collector._list_documents(mock_ssm_client)
433
+
434
+ assert len(result) == 2
435
+ assert result[0]["Name"] == "MyDocument"
436
+ assert result[0]["Owner"] == "123456789012"
437
+ assert result[0]["Region"] == "us-east-1"
438
+ assert result[1]["Name"] == "AmazonDocument"
439
+ assert result[1]["Owner"] == "Amazon"
440
+
441
+ mock_paginator.paginate.assert_called_once_with(Filters=[{"Key": "Owner", "Values": ["123456789012"]}])
442
+
443
+ def test_list_documents_without_account_filter(self, collector_no_account, mock_ssm_client):
444
+ """Test listing documents without account ID filtering."""
445
+ mock_paginator = MagicMock()
446
+ mock_ssm_client.get_paginator.return_value = mock_paginator
447
+
448
+ mock_paginator.paginate.return_value = [
449
+ {
450
+ "DocumentIdentifiers": [
451
+ {
452
+ "Name": "Document1",
453
+ "Owner": "123456789012",
454
+ "PlatformTypes": ["Linux"],
455
+ "DocumentType": "Command",
456
+ "SchemaVersion": "2.2",
457
+ "DocumentFormat": "JSON",
458
+ }
459
+ ]
460
+ }
461
+ ]
462
+
463
+ result = collector_no_account._list_documents(mock_ssm_client)
464
+
465
+ assert len(result) == 1
466
+ assert result[0]["Name"] == "Document1"
467
+
468
+ mock_paginator.paginate.assert_called_once_with(Filters=[])
469
+
470
+ @patch(f"{PATH}.logger")
471
+ def test_list_documents_access_denied(self, mock_logger, collector, mock_ssm_client):
472
+ """Test listing documents with access denied error."""
473
+ mock_paginator = MagicMock()
474
+ mock_ssm_client.get_paginator.return_value = mock_paginator
475
+ error = ClientError({"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "ListDocuments")
476
+
477
+ mock_paginator.paginate.side_effect = error
478
+
479
+ result = collector._list_documents(mock_ssm_client)
480
+
481
+ assert result == []
482
+ mock_logger.warning.assert_called_once()
483
+ assert "Access denied to list documents in us-east-1" in str(mock_logger.warning.call_args)
484
+
485
+ @patch(f"{PATH}.logger")
486
+ def test_list_documents_other_error(self, mock_logger, collector, mock_ssm_client):
487
+ """Test listing documents with other ClientError."""
488
+ mock_paginator = MagicMock()
489
+ mock_ssm_client.get_paginator.return_value = mock_paginator
490
+ error = ClientError({"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "ListDocuments")
491
+
492
+ mock_paginator.paginate.side_effect = error
493
+
494
+ result = collector._list_documents(mock_ssm_client)
495
+
496
+ assert result == []
497
+ mock_logger.error.assert_called_once()
498
+ assert "Error listing documents" in str(mock_logger.error.call_args)
499
+
500
+ def test_list_patch_baselines_with_account_filter(self, collector, mock_ssm_client):
501
+ """Test listing patch baselines with account ID filtering."""
502
+ mock_paginator = MagicMock()
503
+ mock_ssm_client.get_paginator.return_value = mock_paginator
504
+
505
+ test_datetime = datetime(2024, 1, 1, 12, 0, 0)
506
+ mock_paginator.paginate.return_value = [
507
+ {
508
+ "BaselineIdentities": [
509
+ {
510
+ "BaselineId": "pb-12345",
511
+ "BaselineName": "MyBaseline",
512
+ "OperatingSystem": "AMAZON_LINUX_2",
513
+ "DefaultBaseline": True,
514
+ },
515
+ {
516
+ "BaselineId": "pb-67890",
517
+ "BaselineName": "WindowsBaseline",
518
+ "OperatingSystem": "WINDOWS",
519
+ "DefaultBaseline": False,
520
+ },
521
+ ]
522
+ }
523
+ ]
524
+
525
+ mock_ssm_client.get_patch_baseline.side_effect = [
526
+ {
527
+ "BaselineDescription": "Test baseline for Linux",
528
+ "ApprovalRules": {"PatchRules": [{"PatchFilterGroup": {}}]},
529
+ "ApprovedPatches": ["KB123456"],
530
+ "RejectedPatches": ["KB999999"],
531
+ "CreatedDate": test_datetime,
532
+ "ModifiedDate": test_datetime,
533
+ },
534
+ {
535
+ "BaselineDescription": "Test baseline for Windows",
536
+ "ApprovalRules": {},
537
+ "ApprovedPatches": [],
538
+ "RejectedPatches": [],
539
+ "CreatedDate": test_datetime,
540
+ },
541
+ ]
542
+
543
+ result = collector._list_patch_baselines(mock_ssm_client)
544
+
545
+ assert len(result) == 2
546
+ assert result[0]["BaselineId"] == "pb-12345"
547
+ assert result[0]["BaselineName"] == "MyBaseline"
548
+ assert result[0]["Region"] == "us-east-1"
549
+ assert result[0]["DefaultBaseline"] is True
550
+ assert result[0]["Description"] == "Test baseline for Linux"
551
+ assert result[1]["BaselineId"] == "pb-67890"
552
+ assert result[1]["OperatingSystem"] == "WINDOWS"
553
+
554
+ mock_paginator.paginate.assert_called_once_with(Filters=[{"Key": "OWNER", "Values": ["123456789012"]}])
555
+ assert mock_ssm_client.get_patch_baseline.call_count == 2
556
+
557
+ def test_list_patch_baselines_without_account_filter(self, collector_no_account, mock_ssm_client):
558
+ """Test listing patch baselines without account ID filtering."""
559
+ mock_paginator = MagicMock()
560
+ mock_ssm_client.get_paginator.return_value = mock_paginator
561
+
562
+ mock_paginator.paginate.return_value = [
563
+ {
564
+ "BaselineIdentities": [
565
+ {
566
+ "BaselineId": "pb-12345",
567
+ "BaselineName": "Baseline1",
568
+ "OperatingSystem": "AMAZON_LINUX_2",
569
+ }
570
+ ]
571
+ }
572
+ ]
573
+
574
+ mock_ssm_client.get_patch_baseline.return_value = {
575
+ "BaselineDescription": "Test baseline",
576
+ "ApprovalRules": {},
577
+ "ApprovedPatches": [],
578
+ "RejectedPatches": [],
579
+ }
580
+
581
+ result = collector_no_account._list_patch_baselines(mock_ssm_client)
582
+
583
+ assert len(result) == 1
584
+ assert result[0]["BaselineId"] == "pb-12345"
585
+
586
+ mock_paginator.paginate.assert_called_once_with(Filters=[])
587
+
588
+ @patch(f"{PATH}.logger")
589
+ def test_list_patch_baselines_baseline_detail_error(self, mock_logger, collector, mock_ssm_client):
590
+ """Test listing patch baselines with error getting baseline details."""
591
+ mock_paginator = MagicMock()
592
+ mock_ssm_client.get_paginator.return_value = mock_paginator
593
+
594
+ mock_paginator.paginate.return_value = [
595
+ {
596
+ "BaselineIdentities": [
597
+ {"BaselineId": "pb-12345", "BaselineName": "Baseline1", "OperatingSystem": "AMAZON_LINUX_2"},
598
+ {"BaselineId": "pb-67890", "BaselineName": "Baseline2", "OperatingSystem": "WINDOWS"},
599
+ ]
600
+ }
601
+ ]
602
+
603
+ error = ClientError({"Error": {"Code": "DoesNotExistException", "Message": "Not found"}}, "GetPatchBaseline")
604
+ mock_ssm_client.get_patch_baseline.side_effect = error
605
+
606
+ result = collector._list_patch_baselines(mock_ssm_client)
607
+
608
+ assert result == []
609
+ mock_logger.error.assert_not_called()
610
+
611
+ @patch(f"{PATH}.logger")
612
+ def test_list_patch_baselines_baseline_detail_access_denied(self, mock_logger, collector, mock_ssm_client):
613
+ """Test listing patch baselines with access denied getting baseline details."""
614
+ mock_paginator = MagicMock()
615
+ mock_ssm_client.get_paginator.return_value = mock_paginator
616
+
617
+ mock_paginator.paginate.return_value = [
618
+ {
619
+ "BaselineIdentities": [
620
+ {"BaselineId": "pb-12345", "BaselineName": "Baseline1", "OperatingSystem": "AMAZON_LINUX_2"}
621
+ ]
622
+ }
623
+ ]
624
+
625
+ error = ClientError(
626
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "GetPatchBaseline"
627
+ )
628
+ mock_ssm_client.get_patch_baseline.side_effect = error
629
+
630
+ result = collector._list_patch_baselines(mock_ssm_client)
631
+
632
+ assert result == []
633
+ mock_logger.error.assert_not_called()
634
+
635
+ @patch(f"{PATH}.logger")
636
+ def test_list_patch_baselines_baseline_detail_other_error(self, mock_logger, collector, mock_ssm_client):
637
+ """Test listing patch baselines with other error getting baseline details."""
638
+ mock_paginator = MagicMock()
639
+ mock_ssm_client.get_paginator.return_value = mock_paginator
640
+
641
+ mock_paginator.paginate.return_value = [
642
+ {
643
+ "BaselineIdentities": [
644
+ {"BaselineId": "pb-12345", "BaselineName": "Baseline1", "OperatingSystem": "AMAZON_LINUX_2"}
645
+ ]
646
+ }
647
+ ]
648
+
649
+ error = ClientError({"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "GetPatchBaseline")
650
+ mock_ssm_client.get_patch_baseline.side_effect = error
651
+
652
+ result = collector._list_patch_baselines(mock_ssm_client)
653
+
654
+ assert result == []
655
+ mock_logger.error.assert_called_once()
656
+ assert "Error getting baseline pb-12345" in str(mock_logger.error.call_args)
657
+
658
+ @patch(f"{PATH}.logger")
659
+ def test_list_patch_baselines_access_denied(self, mock_logger, collector, mock_ssm_client):
660
+ """Test listing patch baselines with access denied error."""
661
+ mock_paginator = MagicMock()
662
+ mock_ssm_client.get_paginator.return_value = mock_paginator
663
+ error = ClientError(
664
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "DescribePatchBaselines"
665
+ )
666
+
667
+ mock_paginator.paginate.side_effect = error
668
+
669
+ result = collector._list_patch_baselines(mock_ssm_client)
670
+
671
+ assert result == []
672
+ mock_logger.warning.assert_called_once()
673
+ assert "Access denied to list patch baselines in us-east-1" in str(mock_logger.warning.call_args)
674
+
675
+ @patch(f"{PATH}.logger")
676
+ def test_list_patch_baselines_other_error(self, mock_logger, collector, mock_ssm_client):
677
+ """Test listing patch baselines with other ClientError."""
678
+ mock_paginator = MagicMock()
679
+ mock_ssm_client.get_paginator.return_value = mock_paginator
680
+ error = ClientError(
681
+ {"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "DescribePatchBaselines"
682
+ )
683
+
684
+ mock_paginator.paginate.side_effect = error
685
+
686
+ result = collector._list_patch_baselines(mock_ssm_client)
687
+
688
+ assert result == []
689
+ mock_logger.error.assert_called_once()
690
+ assert "Error listing patch baselines" in str(mock_logger.error.call_args)
691
+
692
+ def test_list_maintenance_windows_success(self, collector, mock_ssm_client):
693
+ """Test successful listing of maintenance windows with pagination."""
694
+ mock_paginator = MagicMock()
695
+ mock_ssm_client.get_paginator.return_value = mock_paginator
696
+
697
+ mock_paginator.paginate.return_value = [
698
+ {
699
+ "WindowIdentities": [
700
+ {
701
+ "WindowId": "mw-12345",
702
+ "Name": "PatchWindow",
703
+ "Description": "Weekly patching window",
704
+ "Enabled": True,
705
+ "Duration": 2,
706
+ "Cutoff": 0,
707
+ "Schedule": "cron(0 2 ? * SUN *)",
708
+ "ScheduleTimezone": "America/New_York",
709
+ "NextExecutionTime": "2024-01-07T02:00:00Z",
710
+ },
711
+ {
712
+ "WindowId": "mw-67890",
713
+ "Name": "MaintenanceWindow",
714
+ "Enabled": False,
715
+ "Duration": 4,
716
+ "Cutoff": 1,
717
+ "Schedule": "rate(7 days)",
718
+ },
719
+ ]
720
+ }
721
+ ]
722
+
723
+ result = collector._list_maintenance_windows(mock_ssm_client)
724
+
725
+ assert len(result) == 2
726
+ assert result[0]["WindowId"] == "mw-12345"
727
+ assert result[0]["Name"] == "PatchWindow"
728
+ assert result[0]["Region"] == "us-east-1"
729
+ assert result[0]["Enabled"] is True
730
+ assert result[0]["Duration"] == 2
731
+ assert result[1]["WindowId"] == "mw-67890"
732
+ assert result[1]["Enabled"] is False
733
+
734
+ mock_ssm_client.get_paginator.assert_called_once_with("describe_maintenance_windows")
735
+
736
+ @patch(f"{PATH}.logger")
737
+ def test_list_maintenance_windows_access_denied(self, mock_logger, collector, mock_ssm_client):
738
+ """Test listing maintenance windows with access denied error."""
739
+ mock_paginator = MagicMock()
740
+ mock_ssm_client.get_paginator.return_value = mock_paginator
741
+ error = ClientError(
742
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "DescribeMaintenanceWindows"
743
+ )
744
+
745
+ mock_paginator.paginate.side_effect = error
746
+
747
+ result = collector._list_maintenance_windows(mock_ssm_client)
748
+
749
+ assert result == []
750
+ mock_logger.warning.assert_called_once()
751
+ assert "Access denied to list maintenance windows in us-east-1" in str(mock_logger.warning.call_args)
752
+
753
+ @patch(f"{PATH}.logger")
754
+ def test_list_maintenance_windows_other_error(self, mock_logger, collector, mock_ssm_client):
755
+ """Test listing maintenance windows with other ClientError."""
756
+ mock_paginator = MagicMock()
757
+ mock_ssm_client.get_paginator.return_value = mock_paginator
758
+ error = ClientError(
759
+ {"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "DescribeMaintenanceWindows"
760
+ )
761
+
762
+ mock_paginator.paginate.side_effect = error
763
+
764
+ result = collector._list_maintenance_windows(mock_ssm_client)
765
+
766
+ assert result == []
767
+ mock_logger.error.assert_called_once()
768
+ assert "Error listing maintenance windows" in str(mock_logger.error.call_args)
769
+
770
+ def test_list_associations_success(self, collector, mock_ssm_client):
771
+ """Test successful listing of associations with pagination."""
772
+ mock_paginator = MagicMock()
773
+ mock_ssm_client.get_paginator.return_value = mock_paginator
774
+
775
+ test_datetime = datetime(2024, 1, 1, 12, 0, 0)
776
+ mock_paginator.paginate.return_value = [
777
+ {
778
+ "Associations": [
779
+ {
780
+ "AssociationId": "assoc-12345",
781
+ "AssociationName": "PatchAssociation",
782
+ "InstanceId": "i-12345",
783
+ "DocumentVersion": "1",
784
+ "Targets": [{"Key": "tag:Environment", "Values": ["Production"]}],
785
+ "LastExecutionDate": test_datetime,
786
+ "ScheduleExpression": "rate(1 day)",
787
+ "AssociationVersion": "1",
788
+ },
789
+ {
790
+ "AssociationId": "assoc-67890",
791
+ "AssociationName": "ConfigureAssociation",
792
+ "DocumentVersion": "2",
793
+ "Targets": [],
794
+ "AssociationVersion": "2",
795
+ },
796
+ ]
797
+ }
798
+ ]
799
+
800
+ result = collector._list_associations(mock_ssm_client)
801
+
802
+ assert len(result) == 2
803
+ assert result[0]["AssociationId"] == "assoc-12345"
804
+ assert result[0]["AssociationName"] == "PatchAssociation"
805
+ assert result[0]["Region"] == "us-east-1"
806
+ assert result[0]["InstanceId"] == "i-12345"
807
+ assert len(result[0]["Targets"]) == 1
808
+ assert result[1]["AssociationId"] == "assoc-67890"
809
+ assert result[1]["Targets"] == []
810
+
811
+ mock_ssm_client.get_paginator.assert_called_once_with("list_associations")
812
+
813
+ @patch(f"{PATH}.logger")
814
+ def test_list_associations_access_denied(self, mock_logger, collector, mock_ssm_client):
815
+ """Test listing associations with access denied error."""
816
+ mock_paginator = MagicMock()
817
+ mock_ssm_client.get_paginator.return_value = mock_paginator
818
+ error = ClientError(
819
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "ListAssociations"
820
+ )
821
+
822
+ mock_paginator.paginate.side_effect = error
823
+
824
+ result = collector._list_associations(mock_ssm_client)
825
+
826
+ assert result == []
827
+ mock_logger.warning.assert_called_once()
828
+ assert "Access denied to list associations in us-east-1" in str(mock_logger.warning.call_args)
829
+
830
+ @patch(f"{PATH}.logger")
831
+ def test_list_associations_other_error(self, mock_logger, collector, mock_ssm_client):
832
+ """Test listing associations with other ClientError."""
833
+ mock_paginator = MagicMock()
834
+ mock_ssm_client.get_paginator.return_value = mock_paginator
835
+ error = ClientError({"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "ListAssociations")
836
+
837
+ mock_paginator.paginate.side_effect = error
838
+
839
+ result = collector._list_associations(mock_ssm_client)
840
+
841
+ assert result == []
842
+ mock_logger.error.assert_called_once()
843
+ assert "Error listing associations" in str(mock_logger.error.call_args)
844
+
845
+ def test_get_compliance_summary_success(self, collector, mock_ssm_client):
846
+ """Test successful retrieval of compliance summary."""
847
+ mock_ssm_client.list_compliance_summaries.return_value = {
848
+ "ComplianceSummaryItems": [
849
+ {"ComplianceType": "Patch", "CompliantCount": 50, "NonCompliantCount": 5},
850
+ {"ComplianceType": "Association", "CompliantCount": 30, "NonCompliantCount": 2},
851
+ {"ComplianceType": "Custom:Security", "CompliantCount": 20, "NonCompliantCount": 0},
852
+ ]
853
+ }
854
+
855
+ result = collector._get_compliance_summary(mock_ssm_client)
856
+
857
+ assert result["TotalCompliant"] == 100
858
+ assert result["TotalNonCompliant"] == 7
859
+ assert len(result["ComplianceTypes"]) == 3
860
+ assert result["ComplianceTypes"][0]["ComplianceType"] == "Patch"
861
+ assert result["ComplianceTypes"][0]["CompliantCount"] == 50
862
+ assert result["ComplianceTypes"][1]["NonCompliantCount"] == 2
863
+
864
+ mock_ssm_client.list_compliance_summaries.assert_called_once_with(MaxResults=50)
865
+
866
+ def test_get_compliance_summary_empty(self, collector, mock_ssm_client):
867
+ """Test compliance summary with no summaries returned."""
868
+ mock_ssm_client.list_compliance_summaries.return_value = {"ComplianceSummaryItems": []}
869
+
870
+ result = collector._get_compliance_summary(mock_ssm_client)
871
+
872
+ assert result == {}
873
+
874
+ def test_get_compliance_summary_missing_counts(self, collector, mock_ssm_client):
875
+ """Test compliance summary with missing count fields."""
876
+ mock_ssm_client.list_compliance_summaries.return_value = {
877
+ "ComplianceSummaryItems": [
878
+ {"ComplianceType": "Patch"},
879
+ {"ComplianceType": "Association", "CompliantCount": 10},
880
+ ]
881
+ }
882
+
883
+ result = collector._get_compliance_summary(mock_ssm_client)
884
+
885
+ assert result["TotalCompliant"] == 10
886
+ assert result["TotalNonCompliant"] == 0
887
+ assert result["ComplianceTypes"][0]["CompliantCount"] == 0
888
+ assert result["ComplianceTypes"][0]["NonCompliantCount"] == 0
889
+
890
+ @patch(f"{PATH}.logger")
891
+ def test_get_compliance_summary_access_denied(self, mock_logger, collector, mock_ssm_client):
892
+ """Test compliance summary with access denied error."""
893
+ error = ClientError(
894
+ {"Error": {"Code": "AccessDeniedException", "Message": "Access denied"}}, "ListComplianceSummaries"
895
+ )
896
+
897
+ mock_ssm_client.list_compliance_summaries.side_effect = error
898
+
899
+ result = collector._get_compliance_summary(mock_ssm_client)
900
+
901
+ assert result == {}
902
+ mock_logger.warning.assert_called_once()
903
+ assert "Access denied to get compliance summary in us-east-1" in str(mock_logger.warning.call_args)
904
+
905
+ @patch(f"{PATH}.logger")
906
+ def test_get_compliance_summary_other_error(self, mock_logger, collector, mock_ssm_client):
907
+ """Test compliance summary with other ClientError."""
908
+ error = ClientError(
909
+ {"Error": {"Code": "InternalServerError", "Message": "Server error"}}, "ListComplianceSummaries"
910
+ )
911
+
912
+ mock_ssm_client.list_compliance_summaries.side_effect = error
913
+
914
+ result = collector._get_compliance_summary(mock_ssm_client)
915
+
916
+ assert result == {}
917
+ mock_logger.debug.assert_called_once()
918
+ assert "Error getting compliance summary" in str(mock_logger.debug.call_args)