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,3742 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Unit tests for AWS integration in RegScale CLI."""
4
+
5
+ from unittest.mock import MagicMock, patch, mock_open
6
+
7
+ import pytest
8
+
9
+ from regscale.integrations.commercial.aws.scanner import AWSInventoryIntegration
10
+ from regscale.integrations.scanner_integration import IntegrationAsset
11
+ from regscale.models import regscale_models
12
+ from tests import CLITestFixture
13
+
14
+
15
+ class TestAws(CLITestFixture):
16
+ """Test suite for AWS integration in RegScale CLI."""
17
+
18
+ @staticmethod
19
+ def _build_ec2_instance_data(
20
+ instance_id: str = "i-1234567890abcdef0",
21
+ name: str = "Test Instance",
22
+ state: str = "running",
23
+ instance_type: str = "t3.micro",
24
+ **kwargs,
25
+ ) -> dict:
26
+ """Build test EC2 instance data with sensible defaults."""
27
+ base_data = {
28
+ "InstanceId": instance_id,
29
+ "InstanceType": instance_type,
30
+ "State": state,
31
+ "Region": "us-east-1",
32
+ "OwnerId": "123456789012",
33
+ "CpuOptions": {"CoreCount": 2, "ThreadsPerCore": 1},
34
+ "BlockDeviceMappings": [{"DeviceName": "/dev/xvda", "Ebs": {"VolumeId": "vol-12345678"}}],
35
+ "ImageInfo": {
36
+ "Name": "amzn2-ami-hvm-2.0.20231212.0-x86_64-gp2",
37
+ "Description": "Amazon Linux 2 AMI",
38
+ "RootDeviceType": "ebs",
39
+ "VirtualizationType": "hvm",
40
+ },
41
+ "PlatformDetails": "Linux/UNIX",
42
+ "Architecture": "x86_64",
43
+ }
44
+
45
+ if name != "Test Instance":
46
+ base_data["Tags"] = [{"Key": "Name", "Value": name}]
47
+
48
+ base_data.update(kwargs)
49
+ return base_data
50
+
51
+ @pytest.fixture
52
+ def mock_aws_integration(self):
53
+ """Create a properly configured MagicMock for AWSInventoryIntegration."""
54
+ mock_self = MagicMock()
55
+ mock_self.collector = None
56
+ mock_self.authenticate = MagicMock()
57
+ mock_self.num_assets_to_process = 0
58
+ return mock_self
59
+
60
+ @pytest.fixture
61
+ def aws_integration(self):
62
+ """Create a real AWSInventoryIntegration instance for parser tests."""
63
+ return AWSInventoryIntegration(plan_id=1)
64
+
65
+ @patch("regscale.integrations.commercial.aws.scanner.json.load")
66
+ @patch("regscale.integrations.commercial.aws.scanner.os.path.exists", return_value=True)
67
+ @patch("regscale.integrations.commercial.aws.scanner.os.path.getmtime")
68
+ @patch("regscale.integrations.commercial.aws.scanner.time.time")
69
+ @patch("builtins.open", new_callable=mock_open)
70
+ def test_returns_cached_data_when_cache_is_valid(
71
+ self, mock_open, mock_time, mock_getmtime, mock_exists, mock_json_load, mock_aws_integration
72
+ ):
73
+ """Should return cached data when cache exists and is not expired."""
74
+ from regscale.integrations.commercial.aws.scanner import CACHE_TTL_SECONDS
75
+
76
+ cached_data = {"test": "cached_data"}
77
+ mock_json_load.return_value = cached_data
78
+ mock_getmtime.return_value = 0
79
+ mock_time.return_value = CACHE_TTL_SECONDS - 1
80
+
81
+ result = AWSInventoryIntegration.fetch_aws_data_if_needed(
82
+ mock_aws_integration,
83
+ region="us-east-1",
84
+ aws_access_key_id=None,
85
+ aws_secret_access_key=None,
86
+ aws_session_token=None,
87
+ )
88
+
89
+ assert result == cached_data
90
+ mock_open.assert_called_once()
91
+ mock_json_load.assert_called_once()
92
+
93
+ @patch("regscale.integrations.commercial.aws.scanner.os.path.exists", return_value=False)
94
+ @patch("regscale.integrations.commercial.aws.scanner.os.makedirs")
95
+ @patch("regscale.integrations.commercial.aws.scanner.json.dump")
96
+ @patch("builtins.open", new_callable=mock_open)
97
+ @patch("regscale.integrations.commercial.aws.scanner.AWSInventoryCollector")
98
+ def test_fetches_fresh_data_when_cache_missing(
99
+ self, mock_collector_class, mock_open, mock_json_dump, mock_makedirs, mock_exists, mock_aws_integration
100
+ ):
101
+ """Should fetch fresh data when cache doesn't exist."""
102
+ fresh_data = {"fresh": "data"}
103
+ mock_collector = MagicMock()
104
+ mock_collector.collect_all.return_value = fresh_data
105
+ mock_collector_class.return_value = mock_collector
106
+
107
+ def mock_authenticate(*args, **kwargs):
108
+ mock_aws_integration.collector = mock_collector
109
+
110
+ mock_aws_integration.authenticate.side_effect = mock_authenticate
111
+
112
+ result = AWSInventoryIntegration.fetch_aws_data_if_needed(
113
+ mock_aws_integration,
114
+ region="us-east-1",
115
+ aws_access_key_id="test_key",
116
+ aws_secret_access_key="test_secret",
117
+ aws_session_token=None,
118
+ )
119
+
120
+ assert result == fresh_data
121
+ mock_aws_integration.authenticate.assert_called_once_with(
122
+ "test_key", "test_secret", "us-east-1", None, None, None, None
123
+ )
124
+ mock_collector.collect_all.assert_called_once()
125
+ mock_makedirs.assert_called_once()
126
+ mock_json_dump.assert_called_once()
127
+
128
+ @patch("regscale.integrations.commercial.aws.scanner.os.path.exists", return_value=True)
129
+ @patch("regscale.integrations.commercial.aws.scanner.os.path.getmtime")
130
+ @patch("regscale.integrations.commercial.aws.scanner.time.time")
131
+ @patch("regscale.integrations.commercial.aws.scanner.json.load")
132
+ @patch("builtins.open", new_callable=mock_open)
133
+ def test_raises_error_when_cache_expired_and_authentication_fails(
134
+ self, mock_open, mock_json_load, mock_time, mock_getmtime, mock_exists, mock_aws_integration
135
+ ):
136
+ """Should raise RuntimeError when cache is expired and authentication fails."""
137
+ from regscale.integrations.commercial.aws.scanner import CACHE_TTL_SECONDS
138
+
139
+ mock_getmtime.return_value = 0
140
+ mock_time.return_value = CACHE_TTL_SECONDS + 1
141
+ mock_aws_integration.authenticate.return_value = None
142
+
143
+ with pytest.raises(RuntimeError, match="Failed to initialize AWS inventory collector"):
144
+ AWSInventoryIntegration.fetch_aws_data_if_needed(
145
+ mock_aws_integration,
146
+ region="us-east-1",
147
+ aws_access_key_id=None,
148
+ aws_secret_access_key=None,
149
+ aws_session_token=None,
150
+ )
151
+
152
+ def test_processes_normal_asset_list(self, mock_aws_integration):
153
+ """Should process a normal list of asset dictionaries."""
154
+ assets = [
155
+ {"id": "asset1", "name": "Test Asset 1"},
156
+ {"id": "asset2", "name": "Test Asset 2"},
157
+ ]
158
+ asset_type = "EC2 instance"
159
+
160
+ mock_parser = MagicMock()
161
+ mock_parser.side_effect = [
162
+ IntegrationAsset(name="Asset 1", identifier="asset1", asset_type="EC2", asset_category="Compute"),
163
+ IntegrationAsset(name="Asset 2", identifier="asset2", asset_type="EC2", asset_category="Compute"),
164
+ ]
165
+
166
+ results = list(
167
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
168
+ )
169
+
170
+ assert len(results) == 2
171
+ assert results[0].name == "Asset 1"
172
+ assert results[1].name == "Asset 2"
173
+ assert mock_parser.call_count == 2
174
+
175
+ def test_processes_special_users_structure(self, mock_aws_integration):
176
+ """Should process special Users structure correctly."""
177
+ assets = [
178
+ {"id": "user1", "name": "User 1"},
179
+ {"id": "user2", "name": "User 2"},
180
+ ]
181
+ asset_type = "IAM Users"
182
+
183
+ mock_parser = MagicMock()
184
+ mock_parser.side_effect = [
185
+ IntegrationAsset(name="User 1", identifier="user1", asset_type="IAM", asset_category="Identity"),
186
+ IntegrationAsset(name="User 2", identifier="user2", asset_type="IAM", asset_category="Identity"),
187
+ ]
188
+
189
+ results = list(
190
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
191
+ )
192
+
193
+ assert len(results) == 2
194
+ assert results[0].name == "User 1"
195
+ assert results[1].name == "User 2"
196
+ assert mock_parser.call_count == 2
197
+
198
+ def test_process_asset_collection_roles_special_case(self, mock_aws_integration):
199
+ """Test processing special 'Roles' case"""
200
+ assets = [
201
+ {"id": "role1", "name": "Role 1"},
202
+ {"id": "role2", "name": "Role 2"},
203
+ ]
204
+ asset_type = "IAM Roles"
205
+
206
+ mock_parser = MagicMock()
207
+ mock_parser.side_effect = [
208
+ IntegrationAsset(name="Role 1", identifier="role1", asset_type="IAM", asset_category="Identity"),
209
+ IntegrationAsset(name="Role 2", identifier="role2", asset_type="IAM", asset_category="Identity"),
210
+ ]
211
+
212
+ results = list(
213
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
214
+ )
215
+
216
+ assert len(results) == 2
217
+ assert results[0].name == "Role 1"
218
+ assert results[1].name == "Role 2"
219
+ assert mock_parser.call_count == 2
220
+
221
+ def test_skips_invalid_asset_format(self, mock_aws_integration):
222
+ """Should skip assets with invalid format and log warning."""
223
+ assets = ["invalid_asset", {"id": "valid_asset", "name": "Valid Asset"}]
224
+ asset_type = "EC2 instance"
225
+
226
+ mock_parser = MagicMock()
227
+ mock_parser.return_value = IntegrationAsset(
228
+ name="Valid Asset", identifier="valid_asset", asset_type="EC2", asset_category="Compute"
229
+ )
230
+
231
+ with patch("regscale.integrations.commercial.aws.scanner.logger"):
232
+ results = list(
233
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
234
+ )
235
+
236
+ assert len(results) == 1
237
+ assert results[0].name == "Valid Asset"
238
+ assert mock_parser.call_count == 1
239
+
240
+ def test_process_asset_collection_parser_exception(self, mock_aws_integration):
241
+ """Test handling of parser method exceptions"""
242
+ assets = [
243
+ {"id": "asset1", "name": "Test Asset 1"},
244
+ {"id": "asset2", "name": "Test Asset 2"},
245
+ ]
246
+ asset_type = "EC2 instance"
247
+
248
+ mock_parser = MagicMock()
249
+ mock_parser.side_effect = [
250
+ IntegrationAsset(name="Asset 1", identifier="asset1", asset_type="EC2", asset_category="Compute"),
251
+ Exception("Parser error for asset 2"),
252
+ ]
253
+
254
+ with patch("regscale.integrations.commercial.aws.scanner.logger") as mock_logger:
255
+ results = list(
256
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
257
+ )
258
+
259
+ mock_logger.error.assert_called_once()
260
+ error_call_args = mock_logger.error.call_args
261
+ # The first argument is the message string
262
+ error_message = error_call_args[0][0]
263
+ assert "Error parsing EC2 instance" in error_message
264
+ assert "Parser error for asset 2" in error_message
265
+
266
+ assert len(results) == 1
267
+ assert results[0].name == "Asset 1"
268
+ assert mock_parser.call_count == 2
269
+
270
+ def test_process_asset_collection_empty_list(self, mock_aws_integration):
271
+ """Test processing empty asset list"""
272
+ assets = []
273
+ asset_type = "EC2 instance"
274
+
275
+ mock_parser = MagicMock()
276
+
277
+ results = list(
278
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
279
+ )
280
+
281
+ assert len(results) == 0
282
+ assert mock_parser.call_count == 0
283
+
284
+ def test_process_asset_collection_mixed_valid_invalid(self, mock_aws_integration):
285
+ """Test processing mixed valid and invalid assets"""
286
+ assets = [
287
+ {"id": "valid1", "name": "Valid 1"},
288
+ "invalid_string",
289
+ {"id": "valid2", "name": "Valid 2"},
290
+ None,
291
+ {"id": "valid3", "name": "Valid 3"},
292
+ ]
293
+ asset_type = "EC2 instance"
294
+
295
+ mock_parser = MagicMock()
296
+ mock_parser.side_effect = [
297
+ IntegrationAsset(name="Valid 1", identifier="valid1", asset_type="EC2", asset_category="Compute"),
298
+ IntegrationAsset(name="Valid 2", identifier="valid2", asset_type="EC2", asset_category="Compute"),
299
+ IntegrationAsset(name="Valid 3", identifier="valid3", asset_type="EC2", asset_category="Compute"),
300
+ ]
301
+
302
+ results = list(
303
+ AWSInventoryIntegration._process_asset_collection(mock_aws_integration, assets, asset_type, mock_parser)
304
+ )
305
+
306
+ assert len(results) == 3
307
+ assert results[0].name == "Valid 1"
308
+ assert results[1].name == "Valid 2"
309
+ assert results[2].name == "Valid 3"
310
+ assert mock_parser.call_count == 3
311
+
312
+ def test_process_asset_collection_empty_users_roles(self, mock_aws_integration):
313
+ """Test processing empty Users/Roles collections"""
314
+ assets_users = {"Users": []}
315
+ asset_type = "IAM Users"
316
+
317
+ mock_parser = MagicMock()
318
+
319
+ results = list(
320
+ AWSInventoryIntegration._process_asset_collection(
321
+ mock_aws_integration, assets_users, asset_type, mock_parser
322
+ )
323
+ )
324
+
325
+ assert len(results) == 0
326
+ assert mock_parser.call_count == 0
327
+
328
+ mock_aws_integration.num_assets_to_process = 0
329
+
330
+ assets_roles = {"Roles": []}
331
+ asset_type = "IAM Roles"
332
+
333
+ results = list(
334
+ AWSInventoryIntegration._process_asset_collection(
335
+ mock_aws_integration, assets_roles, asset_type, mock_parser
336
+ )
337
+ )
338
+
339
+ assert len(results) == 0
340
+ assert mock_parser.call_count == 0
341
+
342
+ def test_process_inventory_section_normal_processing(self, mock_aws_integration):
343
+ """Test normal processing of an inventory section"""
344
+ inventory = {
345
+ "EC2Instances": [
346
+ {"id": "i-1234567890", "name": "Test Instance 1"},
347
+ {"id": "i-0987654321", "name": "Test Instance 2"},
348
+ ]
349
+ }
350
+ section_key = "EC2Instances"
351
+ asset_type = "EC2 instance"
352
+
353
+ mock_parser = MagicMock()
354
+ mock_parser.side_effect = [
355
+ IntegrationAsset(name="Instance 1", identifier="i-1234567890", asset_type="EC2", asset_category="Compute"),
356
+ IntegrationAsset(name="Instance 2", identifier="i-0987654321", asset_type="EC2", asset_category="Compute"),
357
+ ]
358
+
359
+ mock_aws_integration._process_asset_collection = MagicMock()
360
+ mock_aws_integration._process_asset_collection.return_value = [
361
+ IntegrationAsset(name="Instance 1", identifier="i-1234567890", asset_type="EC2", asset_category="Compute"),
362
+ IntegrationAsset(name="Instance 2", identifier="i-0987654321", asset_type="EC2", asset_category="Compute"),
363
+ ]
364
+
365
+ results = list(
366
+ AWSInventoryIntegration._process_inventory_section(
367
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
368
+ )
369
+ )
370
+
371
+ mock_aws_integration._process_asset_collection.assert_called_once_with(
372
+ inventory["EC2Instances"], asset_type, mock_parser
373
+ )
374
+
375
+ assert len(results) == 2
376
+ assert results[0].name == "Instance 1"
377
+ assert results[1].name == "Instance 2"
378
+
379
+ def test_process_inventory_section_missing_key(self, mock_aws_integration):
380
+ """Test processing when section key doesn't exist in inventory"""
381
+ inventory = {
382
+ "S3Buckets": [
383
+ {"name": "test-bucket-1"},
384
+ {"name": "test-bucket-2"},
385
+ ]
386
+ }
387
+ section_key = "EC2Instances"
388
+ asset_type = "EC2 instance"
389
+
390
+ mock_parser = MagicMock()
391
+
392
+ mock_aws_integration._process_asset_collection = MagicMock()
393
+ mock_aws_integration._process_asset_collection.return_value = []
394
+
395
+ results = list(
396
+ AWSInventoryIntegration._process_inventory_section(
397
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
398
+ )
399
+ )
400
+
401
+ mock_aws_integration._process_asset_collection.assert_called_once_with([], asset_type, mock_parser)
402
+
403
+ assert len(results) == 0
404
+
405
+ def test_process_inventory_section_empty_section(self, mock_aws_integration):
406
+ """Test processing when section exists but is empty"""
407
+ inventory = {
408
+ "EC2Instances": [],
409
+ "S3Buckets": [
410
+ {"name": "test-bucket-1"},
411
+ ],
412
+ }
413
+ section_key = "EC2Instances"
414
+ asset_type = "EC2 instance"
415
+
416
+ mock_parser = MagicMock()
417
+
418
+ mock_aws_integration._process_asset_collection = MagicMock()
419
+ mock_aws_integration._process_asset_collection.return_value = []
420
+
421
+ results = list(
422
+ AWSInventoryIntegration._process_inventory_section(
423
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
424
+ )
425
+ )
426
+
427
+ mock_aws_integration._process_asset_collection.assert_called_once_with([], asset_type, mock_parser)
428
+
429
+ assert len(results) == 0
430
+
431
+ def test_process_inventory_section_empty_inventory(self, mock_aws_integration):
432
+ """Test processing with completely empty inventory"""
433
+ inventory = {}
434
+ section_key = "EC2Instances"
435
+ asset_type = "EC2 instance"
436
+
437
+ mock_parser = MagicMock()
438
+
439
+ mock_aws_integration._process_asset_collection = MagicMock()
440
+ mock_aws_integration._process_asset_collection.return_value = []
441
+
442
+ results = list(
443
+ AWSInventoryIntegration._process_inventory_section(
444
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
445
+ )
446
+ )
447
+
448
+ mock_aws_integration._process_asset_collection.assert_called_once_with([], asset_type, mock_parser)
449
+
450
+ assert len(results) == 0
451
+
452
+ def test_process_inventory_section_multiple_sections(self, mock_aws_integration):
453
+ """Test processing when inventory has multiple sections"""
454
+
455
+ inventory = {
456
+ "EC2Instances": [
457
+ {"id": "i-1234567890", "name": "Test Instance 1"},
458
+ ],
459
+ "S3Buckets": [
460
+ {"name": "test-bucket-1"},
461
+ {"name": "test-bucket-2"},
462
+ ],
463
+ "LambdaFunctions": [
464
+ {"name": "test-function-1"},
465
+ ],
466
+ }
467
+ section_key = "S3Buckets"
468
+ asset_type = "S3 bucket"
469
+
470
+ mock_parser = MagicMock()
471
+ mock_parser.side_effect = [
472
+ IntegrationAsset(name="Bucket 1", identifier="test-bucket-1", asset_type="S3", asset_category="Storage"),
473
+ IntegrationAsset(name="Bucket 2", identifier="test-bucket-2", asset_type="S3", asset_category="Storage"),
474
+ ]
475
+
476
+ mock_aws_integration._process_asset_collection = MagicMock()
477
+ mock_aws_integration._process_asset_collection.return_value = [
478
+ IntegrationAsset(name="Bucket 1", identifier="test-bucket-1", asset_type="S3", asset_category="Storage"),
479
+ IntegrationAsset(name="Bucket 2", identifier="test-bucket-2", asset_type="S3", asset_category="Storage"),
480
+ ]
481
+
482
+ results = list(
483
+ AWSInventoryIntegration._process_inventory_section(
484
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
485
+ )
486
+ )
487
+
488
+ mock_aws_integration._process_asset_collection.assert_called_once_with(
489
+ inventory["S3Buckets"], asset_type, mock_parser
490
+ )
491
+
492
+ assert len(results) == 2
493
+ assert results[0].name == "Bucket 1"
494
+ assert results[1].name == "Bucket 2"
495
+
496
+ def test_process_inventory_section_with_special_users_structure(self, mock_aws_integration):
497
+ """Test processing section that contains special Users structure"""
498
+ inventory = {
499
+ "IAM": {
500
+ "Users": [
501
+ {"id": "user1", "name": "User 1"},
502
+ {"id": "user2", "name": "User 2"},
503
+ ]
504
+ }
505
+ }
506
+ section_key = "IAM"
507
+ asset_type = "IAM Users"
508
+
509
+ mock_parser = MagicMock()
510
+ mock_parser.side_effect = [
511
+ IntegrationAsset(name="User 1", identifier="user1", asset_type="IAM", asset_category="Identity"),
512
+ IntegrationAsset(name="User 2", identifier="user2", asset_type="IAM", asset_category="Identity"),
513
+ ]
514
+
515
+ mock_aws_integration._process_asset_collection = MagicMock()
516
+ mock_aws_integration._process_asset_collection.return_value = [
517
+ IntegrationAsset(name="User 1", identifier="user1", asset_type="IAM", asset_category="Identity"),
518
+ IntegrationAsset(name="User 2", identifier="user2", asset_type="IAM", asset_category="Identity"),
519
+ ]
520
+
521
+ results = list(
522
+ AWSInventoryIntegration._process_inventory_section(
523
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
524
+ )
525
+ )
526
+
527
+ # The implementation extracts the list from the IAM dict using asset_type as key
528
+ # Since asset_type is "IAM Users" but the dict has "Users", it returns []
529
+ mock_aws_integration._process_asset_collection.assert_called_once_with([], asset_type, mock_parser)
530
+
531
+ assert len(results) == 2
532
+ assert results[0].name == "User 1"
533
+ assert results[1].name == "User 2"
534
+
535
+ def test_process_inventory_section_delegates_to_process_asset_collection(self, mock_aws_integration):
536
+ """Test that _process_inventory_section properly delegates to _process_asset_collection"""
537
+ inventory = {
538
+ "EC2Instances": [
539
+ {"id": "i-1234567890", "name": "Test Instance"},
540
+ ]
541
+ }
542
+ section_key = "EC2Instances"
543
+ asset_type = "EC2 instance"
544
+
545
+ mock_parser = MagicMock()
546
+ mock_parser.return_value = IntegrationAsset(
547
+ name="Test Instance", identifier="i-1234567890", asset_type="EC2", asset_category="Compute"
548
+ )
549
+
550
+ mock_aws_integration._process_asset_collection = MagicMock()
551
+ mock_aws_integration._process_asset_collection.return_value = [mock_parser.return_value]
552
+
553
+ results = list(
554
+ AWSInventoryIntegration._process_inventory_section(
555
+ mock_aws_integration, inventory, section_key, asset_type, mock_parser
556
+ )
557
+ )
558
+
559
+ mock_aws_integration._process_asset_collection.assert_called_once_with(
560
+ inventory["EC2Instances"], asset_type, mock_parser
561
+ )
562
+
563
+ assert len(results) == 1
564
+ assert results[0].name == "Test Instance"
565
+
566
+ def test_fetch_assets_normal_processing(self, mock_aws_integration):
567
+ """Test normal processing of assets from inventory"""
568
+ inventory = {
569
+ "EC2Instances": [
570
+ {"id": "i-1234567890", "name": "Test Instance 1"},
571
+ {"id": "i-0987654321", "name": "Test Instance 2"},
572
+ ],
573
+ "S3Buckets": [
574
+ {"name": "test-bucket-1"},
575
+ {"name": "test-bucket-2"},
576
+ ],
577
+ "IAM": {
578
+ "Users": [
579
+ {"id": "user1", "name": "User 1"},
580
+ ]
581
+ },
582
+ }
583
+
584
+ mock_aws_integration.fetch_aws_data_if_needed = MagicMock(return_value=inventory)
585
+
586
+ mock_aws_integration.get_asset_configs = MagicMock(
587
+ return_value=[
588
+ ("EC2Instances", "EC2 instance", MagicMock()),
589
+ ("S3Buckets", "S3 bucket", MagicMock()),
590
+ ]
591
+ )
592
+
593
+ mock_aws_integration._process_inventory_section = MagicMock()
594
+ mock_aws_integration._process_inventory_section.side_effect = [
595
+ [
596
+ IntegrationAsset(
597
+ name="Instance 1", identifier="i-1234567890", asset_type="EC2", asset_category="Compute"
598
+ ),
599
+ IntegrationAsset(
600
+ name="Instance 2", identifier="i-0987654321", asset_type="EC2", asset_category="Compute"
601
+ ),
602
+ ],
603
+ [
604
+ IntegrationAsset(
605
+ name="Bucket 1", identifier="test-bucket-1", asset_type="S3", asset_category="Storage"
606
+ ),
607
+ IntegrationAsset(
608
+ name="Bucket 2", identifier="test-bucket-2", asset_type="S3", asset_category="Storage"
609
+ ),
610
+ ],
611
+ ]
612
+
613
+ results = list(
614
+ AWSInventoryIntegration.fetch_assets(
615
+ mock_aws_integration,
616
+ region="us-east-1",
617
+ aws_access_key_id="test_key",
618
+ aws_secret_access_key="test_secret",
619
+ aws_session_token="test_token",
620
+ )
621
+ )
622
+
623
+ mock_aws_integration.fetch_aws_data_if_needed.assert_called_once_with(
624
+ "us-east-1", "test_key", "test_secret", "test_token", None, None, None, False
625
+ )
626
+
627
+ mock_aws_integration.get_asset_configs.assert_called_once()
628
+
629
+ assert mock_aws_integration._process_inventory_section.call_count == 2
630
+
631
+ assert mock_aws_integration.num_assets_to_process == 0
632
+
633
+ assert len(results) == 4
634
+ assert results[0].name == "Instance 1"
635
+ assert results[1].name == "Instance 2"
636
+ assert results[2].name == "Bucket 1"
637
+ assert results[3].name == "Bucket 2"
638
+
639
+ def test_fetch_assets_empty_inventory(self, mock_aws_integration):
640
+ """Test fetching assets when inventory is empty"""
641
+ inventory = {}
642
+
643
+ mock_aws_integration.fetch_aws_data_if_needed = MagicMock(return_value=inventory)
644
+
645
+ mock_aws_integration.get_asset_configs = MagicMock(
646
+ return_value=[
647
+ ("EC2Instances", "EC2 instance", MagicMock()),
648
+ ("S3Buckets", "S3 bucket", MagicMock()),
649
+ ]
650
+ )
651
+
652
+ mock_aws_integration._process_inventory_section = MagicMock(return_value=[])
653
+
654
+ results = list(AWSInventoryIntegration.fetch_assets(mock_aws_integration, region="us-east-1"))
655
+
656
+ mock_aws_integration.fetch_aws_data_if_needed.assert_called_once_with(
657
+ "us-east-1", None, None, None, None, None, None, False
658
+ )
659
+
660
+ assert mock_aws_integration._process_inventory_section.call_count == 2
661
+
662
+ assert len(results) == 0
663
+
664
+ def test_fetch_assets_no_asset_configs(self, mock_aws_integration):
665
+ """Test fetching assets when no asset configs are available"""
666
+ inventory = {
667
+ "EC2Instances": [
668
+ {"id": "i-1234567890", "name": "Test Instance"},
669
+ ]
670
+ }
671
+
672
+ mock_aws_integration.fetch_aws_data_if_needed = MagicMock(return_value=inventory)
673
+
674
+ mock_aws_integration.get_asset_configs = MagicMock(return_value=[])
675
+
676
+ mock_aws_integration._process_inventory_section = MagicMock()
677
+
678
+ results = list(AWSInventoryIntegration.fetch_assets(mock_aws_integration, region="us-east-1"))
679
+
680
+ mock_aws_integration.fetch_aws_data_if_needed.assert_called_once()
681
+
682
+ mock_aws_integration.get_asset_configs.assert_called_once()
683
+
684
+ mock_aws_integration._process_inventory_section.assert_not_called()
685
+
686
+ assert len(results) == 0
687
+
688
+ def test_fetch_assets_all_asset_types(self, mock_aws_integration):
689
+ """Test fetching assets for all configured asset types"""
690
+ inventory = {
691
+ "IAM": {"Users": [{"id": "user1", "name": "User 1"}]},
692
+ "EC2Instances": [{"id": "i-1234567890", "name": "Test Instance"}],
693
+ "LambdaFunctions": [{"name": "test-function"}],
694
+ "S3Buckets": [{"name": "test-bucket"}],
695
+ "RDSInstances": [{"name": "test-rds"}],
696
+ "DynamoDBTables": [{"name": "test-dynamo"}],
697
+ "VPCs": [{"name": "test-vpc"}],
698
+ "LoadBalancers": [{"name": "test-lb"}],
699
+ "ECRRepositories": [{"name": "test-ecr"}],
700
+ }
701
+
702
+ mock_aws_integration.fetch_aws_data_if_needed = MagicMock(return_value=inventory)
703
+
704
+ mock_aws_integration.get_asset_configs = MagicMock(
705
+ return_value=[
706
+ ("IAM", "Roles", MagicMock()),
707
+ ("EC2Instances", "EC2 instance", MagicMock()),
708
+ ("LambdaFunctions", "Lambda function", MagicMock()),
709
+ ("S3Buckets", "S3 bucket", MagicMock()),
710
+ ("RDSInstances", "RDS instance", MagicMock()),
711
+ ("DynamoDBTables", "DynamoDB table", MagicMock()),
712
+ ("VPCs", "VPC", MagicMock()),
713
+ ("LoadBalancers", "Load Balancer", MagicMock()),
714
+ ("ECRRepositories", "ECR repository", MagicMock()),
715
+ ]
716
+ )
717
+
718
+ mock_aws_integration._process_inventory_section = MagicMock()
719
+ mock_aws_integration._process_inventory_section.side_effect = [
720
+ [IntegrationAsset(name="User 1", identifier="user1", asset_type="IAM", asset_category="Identity")],
721
+ [
722
+ IntegrationAsset(
723
+ name="Instance 1", identifier="i-1234567890", asset_type="EC2", asset_category="Compute"
724
+ )
725
+ ],
726
+ [
727
+ IntegrationAsset(
728
+ name="Function 1", identifier="test-function", asset_type="Lambda", asset_category="Compute"
729
+ )
730
+ ],
731
+ [IntegrationAsset(name="Bucket 1", identifier="test-bucket", asset_type="S3", asset_category="Storage")],
732
+ [IntegrationAsset(name="RDS 1", identifier="test-rds", asset_type="RDS", asset_category="Database")],
733
+ [
734
+ IntegrationAsset(
735
+ name="Dynamo 1", identifier="test-dynamo", asset_type="DynamoDB", asset_category="Database"
736
+ )
737
+ ],
738
+ [IntegrationAsset(name="VPC 1", identifier="test-vpc", asset_type="VPC", asset_category="Network")],
739
+ [IntegrationAsset(name="LB 1", identifier="test-lb", asset_type="LoadBalancer", asset_category="Network")],
740
+ [IntegrationAsset(name="ECR 1", identifier="test-ecr", asset_type="ECR", asset_category="Container")],
741
+ ]
742
+
743
+ results = list(AWSInventoryIntegration.fetch_assets(mock_aws_integration, region="us-east-1"))
744
+
745
+ assert mock_aws_integration._process_inventory_section.call_count == 9
746
+
747
+ assert len(results) == 9
748
+ assert results[0].name == "User 1"
749
+ assert results[1].name == "Instance 1"
750
+ assert results[2].name == "Function 1"
751
+ assert results[3].name == "Bucket 1"
752
+ assert results[4].name == "RDS 1"
753
+ assert results[5].name == "Dynamo 1"
754
+ assert results[6].name == "VPC 1"
755
+ assert results[7].name == "LB 1"
756
+ assert results[8].name == "ECR 1"
757
+
758
+ def test_fetch_assets_delegates_to_other_methods(self, mock_aws_integration):
759
+ """Test that fetch_assets properly delegates to other methods"""
760
+ inventory = {
761
+ "EC2Instances": [{"id": "i-1234567890", "name": "Test Instance"}],
762
+ }
763
+
764
+ mock_aws_integration.fetch_aws_data_if_needed = MagicMock(return_value=inventory)
765
+ mock_aws_integration.get_asset_configs = MagicMock(
766
+ return_value=[
767
+ ("EC2Instances", "EC2 instance", MagicMock()),
768
+ ]
769
+ )
770
+ mock_aws_integration._process_inventory_section = MagicMock(
771
+ return_value=[
772
+ IntegrationAsset(
773
+ name="Test Instance", identifier="i-1234567890", asset_type="EC2", asset_category="Compute"
774
+ )
775
+ ]
776
+ )
777
+
778
+ results = list(
779
+ AWSInventoryIntegration.fetch_assets(
780
+ mock_aws_integration,
781
+ region="us-east-1",
782
+ aws_access_key_id="test_key",
783
+ aws_secret_access_key="test_secret",
784
+ )
785
+ )
786
+
787
+ mock_aws_integration.fetch_aws_data_if_needed.assert_called_once_with(
788
+ "us-east-1", "test_key", "test_secret", None, None, None, None, False
789
+ )
790
+
791
+ mock_aws_integration.get_asset_configs.assert_called_once()
792
+
793
+ mock_aws_integration._process_inventory_section.assert_called_once_with(
794
+ inventory, "EC2Instances", "EC2 instance", mock_aws_integration.get_asset_configs.return_value[0][2]
795
+ )
796
+
797
+ assert mock_aws_integration.num_assets_to_process == 0
798
+
799
+ assert len(results) == 1
800
+ assert results[0].name == "Test Instance"
801
+
802
+ def test_parses_linux_instance_with_name_tag(self, aws_integration):
803
+ """Should parse Linux EC2 instance with Name tag correctly."""
804
+ instance = self._build_ec2_instance_data(
805
+ instance_id="i-1234567890abcdef0",
806
+ name="Test Linux Server",
807
+ PrivateIpAddress="10.0.1.100",
808
+ PublicIpAddress="52.1.2.3",
809
+ PrivateDnsName="ip-10-0-1-100.ec2.internal",
810
+ PublicDnsName="ec2-52-1-2-3.compute-1.amazonaws.com",
811
+ VpcId="vpc-12345678",
812
+ SubnetId="subnet-12345678",
813
+ ImageId="ami-12345678",
814
+ Architecture="x86_64",
815
+ PlatformDetails="Linux/UNIX",
816
+ CpuOptions={"CoreCount": 2, "ThreadsPerCore": 2},
817
+ Tags=[{"Key": "Name", "Value": "Test Linux Server"}, {"Key": "Environment", "Value": "Production"}],
818
+ )
819
+
820
+ result = aws_integration.parse_ec2_instance(instance)
821
+
822
+ assert result.name == "Test Linux Server"
823
+ assert result.identifier == "arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0"
824
+ assert result.asset_type == regscale_models.AssetType.VM
825
+ assert result.operating_system == regscale_models.AssetOperatingSystem.Linux
826
+ assert result.is_public_facing is True
827
+ assert result.ip_address == "10.0.1.100"
828
+ assert result.fqdn == "ec2-52-1-2-3.compute-1.amazonaws.com"
829
+ assert result.cpu == 4 # 2 cores * 2 threads
830
+ assert result.ram == 16
831
+ assert result.location == "us-east-1"
832
+ assert result.model == "t3.micro"
833
+ assert result.manufacturer == "AWS"
834
+ assert result.vlan_id == "subnet-12345678"
835
+ assert result.is_virtual is True
836
+ assert result.source_data == instance
837
+
838
+ expected_uri = (
839
+ "https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#InstanceDetails:instanceId=i-1234567890abcdef0"
840
+ )
841
+ assert result.uri == expected_uri
842
+
843
+ def test_parses_windows_instance(self, aws_integration):
844
+ """Should parse Windows EC2 instance correctly."""
845
+ instance = self._build_ec2_instance_data(
846
+ instance_id="i-0987654321fedcba0",
847
+ instance_type="t3.small",
848
+ PrivateIpAddress="10.0.1.101",
849
+ Region="us-west-2",
850
+ Platform="windows",
851
+ PlatformDetails="Windows",
852
+ CpuOptions={"CoreCount": 1, "ThreadsPerCore": 2},
853
+ BlockDeviceMappings=[
854
+ {"DeviceName": "/dev/sda1", "Ebs": {"VolumeId": "vol-87654321"}},
855
+ {"DeviceName": "/dev/sdb", "Ebs": {"VolumeId": "vol-87654322"}},
856
+ ],
857
+ ImageInfo={
858
+ "Name": "Windows_Server-2019-English-Full-Base-2023.12.13",
859
+ "Description": "Microsoft Windows Server 2019 with Full Desktop Experience",
860
+ "RootDeviceType": "ebs",
861
+ "VirtualizationType": "hvm",
862
+ },
863
+ )
864
+
865
+ result = aws_integration.parse_ec2_instance(instance)
866
+
867
+ assert result.operating_system == regscale_models.AssetOperatingSystem.WindowsServer
868
+ assert result.asset_type == regscale_models.AssetType.VM
869
+ assert result.cpu == 2 # 1 core * 2 threads
870
+ assert result.disk_storage == 16 # 2 devices * 8GB each
871
+ assert result.fqdn == "i-0987654321fedcba0" # No DNS names, falls back to instance ID
872
+ assert "Windows" in result.description
873
+
874
+ def test_parse_ec2_instance_palo_alto(self, aws_integration):
875
+ """Test parsing a Palo Alto EC2 instance"""
876
+ instance = {
877
+ "InstanceId": "i-paloalto123456",
878
+ "InstanceType": "c5.large",
879
+ "State": "running",
880
+ "PrivateIpAddress": "10.0.1.102",
881
+ "Region": "us-east-1",
882
+ "CpuOptions": {"CoreCount": 2, "ThreadsPerCore": 1},
883
+ "BlockDeviceMappings": [{"DeviceName": "/dev/xvda", "Ebs": {"VolumeId": "vol-palo123"}}],
884
+ "ImageInfo": {
885
+ "Name": "pa-vm-aws-10.2.3-h4",
886
+ "Description": "Palo Alto Networks VM-Series Firewall",
887
+ "RootDeviceType": "ebs",
888
+ "VirtualizationType": "hvm",
889
+ },
890
+ }
891
+
892
+ result = aws_integration.parse_ec2_instance(instance)
893
+
894
+ assert result.operating_system == regscale_models.AssetOperatingSystem.PaloAlto
895
+ assert result.asset_type == regscale_models.AssetType.Appliance
896
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
897
+ assert result.component_type == regscale_models.ComponentType.Hardware
898
+ assert result.component_names == ["Palo Alto Networks IDPS"]
899
+ assert result.cpu == 2 # 2 cores * 1 thread
900
+ assert "Palo Alto Networks VM-Series Firewall" in result.os_version
901
+
902
+ def test_parse_ec2_instance_no_name_tag(self, aws_integration):
903
+ """Test parsing an EC2 instance without a Name tag"""
904
+ instance = {
905
+ "InstanceId": "i-noname123456",
906
+ "Tags": [{"Key": "Environment", "Value": "Development"}, {"Key": "Project", "Value": "TestProject"}],
907
+ "InstanceType": "t2.micro",
908
+ "State": "stopped",
909
+ "PrivateIpAddress": "10.0.1.103",
910
+ "Region": "us-east-1",
911
+ "CpuOptions": {"CoreCount": 1, "ThreadsPerCore": 1},
912
+ "BlockDeviceMappings": [],
913
+ "ImageInfo": {
914
+ "Name": "amzn2-ami-hvm-2.0.20231212.0-x86_64-gp2",
915
+ "Description": "Amazon Linux 2 AMI",
916
+ "RootDeviceType": "ebs",
917
+ "VirtualizationType": "hvm",
918
+ },
919
+ }
920
+
921
+ result = aws_integration.parse_ec2_instance(instance)
922
+
923
+ assert result.name == "i-noname123456"
924
+ assert result.status == regscale_models.AssetStatus.Inactive # stopped state
925
+ assert result.cpu == 1 # 1 core * 1 thread
926
+ assert result.disk_storage == 0 # No block devices
927
+
928
+ def test_parse_ec2_instance_no_tags(self, aws_integration):
929
+ """Test parsing an EC2 instance with no tags"""
930
+ instance = {
931
+ "InstanceId": "i-notags123456",
932
+ "InstanceType": "t3.nano",
933
+ "State": "running",
934
+ "Region": "us-east-1",
935
+ "CpuOptions": {"CoreCount": 2, "ThreadsPerCore": 1},
936
+ "BlockDeviceMappings": [{"DeviceName": "/dev/xvda", "Ebs": {"VolumeId": "vol-notags123"}}],
937
+ "ImageInfo": {
938
+ "Name": "amzn2-ami-hvm-2.0.20231212.0-x86_64-gp2",
939
+ "Description": "Amazon Linux 2 AMI",
940
+ "RootDeviceType": "ebs",
941
+ "VirtualizationType": "hvm",
942
+ },
943
+ }
944
+
945
+ result = aws_integration.parse_ec2_instance(instance)
946
+
947
+ assert result.name == "i-notags123456"
948
+ assert result.cpu == 2 # 2 cores * 1 thread
949
+ assert result.disk_storage == 8 # 1 device * 8GB
950
+
951
+ def test_parse_ec2_instance_public_facing(self, aws_integration):
952
+ """Test parsing a public-facing EC2 instance"""
953
+ instance = {
954
+ "InstanceId": "i-public123456",
955
+ "InstanceType": "t3.micro",
956
+ "State": "running",
957
+ "PrivateIpAddress": "10.0.1.104",
958
+ "PublicIpAddress": "54.1.2.3",
959
+ "PrivateDnsName": "ip-10-0-1-104.ec2.internal",
960
+ "PublicDnsName": "ec2-54-1-2-3.compute-1.amazonaws.com",
961
+ "Region": "us-east-1",
962
+ "CpuOptions": {"CoreCount": 2, "ThreadsPerCore": 1},
963
+ "BlockDeviceMappings": [],
964
+ "ImageInfo": {
965
+ "Name": "amzn2-ami-hvm-2.0.20231212.0-x86_64-gp2",
966
+ "Description": "Amazon Linux 2 AMI",
967
+ "RootDeviceType": "ebs",
968
+ "VirtualizationType": "hvm",
969
+ },
970
+ }
971
+
972
+ result = aws_integration.parse_ec2_instance(instance)
973
+
974
+ assert result.is_public_facing is True
975
+ assert result.ip_address == "10.0.1.104" # Prefers private IP
976
+ assert result.fqdn == "ec2-54-1-2-3.compute-1.amazonaws.com" # Prefers public DNS
977
+ assert "Public IP: 54.1.2.3" in result.notes
978
+
979
+ def test_parse_ec2_instance_private_only(self, aws_integration):
980
+ """Test parsing a private-only EC2 instance"""
981
+ instance = {
982
+ "InstanceId": "i-private123456",
983
+ "InstanceType": "t3.micro",
984
+ "State": "running",
985
+ "PrivateIpAddress": "10.0.1.105",
986
+ "PrivateDnsName": "ip-10-0-1-105.ec2.internal",
987
+ "Region": "us-east-1",
988
+ "CpuOptions": {"CoreCount": 2, "ThreadsPerCore": 1},
989
+ "BlockDeviceMappings": [],
990
+ "ImageInfo": {
991
+ "Name": "amzn2-ami-hvm-2.0.20231212.0-x86_64-gp2",
992
+ "Description": "Amazon Linux 2 AMI",
993
+ "RootDeviceType": "ebs",
994
+ "VirtualizationType": "hvm",
995
+ },
996
+ }
997
+
998
+ result = aws_integration.parse_ec2_instance(instance)
999
+
1000
+ assert result.is_public_facing is False
1001
+ assert result.ip_address == "10.0.1.105"
1002
+ assert result.fqdn == "ip-10-0-1-105.ec2.internal"
1003
+ assert "Public IP: N/A" in result.notes
1004
+
1005
+ def test_parse_ec2_instance_minimal_data(self, aws_integration):
1006
+ """Test parsing an EC2 instance with minimal data"""
1007
+ instance = {
1008
+ "InstanceId": "i-minimal123456",
1009
+ "InstanceType": "t3.micro",
1010
+ "State": "running",
1011
+ "Region": "us-east-1",
1012
+ }
1013
+
1014
+ result = aws_integration.parse_ec2_instance(instance)
1015
+
1016
+ assert result.name == "i-minimal123456"
1017
+ assert result.identifier == "arn:aws:ec2:us-east-1::instance/i-minimal123456"
1018
+ assert result.ip_address is None # No IP addresses provided
1019
+ assert result.fqdn == "i-minimal123456"
1020
+ assert result.cpu == 0 # No CPU options
1021
+ assert result.disk_storage == 0 # No block devices
1022
+ assert result.operating_system == regscale_models.AssetOperatingSystem.Linux # Default
1023
+ assert result.os_version == ""
1024
+ assert result.location == "us-east-1"
1025
+ assert result.model == "t3.micro"
1026
+ assert result.is_public_facing is False
1027
+ assert result.vlan_id is None # No subnet ID provided
1028
+ assert "Private IP: N/A" in result.notes
1029
+ assert "Public IP: N/A" in result.notes
1030
+
1031
+ def test_parse_ec2_instance_edge_cases(self, aws_integration):
1032
+ """Test parsing EC2 instance with edge cases"""
1033
+ instance = {
1034
+ "InstanceId": "i-edge123456",
1035
+ "InstanceType": "t3.micro",
1036
+ "State": "pending",
1037
+ "Region": "us-east-1",
1038
+ "CpuOptions": {}, # Empty CPU options
1039
+ "BlockDeviceMappings": [
1040
+ {"DeviceName": "/dev/xvda"}, # No Ebs field
1041
+ {"DeviceName": "/dev/sdb", "Ebs": {"VolumeId": "vol-edge123"}},
1042
+ ],
1043
+ "ImageInfo": {
1044
+ "Name": "custom-ami-123",
1045
+ "Description": "",
1046
+ "RootDeviceType": "ebs",
1047
+ "VirtualizationType": "hvm",
1048
+ },
1049
+ }
1050
+
1051
+ result = aws_integration.parse_ec2_instance(instance)
1052
+
1053
+ assert result.cpu == 0 # Empty CPU options
1054
+ assert result.disk_storage == 8 # Only one Ebs device
1055
+ assert result.status == regscale_models.AssetStatus.Inactive # pending state
1056
+ assert result.os_version == ""
1057
+ assert result.operating_system == regscale_models.AssetOperatingSystem.Linux # Default
1058
+
1059
+ def test_parse_lambda_function_basic(self, mock_aws_integration):
1060
+ """Test parsing a basic Lambda function"""
1061
+ function = {
1062
+ "FunctionName": "test-function",
1063
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:test-function",
1064
+ "Runtime": "python3.9",
1065
+ "Handler": "index.handler",
1066
+ "MemorySize": 128,
1067
+ "Timeout": 30,
1068
+ "Region": "us-east-1",
1069
+ }
1070
+
1071
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function)
1072
+
1073
+ assert result.name == "test-function"
1074
+ assert result.identifier == "arn:aws:lambda:us-east-1:123456789012:function:test-function"
1075
+ assert result.asset_type == regscale_models.AssetType.Other
1076
+ assert result.asset_category == regscale_models.AssetCategory.Software
1077
+ assert result.component_type == regscale_models.ComponentType.Software
1078
+ assert result.component_names == ["Lambda Functions"]
1079
+ assert result.parent_id == mock_aws_integration.plan_id
1080
+ assert result.parent_module == "securityplans"
1081
+ assert result.status == regscale_models.AssetStatus.Active
1082
+ assert result.location == "us-east-1"
1083
+ assert result.software_name == "python3.9"
1084
+ assert result.software_version == "9" # The method uses split(".")[-1] to get last part
1085
+ assert result.ram == 128
1086
+ assert result.external_id == "test-function"
1087
+ assert result.aws_identifier == "arn:aws:lambda:us-east-1:123456789012:function:test-function"
1088
+ assert result.manufacturer == "AWS"
1089
+ assert result.is_virtual is True
1090
+ assert result.source_data == function
1091
+
1092
+ assert "AWS Lambda function test-function" in result.description
1093
+ assert "python3.9" in result.description
1094
+ assert "128MB memory" in result.description
1095
+ assert "Function Name: test-function" in result.notes
1096
+ assert "Runtime: python3.9" in result.notes
1097
+ assert "Memory Size: 128 MB" in result.notes
1098
+ assert "Timeout: 30 seconds" in result.notes
1099
+ assert "Handler: index.handler" in result.notes
1100
+
1101
+ def test_parse_lambda_function_with_description(self, mock_aws_integration):
1102
+ """Test parsing a Lambda function with description"""
1103
+ function = {
1104
+ "FunctionName": "api-gateway-function",
1105
+ "FunctionArn": "arn:aws:lambda:us-west-2:123456789012:function:api-gateway-function",
1106
+ "Runtime": "nodejs18.x",
1107
+ "Handler": "app.handler",
1108
+ "MemorySize": 256,
1109
+ "Timeout": 60,
1110
+ "Description": "API Gateway integration function for user authentication",
1111
+ "Region": "us-west-2",
1112
+ }
1113
+
1114
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function)
1115
+
1116
+ assert "API Gateway integration function for user authentication" in result.description
1117
+ assert "Function description: API Gateway integration function for user authentication" in result.description
1118
+ assert "Description: API Gateway integration function for user authentication" in result.notes
1119
+ assert result.software_name == "nodejs18.x"
1120
+ assert result.software_version == "x" # The method uses split(".")[-1] to get last part
1121
+ assert result.ram == 256
1122
+
1123
+ def test_parse_lambda_function_with_function_url(self, mock_aws_integration):
1124
+ """Test parsing a Lambda function with FunctionUrl"""
1125
+ function = {
1126
+ "FunctionName": "webhook-function",
1127
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:webhook-function",
1128
+ "Runtime": "python3.11",
1129
+ "Handler": "lambda_function.lambda_handler",
1130
+ "MemorySize": 512,
1131
+ "Timeout": 120,
1132
+ "FunctionUrl": "https://abc123.lambda-url.us-east-1.on.aws/",
1133
+ "Region": "us-east-1",
1134
+ }
1135
+
1136
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function)
1137
+
1138
+ assert result.uri == "https://abc123.lambda-url.us-east-1.on.aws/"
1139
+ assert result.software_name == "python3.11"
1140
+ assert result.software_version == "11" # The method uses split(".")[-1] to get last part
1141
+ assert result.ram == 512
1142
+
1143
+ @pytest.mark.parametrize(
1144
+ "function_data,expected_software_name,expected_software_version,expected_ram",
1145
+ [
1146
+ (
1147
+ {
1148
+ "FunctionName": "simple-function",
1149
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:simple-function",
1150
+ "Runtime": "python3.8",
1151
+ "Handler": "main.handler",
1152
+ "MemorySize": 64,
1153
+ "Timeout": 15,
1154
+ "Region": "us-east-1",
1155
+ },
1156
+ "python3.8",
1157
+ "8", # The method uses split(".")[-1] to get last part
1158
+ 64,
1159
+ ),
1160
+ (
1161
+ {
1162
+ "FunctionName": "empty-desc-function",
1163
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:empty-desc-function",
1164
+ "Runtime": "java11",
1165
+ "Handler": "com.example.Handler::handleRequest",
1166
+ "MemorySize": 1024,
1167
+ "Timeout": 300,
1168
+ "Description": "",
1169
+ "Region": "us-east-1",
1170
+ },
1171
+ "java11",
1172
+ "java11", # No dots in java11, so full string is used
1173
+ 1024,
1174
+ ),
1175
+ (
1176
+ {
1177
+ "FunctionName": "non-string-desc-function",
1178
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:non-string-desc-function",
1179
+ "Runtime": "dotnet6",
1180
+ "Handler": "MyFunction::FunctionHandler",
1181
+ "MemorySize": 256,
1182
+ "Timeout": 60,
1183
+ "Description": None, # Non-string description
1184
+ "Region": "us-east-1",
1185
+ },
1186
+ "dotnet6",
1187
+ "dotnet6", # No dots in dotnet6, so full string is used
1188
+ 256,
1189
+ ),
1190
+ ],
1191
+ ids=["no_description", "empty_description", "non_string_description"],
1192
+ )
1193
+ def test_parse_lambda_function_description_variations(
1194
+ self, function_data, expected_software_name, expected_software_version, expected_ram, mock_aws_integration
1195
+ ):
1196
+ """Test parsing Lambda functions with various description scenarios."""
1197
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function_data)
1198
+
1199
+ assert "Function description:" not in result.description
1200
+ assert "Description: " in result.notes
1201
+ assert result.software_name == expected_software_name
1202
+ assert result.software_version == expected_software_version
1203
+ assert result.ram == expected_ram
1204
+
1205
+ @pytest.mark.parametrize(
1206
+ "function_data,expected_software_name,expected_software_version,expected_description_contains,expected_notes_contains",
1207
+ [
1208
+ (
1209
+ {
1210
+ "FunctionName": "no-runtime-function",
1211
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:no-runtime-function",
1212
+ "Handler": "index.handler",
1213
+ "MemorySize": 128,
1214
+ "Timeout": 30,
1215
+ "Region": "us-east-1",
1216
+ },
1217
+ None,
1218
+ None,
1219
+ "unknown runtime",
1220
+ "Runtime: unknown",
1221
+ ),
1222
+ (
1223
+ {
1224
+ "FunctionName": "empty-runtime-function",
1225
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:empty-runtime-function",
1226
+ "Runtime": "",
1227
+ "Handler": "index.handler",
1228
+ "MemorySize": 128,
1229
+ "Timeout": 30,
1230
+ "Region": "us-east-1",
1231
+ },
1232
+ "",
1233
+ None,
1234
+ "running with 128MB memory", # Empty runtime shows as empty space
1235
+ "Runtime: ", # Empty runtime shows as empty in notes
1236
+ ),
1237
+ ],
1238
+ ids=["no_runtime", "empty_runtime"],
1239
+ )
1240
+ def test_parse_lambda_function_runtime_variations(
1241
+ self,
1242
+ function_data,
1243
+ expected_software_name,
1244
+ expected_software_version,
1245
+ expected_description_contains,
1246
+ expected_notes_contains,
1247
+ mock_aws_integration,
1248
+ ):
1249
+ """Test parsing Lambda functions with various runtime scenarios."""
1250
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function_data)
1251
+
1252
+ assert result.software_name == expected_software_name
1253
+ assert result.software_version == expected_software_version
1254
+ assert expected_description_contains in result.description
1255
+ assert expected_notes_contains in result.notes
1256
+
1257
+ def test_parse_lambda_function_minimal_data(self, mock_aws_integration):
1258
+ """Test parsing a Lambda function with minimal data"""
1259
+ function = {"FunctionName": "minimal-function"}
1260
+
1261
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function)
1262
+
1263
+ assert result.name == "minimal-function"
1264
+ assert result.identifier == ""
1265
+ assert result.asset_type == regscale_models.AssetType.Other
1266
+ assert result.asset_category == regscale_models.AssetCategory.Software
1267
+ assert result.component_type == regscale_models.ComponentType.Software
1268
+ assert result.component_names == ["Lambda Functions"]
1269
+ assert result.status == regscale_models.AssetStatus.Active
1270
+ assert result.location is None
1271
+ assert result.software_name is None
1272
+ assert result.software_version is None
1273
+ assert result.ram is None
1274
+ assert result.external_id == "minimal-function"
1275
+ assert result.aws_identifier is None
1276
+ assert result.uri is None
1277
+ assert result.manufacturer == "AWS"
1278
+ assert result.is_virtual is True
1279
+ assert result.source_data == function
1280
+
1281
+ assert "AWS Lambda function minimal-function" in result.description
1282
+ assert "unknown runtime" in result.description
1283
+ assert "0MB memory" in result.description
1284
+ assert "Function Name: minimal-function" in result.notes
1285
+ assert "Runtime: unknown" in result.notes
1286
+ assert "Memory Size: 0 MB" in result.notes
1287
+ assert "Timeout: 0 seconds" in result.notes
1288
+ assert "Handler: " in result.notes
1289
+ assert "Description: " in result.notes
1290
+
1291
+ def test_parse_lambda_function_edge_cases(self, mock_aws_integration):
1292
+ """Test parsing Lambda function with edge cases"""
1293
+ function = {
1294
+ "FunctionName": "edge-case-function",
1295
+ "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:edge-case-function",
1296
+ "Runtime": "python3.12.1", # Runtime with multiple dots
1297
+ "Handler": "", # Empty handler
1298
+ "MemorySize": 0, # Zero memory
1299
+ "Timeout": 0, # Zero timeout
1300
+ "Description": " ", # Whitespace-only description
1301
+ "Region": "us-east-1",
1302
+ }
1303
+
1304
+ result = AWSInventoryIntegration.parse_lambda_function(mock_aws_integration, function)
1305
+
1306
+ assert result.software_name == "python3.12.1"
1307
+ assert result.software_version == "1" # The method uses split(".")[-1] to get last part
1308
+ assert result.ram == 0
1309
+ assert "0MB memory" in result.description
1310
+ assert "Memory Size: 0 MB" in result.notes
1311
+ assert "Timeout: 0 seconds" in result.notes
1312
+ assert "Handler: " in result.notes
1313
+
1314
+ assert "Function description: " in result.description
1315
+
1316
+ def test_parse_aws_account_basic(self, mock_aws_integration):
1317
+ """Test parsing a basic AWS account with IAM ARN"""
1318
+ iam = {
1319
+ "Arn": "arn:aws:iam::123456789012:user/test-user",
1320
+ "UserName": "test-user",
1321
+ "Path": "/",
1322
+ "CreateDate": "2023-01-01T00:00:00Z",
1323
+ }
1324
+
1325
+ result = AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1326
+
1327
+ assert result.name == "123456789012"
1328
+ assert result.identifier == "AWS::::Account:123456789012"
1329
+ assert result.asset_type == regscale_models.AssetType.Other
1330
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1331
+ assert result.component_type == regscale_models.ComponentType.Software
1332
+ assert result.component_names == ["AWS Account"]
1333
+ assert result.parent_id == mock_aws_integration.plan_id
1334
+ assert result.parent_module == "securityplans"
1335
+ assert result.status == regscale_models.AssetStatus.Active
1336
+ assert result.location == "Unknown"
1337
+ assert result.external_id == "123456789012"
1338
+ assert result.aws_identifier == "AWS::::Account:123456789012"
1339
+ assert result.manufacturer == "AWS"
1340
+ assert result.source_data == iam
1341
+
1342
+ def test_parse_aws_account_role_arn(self, mock_aws_integration):
1343
+ """Test parsing AWS account from role ARN"""
1344
+ iam = {
1345
+ "Arn": "arn:aws:iam::987654321098:role/EC2Role",
1346
+ "RoleName": "EC2Role",
1347
+ "Path": "/",
1348
+ "CreateDate": "2023-01-01T00:00:00Z",
1349
+ }
1350
+
1351
+ result = AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1352
+
1353
+ assert result.name == "987654321098"
1354
+ assert result.identifier == "AWS::::Account:987654321098"
1355
+ assert result.external_id == "987654321098"
1356
+ assert result.aws_identifier == "AWS::::Account:987654321098"
1357
+
1358
+ def test_parse_aws_account_group_arn(self, mock_aws_integration):
1359
+ """Test parsing AWS account from group ARN"""
1360
+ iam = {
1361
+ "Arn": "arn:aws:iam::555666777888:group/Developers",
1362
+ "GroupName": "Developers",
1363
+ "Path": "/",
1364
+ "CreateDate": "2023-01-01T00:00:00Z",
1365
+ }
1366
+
1367
+ result = AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1368
+
1369
+ assert result.name == "555666777888"
1370
+ assert result.identifier == "AWS::::Account:555666777888"
1371
+ assert result.external_id == "555666777888"
1372
+ assert result.aws_identifier == "AWS::::Account:555666777888"
1373
+
1374
+ def test_parse_aws_account_policy_arn(self, mock_aws_integration):
1375
+ """Test parsing AWS account from policy ARN"""
1376
+ iam = {
1377
+ "Arn": "arn:aws:iam::111222333444:policy/AdminPolicy",
1378
+ "PolicyName": "AdminPolicy",
1379
+ "Path": "/",
1380
+ "CreateDate": "2023-01-01T00:00:00Z",
1381
+ }
1382
+
1383
+ result = AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1384
+
1385
+ assert result.name == "111222333444"
1386
+ assert result.identifier == "AWS::::Account:111222333444"
1387
+ assert result.external_id == "111222333444"
1388
+ assert result.aws_identifier == "AWS::::Account:111222333444"
1389
+
1390
+ def test_parse_aws_account_no_arn(self, mock_aws_integration):
1391
+ """Test parsing AWS account with no ARN"""
1392
+ iam = {"UserName": "test-user", "Path": "/", "CreateDate": "2023-01-01T00:00:00Z"}
1393
+
1394
+ with pytest.raises(IndexError):
1395
+ AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1396
+
1397
+ def test_parse_aws_account_empty_arn(self, mock_aws_integration):
1398
+ """Test parsing AWS account with empty ARN"""
1399
+ iam = {"Arn": "", "UserName": "test-user", "Path": "/", "CreateDate": "2023-01-01T00:00:00Z"}
1400
+
1401
+ with pytest.raises(IndexError):
1402
+ AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1403
+
1404
+ def test_parse_aws_account_invalid_arn_format(self, mock_aws_integration):
1405
+ """Test parsing AWS account with invalid ARN format"""
1406
+ iam = {"Arn": "invalid:arn:format", "UserName": "test-user", "Path": "/", "CreateDate": "2023-01-01T00:00:00Z"}
1407
+
1408
+ with pytest.raises(IndexError):
1409
+ AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1410
+
1411
+ def test_parse_aws_account_short_arn(self, mock_aws_integration):
1412
+ """Test parsing AWS account with ARN that has fewer than 5 parts"""
1413
+ iam = {
1414
+ "Arn": "arn:aws:iam::123456789012", # Only 4 parts
1415
+ "UserName": "test-user",
1416
+ "Path": "/",
1417
+ "CreateDate": "2023-01-01T00:00:00Z",
1418
+ }
1419
+
1420
+ result = AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1421
+
1422
+ assert result.name == "123456789012" # This is what split(":")[4] would return
1423
+ assert result.identifier == "AWS::::Account:123456789012"
1424
+ assert result.external_id == "123456789012"
1425
+ assert result.aws_identifier == "AWS::::Account:123456789012"
1426
+
1427
+ def test_parse_aws_account_minimal_data(self, mock_aws_integration):
1428
+ """Test parsing AWS account with minimal IAM data"""
1429
+ iam = {}
1430
+
1431
+ with pytest.raises(IndexError):
1432
+ AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1433
+
1434
+ def test_parse_aws_account_edge_cases(self, mock_aws_integration):
1435
+ """Test parsing AWS account with edge cases"""
1436
+ iam = {
1437
+ "Arn": "arn:aws:iam::000000000000:user/root", # Root account
1438
+ "UserName": "root",
1439
+ "Path": "/",
1440
+ "CreateDate": "2023-01-01T00:00:00Z",
1441
+ }
1442
+
1443
+ result = AWSInventoryIntegration.parse_aws_account(mock_aws_integration, iam)
1444
+
1445
+ assert result.name == "000000000000" # Root account ID
1446
+ assert result.identifier == "AWS::::Account:000000000000"
1447
+ assert result.external_id == "000000000000"
1448
+ assert result.aws_identifier == "AWS::::Account:000000000000"
1449
+ assert result.asset_type == regscale_models.AssetType.Other
1450
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1451
+ assert result.component_type == regscale_models.ComponentType.Software
1452
+ assert result.component_names == ["AWS Account"]
1453
+ assert result.status == regscale_models.AssetStatus.Active
1454
+ assert result.location == "Unknown"
1455
+ assert result.manufacturer == "AWS"
1456
+ assert result.source_data == iam
1457
+
1458
+ def test_parse_s3_bucket_basic(self, mock_aws_integration):
1459
+ """Test parsing a basic S3 bucket"""
1460
+ bucket = {"Name": "my-test-bucket", "Region": "us-east-1", "CreationDate": "2023-01-01T00:00:00Z"}
1461
+
1462
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket)
1463
+
1464
+ assert result.name == "my-test-bucket"
1465
+ assert result.identifier == "arn:aws:s3:::my-test-bucket"
1466
+ assert result.asset_type == regscale_models.AssetType.Other
1467
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1468
+ assert result.component_type == regscale_models.ComponentType.Hardware
1469
+ assert result.component_names == ["S3 Buckets"]
1470
+ assert result.parent_id == mock_aws_integration.plan_id
1471
+ assert result.parent_module == "securityplans"
1472
+ assert result.status == regscale_models.AssetStatus.Active
1473
+ assert result.location == "us-east-1"
1474
+ assert result.external_id == "my-test-bucket"
1475
+ assert result.aws_identifier == "arn:aws:s3:::my-test-bucket"
1476
+ assert result.uri == "https://my-test-bucket.s3.amazonaws.com"
1477
+ assert result.manufacturer == "AWS"
1478
+ assert result.is_public_facing is False
1479
+ assert result.source_data == bucket
1480
+
1481
+ def test_parse_s3_bucket_public_facing(self, mock_aws_integration):
1482
+ """Test parsing a public-facing S3 bucket"""
1483
+ bucket = {
1484
+ "Name": "public-bucket",
1485
+ "Region": "us-west-2",
1486
+ "CreationDate": "2023-01-01T00:00:00Z",
1487
+ "Grants": [
1488
+ {"Grantee": {"URI": "http://acs.amazonaws.com/groups/global/AllUsers"}, "Permission": "READ"},
1489
+ {"Grantee": {"ID": "123456789012"}, "Permission": "FULL_CONTROL"},
1490
+ ],
1491
+ }
1492
+
1493
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket)
1494
+
1495
+ assert result.name == "public-bucket"
1496
+ assert result.is_public_facing is True
1497
+ assert result.aws_identifier == "arn:aws:s3:::public-bucket"
1498
+ assert result.uri == "https://public-bucket.s3.amazonaws.com"
1499
+ assert result.location == "us-west-2"
1500
+
1501
+ def test_parse_s3_bucket_private_with_grants(self, mock_aws_integration):
1502
+ """Test parsing a private S3 bucket with grants but no public access"""
1503
+
1504
+ bucket = {
1505
+ "Name": "private-bucket",
1506
+ "Region": "us-east-1",
1507
+ "CreationDate": "2023-01-01T00:00:00Z",
1508
+ "Grants": [
1509
+ {"Grantee": {"ID": "123456789012"}, "Permission": "FULL_CONTROL"},
1510
+ {"Grantee": {"URI": "http://acs.amazonaws.com/groups/global/AuthenticatedUsers"}, "Permission": "READ"},
1511
+ ],
1512
+ }
1513
+
1514
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket)
1515
+
1516
+ assert result.name == "private-bucket"
1517
+ assert result.is_public_facing is False # Not AllUsers, so private
1518
+ assert result.aws_identifier == "arn:aws:s3:::private-bucket"
1519
+ assert result.uri == "https://private-bucket.s3.amazonaws.com"
1520
+
1521
+ @pytest.mark.parametrize(
1522
+ "bucket_data,expected_name,expected_aws_identifier,expected_uri",
1523
+ [
1524
+ (
1525
+ {"Name": "no-grants-bucket", "Region": "us-east-1", "CreationDate": "2023-01-01T00:00:00Z"},
1526
+ "no-grants-bucket",
1527
+ "arn:aws:s3:::no-grants-bucket",
1528
+ "https://no-grants-bucket.s3.amazonaws.com",
1529
+ ),
1530
+ (
1531
+ {
1532
+ "Name": "empty-grants-bucket",
1533
+ "Region": "us-east-1",
1534
+ "CreationDate": "2023-01-01T00:00:00Z",
1535
+ "Grants": [],
1536
+ },
1537
+ "empty-grants-bucket",
1538
+ "arn:aws:s3:::empty-grants-bucket",
1539
+ "https://empty-grants-bucket.s3.amazonaws.com",
1540
+ ),
1541
+ ],
1542
+ ids=["no_grants", "empty_grants"],
1543
+ )
1544
+ def test_parse_s3_bucket_grants_variations(
1545
+ self, bucket_data, expected_name, expected_aws_identifier, expected_uri, mock_aws_integration
1546
+ ):
1547
+ """Test parsing S3 buckets with various grants scenarios."""
1548
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket_data)
1549
+
1550
+ assert result.name == expected_name
1551
+ assert result.is_public_facing is False # No/empty grants means private
1552
+ assert result.aws_identifier == expected_aws_identifier
1553
+ assert result.uri == expected_uri
1554
+
1555
+ @pytest.mark.parametrize(
1556
+ "bucket_data,expected_name,expected_identifier,expected_external_id,expected_aws_identifier,expected_uri",
1557
+ [
1558
+ (
1559
+ {"Region": "us-east-1", "CreationDate": "2023-01-01T00:00:00Z"},
1560
+ "",
1561
+ "arn:aws:s3:::None",
1562
+ None,
1563
+ "arn:aws:s3:::None", # bucket.get('Name') returns None
1564
+ "https://None.s3.amazonaws.com", # bucket.get('Name') returns None
1565
+ ),
1566
+ (
1567
+ {"Name": "", "Region": "us-east-1", "CreationDate": "2023-01-01T00:00:00Z"},
1568
+ "",
1569
+ "arn:aws:s3:::",
1570
+ "",
1571
+ "arn:aws:s3:::",
1572
+ "https://.s3.amazonaws.com",
1573
+ ),
1574
+ ],
1575
+ ids=["no_name", "empty_name"],
1576
+ )
1577
+ def test_parse_s3_bucket_name_variations(
1578
+ self,
1579
+ bucket_data,
1580
+ expected_name,
1581
+ expected_identifier,
1582
+ expected_external_id,
1583
+ expected_aws_identifier,
1584
+ expected_uri,
1585
+ mock_aws_integration,
1586
+ ):
1587
+ """Test parsing S3 buckets with various name scenarios."""
1588
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket_data)
1589
+
1590
+ assert result.name == expected_name
1591
+ assert result.identifier == expected_identifier
1592
+ assert result.external_id == expected_external_id
1593
+ assert result.aws_identifier == expected_aws_identifier
1594
+ assert result.uri == expected_uri
1595
+ assert result.is_public_facing is False
1596
+
1597
+ def test_parse_s3_bucket_no_region(self, mock_aws_integration):
1598
+ """Test parsing an S3 bucket without region"""
1599
+
1600
+ bucket = {"Name": "no-region-bucket", "CreationDate": "2023-01-01T00:00:00Z"}
1601
+
1602
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket)
1603
+
1604
+ assert result.name == "no-region-bucket"
1605
+ assert result.location is None
1606
+ assert result.aws_identifier == "arn:aws:s3:::no-region-bucket"
1607
+ assert result.uri == "https://no-region-bucket.s3.amazonaws.com"
1608
+ assert result.is_public_facing is False
1609
+
1610
+ def test_parse_s3_bucket_minimal_data(self, mock_aws_integration):
1611
+ """Test parsing an S3 bucket with minimal data"""
1612
+
1613
+ bucket = {}
1614
+
1615
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket)
1616
+
1617
+ assert result.name == ""
1618
+ assert result.identifier == "arn:aws:s3:::None"
1619
+ assert result.asset_type == regscale_models.AssetType.Other
1620
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1621
+ assert result.component_type == regscale_models.ComponentType.Hardware
1622
+ assert result.component_names == ["S3 Buckets"]
1623
+ assert result.parent_id == mock_aws_integration.plan_id
1624
+ assert result.parent_module == "securityplans"
1625
+ assert result.status == regscale_models.AssetStatus.Active
1626
+ assert result.location is None
1627
+ assert result.external_id is None
1628
+ assert result.aws_identifier == "arn:aws:s3:::None" # bucket.get('Name') returns None
1629
+ assert result.uri == "https://None.s3.amazonaws.com" # bucket.get('Name') returns None
1630
+ assert result.manufacturer == "AWS"
1631
+ assert result.is_public_facing is False
1632
+ assert result.source_data == bucket
1633
+
1634
+ def test_parse_s3_bucket_edge_cases(self, mock_aws_integration):
1635
+ """Test parsing S3 bucket with edge cases"""
1636
+
1637
+ bucket = {
1638
+ "Name": "edge-case-bucket",
1639
+ "Region": "us-east-1",
1640
+ "Grants": [
1641
+ {
1642
+ "Grantee": {"URI": "http://acs.amazonaws.com/groups/global/AllUsers"},
1643
+ "Permission": "WRITE", # Different permission
1644
+ },
1645
+ {
1646
+ "Grantee": {"URI": "http://acs.amazonaws.com/groups/global/AllUsers"},
1647
+ "Permission": "READ_ACP", # Another permission
1648
+ },
1649
+ ],
1650
+ }
1651
+
1652
+ result = AWSInventoryIntegration.parse_s3_bucket(mock_aws_integration, bucket)
1653
+
1654
+ assert result.name == "edge-case-bucket"
1655
+ assert result.is_public_facing is True # Should detect AllUsers regardless of permission
1656
+ assert result.aws_identifier == "arn:aws:s3:::edge-case-bucket"
1657
+ assert result.uri == "https://edge-case-bucket.s3.amazonaws.com"
1658
+ assert result.location == "us-east-1"
1659
+ assert result.asset_type == regscale_models.AssetType.Other
1660
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1661
+ assert result.component_type == regscale_models.ComponentType.Hardware
1662
+ assert result.component_names == ["S3 Buckets"]
1663
+ assert result.status == regscale_models.AssetStatus.Active
1664
+ assert result.manufacturer == "AWS"
1665
+ assert result.source_data == bucket
1666
+
1667
+ def test_parse_rds_instance_basic(self, mock_aws_integration):
1668
+ """Test parsing a basic RDS instance"""
1669
+
1670
+ db = {
1671
+ "DBInstanceIdentifier": "test-db-instance",
1672
+ "DBInstanceClass": "db.t3.micro",
1673
+ "Engine": "mysql",
1674
+ "EngineVersion": "8.0.28",
1675
+ "DBInstanceStatus": "available",
1676
+ "AllocatedStorage": 20,
1677
+ "AvailabilityZone": "us-east-1a",
1678
+ "VpcId": "vpc-12345678",
1679
+ "PubliclyAccessible": False,
1680
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:test-db-instance",
1681
+ "Endpoint": {"Address": "test-db-instance.abc123.us-east-1.rds.amazonaws.com", "Port": 3306},
1682
+ }
1683
+
1684
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db)
1685
+
1686
+ assert result.name == "test-db-instance 8.0.28) - db.t3.micro"
1687
+ assert result.identifier == "arn:aws:rds:us-east-1:123456789012:db:test-db-instance"
1688
+ assert result.asset_type == regscale_models.AssetType.VM
1689
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1690
+ assert result.component_type == regscale_models.ComponentType.Hardware
1691
+ assert result.component_names == ["RDS Instances"]
1692
+ assert result.parent_id == mock_aws_integration.plan_id
1693
+ assert result.parent_module == "securityplans"
1694
+ assert result.fqdn == "test-db-instance.abc123.us-east-1.rds.amazonaws.com"
1695
+ assert result.vlan_id == "vpc-12345678"
1696
+ assert result.status == regscale_models.AssetStatus.Active
1697
+ assert result.location == "us-east-1a"
1698
+ assert result.model == "db.t3.micro"
1699
+ assert result.manufacturer == "AWS"
1700
+ assert result.disk_storage == 20
1701
+ assert result.software_name == "mysql"
1702
+ assert result.software_version == "8.0.28"
1703
+ assert result.external_id == "test-db-instance"
1704
+ assert result.aws_identifier == "arn:aws:rds:us-east-1:123456789012:db:test-db-instance"
1705
+ assert result.is_public_facing is False
1706
+ assert result.source_data == db
1707
+
1708
+ def test_parse_rds_instance_no_engine_version(self, mock_aws_integration):
1709
+ """Test parsing RDS instance without engine version"""
1710
+
1711
+ db = {
1712
+ "DBInstanceIdentifier": "simple-db-instance",
1713
+ "DBInstanceClass": "db.r5.large",
1714
+ "Engine": "postgres",
1715
+ "DBInstanceStatus": "available",
1716
+ "AllocatedStorage": 100,
1717
+ "AvailabilityZone": "us-west-2a",
1718
+ "VpcId": "vpc-87654321",
1719
+ "PubliclyAccessible": True,
1720
+ "DBInstanceArn": "arn:aws:rds:us-west-2:123456789012:db:simple-db-instance",
1721
+ "Endpoint": {"Address": "simple-db-instance.def456.us-west-2.rds.amazonaws.com", "Port": 5432},
1722
+ }
1723
+
1724
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db)
1725
+
1726
+ assert result.name == "simple-db-instance) - db.r5.large"
1727
+ assert result.software_version is None
1728
+ assert result.is_public_facing is True
1729
+ assert result.fqdn == "simple-db-instance.def456.us-west-2.rds.amazonaws.com"
1730
+ assert result.vlan_id == "vpc-87654321"
1731
+ assert result.location == "us-west-2a"
1732
+
1733
+ def test_parse_rds_instance_no_instance_class(self, mock_aws_integration):
1734
+ """Test parsing RDS instance without instance class"""
1735
+
1736
+ db = {
1737
+ "DBInstanceIdentifier": "no-class-db-instance",
1738
+ "Engine": "mariadb",
1739
+ "EngineVersion": "10.6.8",
1740
+ "DBInstanceStatus": "available",
1741
+ "AllocatedStorage": 50,
1742
+ "AvailabilityZone": "us-east-1b",
1743
+ "VpcId": "vpc-11111111",
1744
+ "PubliclyAccessible": False,
1745
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:no-class-db-instance",
1746
+ "Endpoint": {"Address": "no-class-db-instance.ghi789.us-east-1.rds.amazonaws.com", "Port": 3306},
1747
+ }
1748
+
1749
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db)
1750
+
1751
+ assert result.name == "no-class-db-instance 10.6.8) - "
1752
+ assert result.model is None
1753
+ assert result.software_name == "mariadb"
1754
+ assert result.software_version == "10.6.8"
1755
+
1756
+ def test_parse_rds_instance_inactive_status(self, mock_aws_integration):
1757
+ """Test parsing RDS instance with inactive status"""
1758
+
1759
+ db = {
1760
+ "DBInstanceIdentifier": "inactive-db-instance",
1761
+ "DBInstanceClass": "db.t3.small",
1762
+ "Engine": "mysql",
1763
+ "EngineVersion": "8.0.28",
1764
+ "DBInstanceStatus": "stopped",
1765
+ "AllocatedStorage": 30,
1766
+ "AvailabilityZone": "us-east-1c",
1767
+ "VpcId": "vpc-22222222",
1768
+ "PubliclyAccessible": False,
1769
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:inactive-db-instance",
1770
+ }
1771
+
1772
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db)
1773
+
1774
+ assert result.status == regscale_models.AssetStatus.Inactive
1775
+ assert result.name == "inactive-db-instance 8.0.28) - db.t3.small"
1776
+
1777
+ @pytest.mark.parametrize(
1778
+ "db_data,expected_fqdn,expected_status,expected_name",
1779
+ [
1780
+ (
1781
+ {
1782
+ "DBInstanceIdentifier": "no-endpoint-db-instance",
1783
+ "DBInstanceClass": "db.t3.micro",
1784
+ "Engine": "mysql",
1785
+ "EngineVersion": "8.0.28",
1786
+ "DBInstanceStatus": "creating",
1787
+ "AllocatedStorage": 20,
1788
+ "AvailabilityZone": "us-east-1a",
1789
+ "VpcId": "vpc-33333333",
1790
+ "PubliclyAccessible": False,
1791
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:no-endpoint-db-instance",
1792
+ },
1793
+ None,
1794
+ regscale_models.AssetStatus.Inactive, # creating status
1795
+ "no-endpoint-db-instance 8.0.28) - db.t3.micro",
1796
+ ),
1797
+ (
1798
+ {
1799
+ "DBInstanceIdentifier": "empty-endpoint-db-instance",
1800
+ "DBInstanceClass": "db.t3.micro",
1801
+ "Engine": "mysql",
1802
+ "EngineVersion": "8.0.28",
1803
+ "DBInstanceStatus": "available",
1804
+ "AllocatedStorage": 20,
1805
+ "AvailabilityZone": "us-east-1a",
1806
+ "VpcId": "vpc-44444444",
1807
+ "PubliclyAccessible": False,
1808
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:empty-endpoint-db-instance",
1809
+ "Endpoint": {},
1810
+ },
1811
+ None,
1812
+ regscale_models.AssetStatus.Active,
1813
+ "empty-endpoint-db-instance 8.0.28) - db.t3.micro",
1814
+ ),
1815
+ ],
1816
+ ids=["no_endpoint", "empty_endpoint"],
1817
+ )
1818
+ def test_parse_rds_instance_endpoint_variations(
1819
+ self, db_data, expected_fqdn, expected_status, expected_name, mock_aws_integration
1820
+ ):
1821
+ """Test parsing RDS instances with various endpoint scenarios."""
1822
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db_data)
1823
+
1824
+ assert result.fqdn == expected_fqdn
1825
+ assert result.status == expected_status
1826
+ assert result.name == expected_name
1827
+
1828
+ @pytest.mark.parametrize(
1829
+ "db_data,expected_vlan_id,expected_location,expected_fqdn",
1830
+ [
1831
+ (
1832
+ {
1833
+ "DBInstanceIdentifier": "no-vpc-db-instance",
1834
+ "DBInstanceClass": "db.t3.micro",
1835
+ "Engine": "mysql",
1836
+ "EngineVersion": "8.0.28",
1837
+ "DBInstanceStatus": "available",
1838
+ "AllocatedStorage": 20,
1839
+ "AvailabilityZone": "us-east-1a",
1840
+ "PubliclyAccessible": False,
1841
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:no-vpc-db-instance",
1842
+ "Endpoint": {"Address": "no-vpc-db-instance.jkl012.us-east-1.rds.amazonaws.com", "Port": 3306},
1843
+ },
1844
+ None,
1845
+ "us-east-1a",
1846
+ "no-vpc-db-instance.jkl012.us-east-1.rds.amazonaws.com",
1847
+ ),
1848
+ (
1849
+ {
1850
+ "DBInstanceIdentifier": "no-az-db-instance",
1851
+ "DBInstanceClass": "db.t3.micro",
1852
+ "Engine": "mysql",
1853
+ "EngineVersion": "8.0.28",
1854
+ "DBInstanceStatus": "available",
1855
+ "AllocatedStorage": 20,
1856
+ "VpcId": "vpc-55555555",
1857
+ "PubliclyAccessible": False,
1858
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:no-az-db-instance",
1859
+ "Endpoint": {"Address": "no-az-db-instance.mno345.us-east-1.rds.amazonaws.com", "Port": 3306},
1860
+ },
1861
+ "vpc-55555555",
1862
+ None,
1863
+ "no-az-db-instance.mno345.us-east-1.rds.amazonaws.com",
1864
+ ),
1865
+ ],
1866
+ ids=["no_vpc", "no_availability_zone"],
1867
+ )
1868
+ def test_parse_rds_instance_missing_fields(
1869
+ self, db_data, expected_vlan_id, expected_location, expected_fqdn, mock_aws_integration
1870
+ ):
1871
+ """Test parsing RDS instances with missing fields."""
1872
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db_data)
1873
+
1874
+ assert result.vlan_id == expected_vlan_id
1875
+ assert result.location == expected_location
1876
+ assert result.fqdn == expected_fqdn
1877
+
1878
+ def test_parse_rds_instance_minimal_data(self, mock_aws_integration):
1879
+ """Test parsing RDS instance with minimal data"""
1880
+
1881
+ db = {"DBInstanceIdentifier": "minimal-db-instance"}
1882
+
1883
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db)
1884
+
1885
+ assert result.name == "minimal-db-instance) - "
1886
+ assert result.identifier == ""
1887
+ assert result.asset_type == regscale_models.AssetType.VM
1888
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1889
+ assert result.component_type == regscale_models.ComponentType.Hardware
1890
+ assert result.component_names == ["RDS Instances"]
1891
+ assert result.parent_id == mock_aws_integration.plan_id
1892
+ assert result.parent_module == "securityplans"
1893
+ assert result.fqdn is None
1894
+ assert result.vlan_id is None
1895
+ assert result.status == regscale_models.AssetStatus.Inactive # No status provided
1896
+ assert result.location is None
1897
+ assert result.model is None
1898
+ assert result.manufacturer == "AWS"
1899
+ assert result.disk_storage is None
1900
+ assert result.software_name is None
1901
+ assert result.software_version is None
1902
+ assert result.external_id == "minimal-db-instance"
1903
+ assert result.aws_identifier is None
1904
+ assert result.is_public_facing is False
1905
+ assert result.source_data == db
1906
+
1907
+ def test_parse_rds_instance_edge_cases(self, mock_aws_integration):
1908
+ """Test parsing RDS instance with edge cases"""
1909
+
1910
+ db = {
1911
+ "DBInstanceIdentifier": "edge-case-db-instance",
1912
+ "DBInstanceClass": "db.r5.24xlarge",
1913
+ "Engine": "oracle-ee",
1914
+ "EngineVersion": "19.0.0.0.ru-2021-10.rur-2021-10.r1",
1915
+ "DBInstanceStatus": "modifying",
1916
+ "AllocatedStorage": 1000,
1917
+ "AvailabilityZone": "us-east-1d",
1918
+ "VpcId": "vpc-66666666",
1919
+ "PubliclyAccessible": True,
1920
+ "DBInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:edge-case-db-instance",
1921
+ "Endpoint": {"Address": "edge-case-db-instance.pqr678.us-east-1.rds.amazonaws.com", "Port": 1521},
1922
+ }
1923
+
1924
+ result = AWSInventoryIntegration.parse_rds_instance(mock_aws_integration, db)
1925
+
1926
+ assert result.name == "edge-case-db-instance 19.0.0.0.ru-2021-10.rur-2021-10.r1) - db.r5.24xlarge"
1927
+ assert result.software_name == "oracle-ee"
1928
+ assert result.software_version == "19.0.0.0.ru-2021-10.rur-2021-10.r1"
1929
+ assert result.status == regscale_models.AssetStatus.Inactive # modifying status
1930
+ assert result.disk_storage == 1000
1931
+ assert result.is_public_facing is True
1932
+ assert result.fqdn == "edge-case-db-instance.pqr678.us-east-1.rds.amazonaws.com"
1933
+ assert result.vlan_id == "vpc-66666666"
1934
+ assert result.location == "us-east-1d"
1935
+ assert result.model == "db.r5.24xlarge"
1936
+ assert result.asset_type == regscale_models.AssetType.VM
1937
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
1938
+ assert result.component_type == regscale_models.ComponentType.Hardware
1939
+ assert result.component_names == ["RDS Instances"]
1940
+ assert result.manufacturer == "AWS"
1941
+ assert result.source_data == db
1942
+
1943
+ def test_parse_dynamodb_table_basic(self, mock_aws_integration):
1944
+ """Test parsing a basic DynamoDB table"""
1945
+
1946
+ table = {
1947
+ "TableName": "test-table",
1948
+ "TableStatus": "ACTIVE",
1949
+ "TableSizeBytes": 1024000,
1950
+ "Region": "us-east-1",
1951
+ "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/test-table",
1952
+ "ItemCount": 1000,
1953
+ "CreationDateTime": "2023-01-01T00:00:00Z",
1954
+ }
1955
+
1956
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
1957
+
1958
+ assert result.name == "test-table (ACTIVE)"
1959
+ assert result.identifier == "arn:aws:dynamodb:us-east-1:123456789012:table/test-table"
1960
+ assert result.asset_type == regscale_models.AssetType.Other
1961
+ assert result.asset_category == regscale_models.AssetCategory.Software
1962
+ assert result.component_type == regscale_models.ComponentType.Software
1963
+ assert result.component_names == ["DynamoDB Tables"]
1964
+ assert result.parent_id == mock_aws_integration.plan_id
1965
+ assert result.parent_module == "securityplans"
1966
+ assert result.status == regscale_models.AssetStatus.Active
1967
+ assert result.location == "us-east-1"
1968
+ assert result.disk_storage == 1024000
1969
+ assert result.external_id == "test-table"
1970
+ assert result.aws_identifier == "arn:aws:dynamodb:us-east-1:123456789012:table/test-table"
1971
+ assert result.manufacturer == "AWS"
1972
+ assert result.source_data == table
1973
+
1974
+ def test_parse_dynamodb_table_inactive_status(self, mock_aws_integration):
1975
+ """Test parsing DynamoDB table with inactive status"""
1976
+
1977
+ table = {
1978
+ "TableName": "inactive-table",
1979
+ "TableStatus": "CREATING",
1980
+ "TableSizeBytes": 0,
1981
+ "Region": "us-west-2",
1982
+ "TableArn": "arn:aws:dynamodb:us-west-2:123456789012:table/inactive-table",
1983
+ "ItemCount": 0,
1984
+ "CreationDateTime": "2023-01-01T00:00:00Z",
1985
+ }
1986
+
1987
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
1988
+
1989
+ assert result.name == "inactive-table (CREATING)"
1990
+ assert result.status == regscale_models.AssetStatus.Inactive
1991
+ assert result.location == "us-west-2"
1992
+ assert result.disk_storage == 0
1993
+ assert result.aws_identifier == "arn:aws:dynamodb:us-west-2:123456789012:table/inactive-table"
1994
+
1995
+ def test_parse_dynamodb_table_no_status(self, mock_aws_integration):
1996
+ """Test parsing DynamoDB table without status"""
1997
+
1998
+ table = {
1999
+ "TableName": "no-status-table",
2000
+ "TableSizeBytes": 512000,
2001
+ "Region": "us-east-1",
2002
+ "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/no-status-table",
2003
+ "ItemCount": 500,
2004
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2005
+ }
2006
+
2007
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2008
+
2009
+ assert result.name == "no-status-table"
2010
+ assert result.status == regscale_models.AssetStatus.Inactive # No status provided
2011
+ assert result.location == "us-east-1"
2012
+ assert result.disk_storage == 512000
2013
+ assert result.aws_identifier == "arn:aws:dynamodb:us-east-1:123456789012:table/no-status-table"
2014
+
2015
+ def test_parse_dynamodb_table_empty_status(self, mock_aws_integration):
2016
+ """Test parsing DynamoDB table with empty status"""
2017
+
2018
+ table = {
2019
+ "TableName": "empty-status-table",
2020
+ "TableStatus": "",
2021
+ "TableSizeBytes": 256000,
2022
+ "Region": "us-east-1",
2023
+ "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/empty-status-table",
2024
+ "ItemCount": 250,
2025
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2026
+ }
2027
+
2028
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2029
+
2030
+ assert result.name == "empty-status-table" # Empty status is not included in name
2031
+ assert result.status == regscale_models.AssetStatus.Inactive # Empty status is not ACTIVE
2032
+ assert result.location == "us-east-1"
2033
+ assert result.disk_storage == 256000
2034
+
2035
+ def test_parse_dynamodb_table_no_region(self, mock_aws_integration):
2036
+ """Test parsing DynamoDB table without region"""
2037
+
2038
+ table = {
2039
+ "TableName": "no-region-table",
2040
+ "TableStatus": "ACTIVE",
2041
+ "TableSizeBytes": 1024000,
2042
+ "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/no-region-table",
2043
+ "ItemCount": 1000,
2044
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2045
+ }
2046
+
2047
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2048
+
2049
+ assert result.name == "no-region-table (ACTIVE)"
2050
+ assert result.location is None
2051
+ assert result.status == regscale_models.AssetStatus.Active
2052
+ assert result.disk_storage == 1024000
2053
+
2054
+ def test_parse_dynamodb_table_no_size(self, mock_aws_integration):
2055
+ """Test parsing DynamoDB table without size"""
2056
+
2057
+ table = {
2058
+ "TableName": "no-size-table",
2059
+ "TableStatus": "ACTIVE",
2060
+ "Region": "us-east-1",
2061
+ "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/no-size-table",
2062
+ "ItemCount": 1000,
2063
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2064
+ }
2065
+
2066
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2067
+
2068
+ assert result.name == "no-size-table (ACTIVE)"
2069
+ assert result.disk_storage is None
2070
+ assert result.status == regscale_models.AssetStatus.Active
2071
+ assert result.location == "us-east-1"
2072
+
2073
+ def test_parse_dynamodb_table_no_arn(self, mock_aws_integration):
2074
+ """Test parsing DynamoDB table without ARN"""
2075
+
2076
+ table = {
2077
+ "TableName": "no-arn-table",
2078
+ "TableStatus": "ACTIVE",
2079
+ "TableSizeBytes": 1024000,
2080
+ "Region": "us-east-1",
2081
+ "ItemCount": 1000,
2082
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2083
+ }
2084
+
2085
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2086
+
2087
+ assert result.name == "no-arn-table (ACTIVE)"
2088
+ assert result.aws_identifier is None
2089
+ assert result.status == regscale_models.AssetStatus.Active
2090
+ assert result.location == "us-east-1"
2091
+ assert result.disk_storage == 1024000
2092
+
2093
+ def test_parse_dynamodb_table_minimal_data(self, mock_aws_integration):
2094
+ """Test parsing DynamoDB table with minimal data"""
2095
+
2096
+ table = {"TableName": "minimal-table"}
2097
+
2098
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2099
+
2100
+ assert result.name == "minimal-table"
2101
+ assert result.identifier == ""
2102
+ assert result.asset_type == regscale_models.AssetType.Other
2103
+ assert result.asset_category == regscale_models.AssetCategory.Software
2104
+ assert result.component_type == regscale_models.ComponentType.Software
2105
+ assert result.component_names == ["DynamoDB Tables"]
2106
+ assert result.parent_id == mock_aws_integration.plan_id
2107
+ assert result.parent_module == "securityplans"
2108
+ assert result.status == regscale_models.AssetStatus.Inactive # No status provided
2109
+ assert result.location is None
2110
+ assert result.disk_storage is None
2111
+ assert result.external_id == "minimal-table"
2112
+ assert result.aws_identifier is None
2113
+ assert result.manufacturer == "AWS"
2114
+ assert result.source_data == table
2115
+
2116
+ def test_parse_dynamodb_table_edge_cases(self, mock_aws_integration):
2117
+ """Test parsing DynamoDB table with edge cases"""
2118
+
2119
+ table = {
2120
+ "TableName": "edge-case-table",
2121
+ "TableStatus": "UPDATING",
2122
+ "TableSizeBytes": 0,
2123
+ "Region": "us-east-1",
2124
+ "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/edge-case-table",
2125
+ "ItemCount": 0,
2126
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2127
+ }
2128
+
2129
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2130
+
2131
+ assert result.name == "edge-case-table (UPDATING)"
2132
+ assert result.status == regscale_models.AssetStatus.Inactive # UPDATING is not ACTIVE
2133
+ assert result.disk_storage == 0
2134
+ assert result.location == "us-east-1"
2135
+ assert result.aws_identifier == "arn:aws:dynamodb:us-east-1:123456789012:table/edge-case-table"
2136
+ assert result.asset_type == regscale_models.AssetType.Other
2137
+ assert result.asset_category == regscale_models.AssetCategory.Software
2138
+ assert result.component_type == regscale_models.ComponentType.Software
2139
+ assert result.component_names == ["DynamoDB Tables"]
2140
+ assert result.manufacturer == "AWS"
2141
+ assert result.source_data == table
2142
+
2143
+ def test_parse_dynamodb_table_large_size(self, mock_aws_integration):
2144
+ """Test parsing DynamoDB table with large size"""
2145
+
2146
+ table = {
2147
+ "TableName": "large-table",
2148
+ "TableStatus": "ACTIVE",
2149
+ "TableSizeBytes": 1073741824, # 1 GB
2150
+ "Region": "us-west-2",
2151
+ "TableArn": "arn:aws:dynamodb:us-west-2:123456789012:table/large-table",
2152
+ "ItemCount": 1000000,
2153
+ "CreationDateTime": "2023-01-01T00:00:00Z",
2154
+ }
2155
+
2156
+ result = AWSInventoryIntegration.parse_dynamodb_table(mock_aws_integration, table)
2157
+
2158
+ assert result.name == "large-table (ACTIVE)"
2159
+ assert result.disk_storage == 1073741824
2160
+ assert result.status == regscale_models.AssetStatus.Active
2161
+ assert result.location == "us-west-2"
2162
+ assert result.aws_identifier == "arn:aws:dynamodb:us-west-2:123456789012:table/large-table"
2163
+
2164
+ def test_parse_vpc_basic(self, mock_aws_integration):
2165
+ """Test parsing a basic VPC"""
2166
+
2167
+ vpc = {
2168
+ "VpcId": "vpc-12345678",
2169
+ "CidrBlock": "10.0.0.0/16",
2170
+ "State": "available",
2171
+ "Region": "us-east-1",
2172
+ "OwnerId": "123456789012",
2173
+ "Tags": [{"Key": "Name", "Value": "Production VPC"}, {"Key": "Environment", "Value": "Production"}],
2174
+ }
2175
+
2176
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2177
+
2178
+ assert result.name == "Production VPC"
2179
+ assert result.identifier == "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-12345678"
2180
+ assert result.asset_type == regscale_models.AssetType.NetworkRouter
2181
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
2182
+ assert result.component_type == regscale_models.ComponentType.Hardware
2183
+ assert result.component_names == ["VPCs"]
2184
+ assert result.parent_id == mock_aws_integration.plan_id
2185
+ assert result.parent_module == "securityplans"
2186
+ assert result.status == regscale_models.AssetStatus.Active
2187
+ assert result.location == "us-east-1"
2188
+ assert result.vlan_id == "vpc-12345678"
2189
+ assert result.external_id == "vpc-12345678"
2190
+ assert result.aws_identifier == "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-12345678"
2191
+ assert result.manufacturer == "AWS"
2192
+ assert result.notes == "CIDR: 10.0.0.0/16"
2193
+ assert result.source_data == vpc
2194
+
2195
+ def test_parse_vpc_no_name_tag(self, mock_aws_integration):
2196
+ """Test parsing VPC without Name tag"""
2197
+
2198
+ vpc = {
2199
+ "VpcId": "vpc-87654321",
2200
+ "CidrBlock": "172.16.0.0/16",
2201
+ "State": "available",
2202
+ "Region": "us-west-2",
2203
+ "Tags": [{"Key": "Environment", "Value": "Development"}, {"Key": "Project", "Value": "TestProject"}],
2204
+ }
2205
+
2206
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2207
+
2208
+ assert result.name == "vpc-87654321"
2209
+ assert result.identifier == "arn:aws:ec2:us-west-2::vpc/vpc-87654321"
2210
+ assert result.status == regscale_models.AssetStatus.Active
2211
+ assert result.location == "us-west-2"
2212
+ assert result.notes == "CIDR: 172.16.0.0/16"
2213
+
2214
+ def test_parse_vpc_no_tags(self, mock_aws_integration):
2215
+ """Test parsing VPC with no tags"""
2216
+
2217
+ vpc = {"VpcId": "vpc-notags123", "CidrBlock": "192.168.0.0/16", "State": "available", "Region": "us-east-1"}
2218
+
2219
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2220
+
2221
+ assert result.name == "vpc-notags123"
2222
+ assert result.identifier == "arn:aws:ec2:us-east-1::vpc/vpc-notags123"
2223
+ assert result.status == regscale_models.AssetStatus.Active
2224
+ assert result.location == "us-east-1"
2225
+ assert result.notes == "CIDR: 192.168.0.0/16"
2226
+
2227
+ def test_parse_vpc_default_vpc(self, mock_aws_integration):
2228
+ """Test parsing a default VPC"""
2229
+
2230
+ vpc = {
2231
+ "VpcId": "vpc-default123",
2232
+ "CidrBlock": "10.0.0.0/16",
2233
+ "State": "available",
2234
+ "Region": "us-east-1",
2235
+ "IsDefault": True,
2236
+ "Tags": [{"Key": "Name", "Value": "Default VPC"}],
2237
+ }
2238
+
2239
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2240
+
2241
+ assert result.name == "Default VPC"
2242
+ assert result.identifier == "arn:aws:ec2:us-east-1::vpc/vpc-default123"
2243
+ assert result.status == regscale_models.AssetStatus.Active
2244
+ assert result.location == "us-east-1"
2245
+ assert result.notes == "CIDR: 10.0.0.0/16" # IsDefault logic is overwritten by CIDR notes
2246
+
2247
+ def test_parse_vpc_inactive_state(self, mock_aws_integration):
2248
+ """Test parsing VPC with inactive state"""
2249
+
2250
+ vpc = {
2251
+ "VpcId": "vpc-inactive123",
2252
+ "CidrBlock": "10.0.0.0/16",
2253
+ "State": "pending",
2254
+ "Region": "us-east-1",
2255
+ "Tags": [{"Key": "Name", "Value": "Inactive VPC"}],
2256
+ }
2257
+
2258
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2259
+
2260
+ assert result.name == "Inactive VPC"
2261
+ assert result.status == regscale_models.AssetStatus.Inactive
2262
+ assert result.location == "us-east-1"
2263
+ assert result.notes == "CIDR: 10.0.0.0/16"
2264
+
2265
+ def test_parse_vpc_no_cidr(self, mock_aws_integration):
2266
+ """Test parsing VPC without CIDR block"""
2267
+
2268
+ vpc = {
2269
+ "VpcId": "vpc-nocidr123",
2270
+ "State": "available",
2271
+ "Region": "us-east-1",
2272
+ "Tags": [{"Key": "Name", "Value": "No CIDR VPC"}],
2273
+ }
2274
+
2275
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2276
+
2277
+ assert result.name == "No CIDR VPC"
2278
+ assert result.status == regscale_models.AssetStatus.Active
2279
+ assert result.location == "us-east-1"
2280
+ assert result.notes == "CIDR: None"
2281
+
2282
+ def test_parse_vpc_no_region(self, mock_aws_integration):
2283
+ """Test parsing VPC without region"""
2284
+
2285
+ vpc = {
2286
+ "VpcId": "vpc-noregion123",
2287
+ "CidrBlock": "10.0.0.0/16",
2288
+ "State": "available",
2289
+ "Tags": [{"Key": "Name", "Value": "No Region VPC"}],
2290
+ }
2291
+
2292
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2293
+
2294
+ assert result.name == "No Region VPC"
2295
+ assert result.location == "us-east-1" # Default region when not provided
2296
+ assert result.status == regscale_models.AssetStatus.Active
2297
+ assert result.notes == "CIDR: 10.0.0.0/16"
2298
+
2299
+ def test_parse_vpc_no_vpc_id(self, mock_aws_integration):
2300
+ """Test parsing VPC without VPC ID"""
2301
+
2302
+ vpc = {
2303
+ "CidrBlock": "10.0.0.0/16",
2304
+ "State": "available",
2305
+ "Region": "us-east-1",
2306
+ "Tags": [{"Key": "Name", "Value": "No VPC ID"}],
2307
+ }
2308
+
2309
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2310
+
2311
+ assert result.name == "No VPC ID"
2312
+ assert result.identifier == "arn:aws:ec2:us-east-1::vpc/" # ARN with empty VPC ID
2313
+ assert result.vlan_id == "" # Empty string, not None
2314
+ assert result.external_id == "" # Empty string, not None
2315
+ assert result.aws_identifier == "arn:aws:ec2:us-east-1::vpc/" # ARN with empty VPC ID
2316
+ assert result.status == regscale_models.AssetStatus.Active
2317
+ assert result.location == "us-east-1"
2318
+ assert result.notes == "CIDR: 10.0.0.0/16"
2319
+
2320
+ def test_parse_vpc_minimal_data(self, mock_aws_integration):
2321
+ """Test parsing VPC with minimal data"""
2322
+
2323
+ vpc = {}
2324
+
2325
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2326
+
2327
+ assert result.name == ""
2328
+ assert result.identifier == "arn:aws:ec2:us-east-1::vpc/" # ARN with empty VPC ID
2329
+ assert result.asset_type == regscale_models.AssetType.NetworkRouter
2330
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
2331
+ assert result.component_type == regscale_models.ComponentType.Hardware
2332
+ assert result.component_names == ["VPCs"]
2333
+ assert result.parent_id == mock_aws_integration.plan_id
2334
+ assert result.parent_module == "securityplans"
2335
+ assert result.status == regscale_models.AssetStatus.Inactive # No state provided
2336
+ assert result.location == "us-east-1" # Default region
2337
+ assert result.vlan_id == "" # Empty string
2338
+ assert result.external_id == "" # Empty string
2339
+ assert result.aws_identifier == "arn:aws:ec2:us-east-1::vpc/" # ARN with empty VPC ID
2340
+ assert result.manufacturer == "AWS"
2341
+ assert result.notes == "CIDR: None"
2342
+ assert result.source_data == vpc
2343
+
2344
+ def test_parse_vpc_edge_cases(self, mock_aws_integration):
2345
+ """Test parsing VPC with edge cases"""
2346
+
2347
+ vpc = {
2348
+ "VpcId": "vpc-edge123",
2349
+ "CidrBlock": "10.0.0.0/8",
2350
+ "State": "available",
2351
+ "Region": "us-east-1",
2352
+ "OwnerId": "123456789012",
2353
+ "IsDefault": False,
2354
+ "Tags": [
2355
+ {"Key": "Name", "Value": "Edge Case VPC"},
2356
+ {"Key": "Name", "Value": "Duplicate Name"}, # Duplicate Name tag
2357
+ {"Key": "Description", "Value": "Test VPC"},
2358
+ ],
2359
+ }
2360
+
2361
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2362
+
2363
+ assert result.name == "Edge Case VPC" # First Name tag is used
2364
+ assert result.identifier == "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-edge123"
2365
+ assert result.status == regscale_models.AssetStatus.Active
2366
+ assert result.location == "us-east-1"
2367
+ assert result.vlan_id == "vpc-edge123"
2368
+ assert result.external_id == "vpc-edge123"
2369
+ assert result.aws_identifier == "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-edge123"
2370
+ assert result.notes == "CIDR: 10.0.0.0/8" # No "Default VPC" prefix since IsDefault is False
2371
+ assert result.asset_type == regscale_models.AssetType.NetworkRouter
2372
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
2373
+ assert result.component_type == regscale_models.ComponentType.Hardware
2374
+ assert result.component_names == ["VPCs"]
2375
+ assert result.manufacturer == "AWS"
2376
+ assert result.source_data == vpc
2377
+
2378
+ def test_parse_vpc_empty_tags(self, mock_aws_integration):
2379
+ """Test parsing VPC with empty tags list"""
2380
+
2381
+ vpc = {
2382
+ "VpcId": "vpc-emptytags123",
2383
+ "CidrBlock": "10.0.0.0/16",
2384
+ "State": "available",
2385
+ "Region": "us-east-1",
2386
+ "Tags": [],
2387
+ }
2388
+
2389
+ result = AWSInventoryIntegration.parse_vpc(mock_aws_integration, vpc)
2390
+
2391
+ assert result.name == "vpc-emptytags123" # Falls back to VPC ID
2392
+ assert result.identifier == "arn:aws:ec2:us-east-1::vpc/vpc-emptytags123"
2393
+ assert result.status == regscale_models.AssetStatus.Active
2394
+ assert result.location == "us-east-1"
2395
+ assert result.notes == "CIDR: 10.0.0.0/16"
2396
+
2397
+ def test_parse_load_balancer_basic(self, mock_aws_integration):
2398
+ """Test parsing a basic load balancer"""
2399
+
2400
+ lb = {
2401
+ "LoadBalancerName": "my-load-balancer",
2402
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-load-balancer/1234567890",
2403
+ "DNSName": "my-load-balancer-1234567890.us-east-1.elb.amazonaws.com",
2404
+ "VpcId": "vpc-12345678",
2405
+ "State": "active",
2406
+ "Region": "us-east-1",
2407
+ "Scheme": "internet-facing",
2408
+ "Listeners": [{"Port": 80, "Protocol": "HTTP"}, {"Port": 443, "Protocol": "HTTPS"}],
2409
+ }
2410
+
2411
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2412
+
2413
+ assert result.name == "my-load-balancer"
2414
+ assert (
2415
+ result.identifier
2416
+ == "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-load-balancer/1234567890"
2417
+ )
2418
+ assert result.asset_type == regscale_models.AssetType.NetworkRouter
2419
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
2420
+ assert result.component_type == regscale_models.ComponentType.Hardware
2421
+ assert result.component_names == ["Load Balancers"]
2422
+ assert result.parent_id == mock_aws_integration.plan_id
2423
+ assert result.parent_module == "securityplans"
2424
+ assert result.fqdn == "my-load-balancer-1234567890.us-east-1.elb.amazonaws.com"
2425
+ assert result.vlan_id == "vpc-12345678"
2426
+ assert result.status == regscale_models.AssetStatus.Active
2427
+ assert result.location == "us-east-1"
2428
+ assert result.external_id == "my-load-balancer"
2429
+ assert (
2430
+ result.aws_identifier
2431
+ == "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-load-balancer/1234567890"
2432
+ )
2433
+ assert result.manufacturer == "AWS"
2434
+ assert result.is_public_facing is True
2435
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2436
+ assert result.source_data == lb
2437
+ assert result.ports_and_protocols == [{"port": 80, "protocol": "HTTP"}, {"port": 443, "protocol": "HTTPS"}]
2438
+
2439
+ def test_parse_load_balancer_internal(self, mock_aws_integration):
2440
+ """Test parsing an internal load balancer"""
2441
+
2442
+ lb = {
2443
+ "LoadBalancerName": "internal-lb",
2444
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/internal-lb/0987654321",
2445
+ "DNSName": "internal-lb-0987654321.us-west-2.elb.amazonaws.com",
2446
+ "VpcId": "vpc-87654321",
2447
+ "State": "active",
2448
+ "Region": "us-west-2",
2449
+ "Scheme": "internal",
2450
+ "Listeners": [{"Port": 8080, "Protocol": "HTTP"}],
2451
+ }
2452
+
2453
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2454
+
2455
+ assert result.name == "internal-lb"
2456
+ assert (
2457
+ result.identifier
2458
+ == "arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/internal-lb/0987654321"
2459
+ )
2460
+ assert result.fqdn == "internal-lb-0987654321.us-west-2.elb.amazonaws.com"
2461
+ assert result.vlan_id == "vpc-87654321"
2462
+ assert result.status == regscale_models.AssetStatus.Active
2463
+ assert result.location == "us-west-2"
2464
+ assert result.is_public_facing is False
2465
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2466
+ assert result.ports_and_protocols == [{"port": 8080, "protocol": "HTTP"}]
2467
+
2468
+ def test_parse_load_balancer_inactive_state(self, mock_aws_integration):
2469
+ """Test parsing load balancer with inactive state"""
2470
+
2471
+ lb = {
2472
+ "LoadBalancerName": "inactive-lb",
2473
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/inactive-lb/1111111111",
2474
+ "DNSName": "inactive-lb-1111111111.us-east-1.elb.amazonaws.com",
2475
+ "VpcId": "vpc-11111111",
2476
+ "State": "provisioning",
2477
+ "Region": "us-east-1",
2478
+ "Scheme": "internet-facing",
2479
+ "Listeners": [],
2480
+ }
2481
+
2482
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2483
+
2484
+ assert result.name == "inactive-lb"
2485
+ assert result.status == regscale_models.AssetStatus.Inactive
2486
+ assert result.location == "us-east-1"
2487
+ assert result.is_public_facing is True
2488
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2489
+ assert result.ports_and_protocols == []
2490
+
2491
+ def test_parse_load_balancer_no_scheme(self, mock_aws_integration):
2492
+ """Test parsing load balancer without scheme"""
2493
+
2494
+ lb = {
2495
+ "LoadBalancerName": "no-scheme-lb",
2496
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/no-scheme-lb/2222222222",
2497
+ "DNSName": "no-scheme-lb-2222222222.us-east-1.elb.amazonaws.com",
2498
+ "VpcId": "vpc-22222222",
2499
+ "State": "active",
2500
+ "Region": "us-east-1",
2501
+ "Listeners": [{"Port": 80, "Protocol": "HTTP"}],
2502
+ }
2503
+
2504
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2505
+
2506
+ assert result.name == "no-scheme-lb"
2507
+ assert result.status == regscale_models.AssetStatus.Active
2508
+ assert result.location == "us-east-1"
2509
+ assert result.is_public_facing is False # No scheme means not public-facing
2510
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2511
+ assert result.ports_and_protocols == [{"port": 80, "protocol": "HTTP"}]
2512
+
2513
+ def test_parse_load_balancer_no_name(self, mock_aws_integration):
2514
+ """Test parsing load balancer without name"""
2515
+
2516
+ lb = {
2517
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/unnamed-lb/3333333333",
2518
+ "DNSName": "unnamed-lb-3333333333.us-east-1.elb.amazonaws.com",
2519
+ "VpcId": "vpc-33333333",
2520
+ "State": "active",
2521
+ "Region": "us-east-1",
2522
+ "Scheme": "internet-facing",
2523
+ "Listeners": [],
2524
+ }
2525
+
2526
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2527
+
2528
+ assert result.name == ""
2529
+ assert (
2530
+ result.identifier
2531
+ == "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/unnamed-lb/3333333333"
2532
+ )
2533
+ assert result.external_id is None
2534
+ assert result.status == regscale_models.AssetStatus.Active
2535
+ assert result.location == "us-east-1"
2536
+ assert result.is_public_facing is True
2537
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2538
+ assert result.ports_and_protocols == []
2539
+
2540
+ def test_parse_load_balancer_no_dns(self, mock_aws_integration):
2541
+ """Test parsing load balancer without DNS name"""
2542
+
2543
+ lb = {
2544
+ "LoadBalancerName": "no-dns-lb",
2545
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/no-dns-lb/4444444444",
2546
+ "VpcId": "vpc-44444444",
2547
+ "State": "active",
2548
+ "Region": "us-east-1",
2549
+ "Scheme": "internal",
2550
+ "Listeners": [{"Port": 8080, "Protocol": "HTTP"}],
2551
+ }
2552
+
2553
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2554
+
2555
+ assert result.name == "no-dns-lb"
2556
+ assert result.fqdn is None
2557
+ assert result.status == regscale_models.AssetStatus.Active
2558
+ assert result.location == "us-east-1"
2559
+ assert result.is_public_facing is False
2560
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2561
+ assert result.ports_and_protocols == [{"port": 8080, "protocol": "HTTP"}]
2562
+
2563
+ def test_parse_load_balancer_no_vpc(self, mock_aws_integration):
2564
+ """Test parsing load balancer without VPC ID"""
2565
+
2566
+ lb = {
2567
+ "LoadBalancerName": "no-vpc-lb",
2568
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/no-vpc-lb/5555555555",
2569
+ "DNSName": "no-vpc-lb-5555555555.us-east-1.elb.amazonaws.com",
2570
+ "State": "active",
2571
+ "Region": "us-east-1",
2572
+ "Scheme": "internet-facing",
2573
+ "Listeners": [{"Port": 80, "Protocol": "HTTP"}],
2574
+ }
2575
+
2576
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2577
+
2578
+ assert result.name == "no-vpc-lb"
2579
+ assert result.vlan_id is None
2580
+ assert result.status == regscale_models.AssetStatus.Active
2581
+ assert result.location == "us-east-1"
2582
+ assert result.is_public_facing is True
2583
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2584
+ assert result.ports_and_protocols == [{"port": 80, "protocol": "HTTP"}]
2585
+
2586
+ def test_parse_load_balancer_no_region(self, mock_aws_integration):
2587
+ """Test parsing load balancer without region"""
2588
+
2589
+ lb = {
2590
+ "LoadBalancerName": "no-region-lb",
2591
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/no-region-lb/6666666666",
2592
+ "DNSName": "no-region-lb-6666666666.us-east-1.elb.amazonaws.com",
2593
+ "VpcId": "vpc-66666666",
2594
+ "State": "active",
2595
+ "Scheme": "internet-facing",
2596
+ "Listeners": [{"Port": 443, "Protocol": "HTTPS"}],
2597
+ }
2598
+
2599
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2600
+
2601
+ assert result.name == "no-region-lb"
2602
+ assert result.location is None
2603
+ assert result.status == regscale_models.AssetStatus.Active
2604
+ assert result.is_public_facing is True
2605
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2606
+ assert result.ports_and_protocols == [{"port": 443, "protocol": "HTTPS"}]
2607
+
2608
+ def test_parse_load_balancer_no_listeners(self, mock_aws_integration):
2609
+ """Test parsing load balancer without listeners"""
2610
+
2611
+ lb = {
2612
+ "LoadBalancerName": "no-listeners-lb",
2613
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/no-listeners-lb/7777777777",
2614
+ "DNSName": "no-listeners-lb-7777777777.us-east-1.elb.amazonaws.com",
2615
+ "VpcId": "vpc-77777777",
2616
+ "State": "active",
2617
+ "Region": "us-east-1",
2618
+ "Scheme": "internal",
2619
+ }
2620
+
2621
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2622
+
2623
+ assert result.name == "no-listeners-lb"
2624
+ assert result.status == regscale_models.AssetStatus.Active
2625
+ assert result.location == "us-east-1"
2626
+ assert result.is_public_facing is False
2627
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2628
+ assert result.ports_and_protocols == []
2629
+
2630
+ def test_parse_load_balancer_minimal_data(self, mock_aws_integration):
2631
+ """Test parsing load balancer with minimal data"""
2632
+
2633
+ lb = {}
2634
+
2635
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2636
+
2637
+ assert result.name == ""
2638
+ assert result.identifier is None
2639
+ assert result.asset_type == regscale_models.AssetType.NetworkRouter
2640
+ assert result.asset_category == regscale_models.AssetCategory.Hardware
2641
+ assert result.component_type == regscale_models.ComponentType.Hardware
2642
+ assert result.component_names == ["Load Balancers"]
2643
+ assert result.parent_id == mock_aws_integration.plan_id
2644
+ assert result.parent_module == "securityplans"
2645
+ assert result.fqdn is None
2646
+ assert result.vlan_id is None
2647
+ assert result.status == regscale_models.AssetStatus.Inactive # No state provided
2648
+ assert result.location is None
2649
+ assert result.external_id is None
2650
+ assert result.aws_identifier is None
2651
+ assert result.manufacturer == "AWS"
2652
+ assert result.is_public_facing is False
2653
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2654
+ assert result.source_data == lb
2655
+ assert result.ports_and_protocols == []
2656
+
2657
+ def test_parse_load_balancer_edge_cases(self, mock_aws_integration):
2658
+ """Test parsing load balancer with edge cases"""
2659
+
2660
+ lb = {
2661
+ "LoadBalancerName": "edge-case-lb",
2662
+ "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/edge-case-lb/8888888888",
2663
+ "DNSName": "edge-case-lb-8888888888.us-east-1.elb.amazonaws.com",
2664
+ "VpcId": "vpc-88888888",
2665
+ "State": "active",
2666
+ "Region": "us-east-1",
2667
+ "Scheme": "internet-facing",
2668
+ "Listeners": [
2669
+ {"Port": 80, "Protocol": "HTTP"},
2670
+ {"Port": 443, "Protocol": "HTTPS"},
2671
+ {"Port": 8080, "Protocol": "HTTP"},
2672
+ {"Port": 8443, "Protocol": "HTTPS"},
2673
+ ],
2674
+ }
2675
+
2676
+ result = AWSInventoryIntegration.parse_load_balancer(mock_aws_integration, lb)
2677
+
2678
+ assert result.name == "edge-case-lb"
2679
+ assert (
2680
+ result.identifier
2681
+ == "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/edge-case-lb/8888888888"
2682
+ )
2683
+ assert result.fqdn == "edge-case-lb-8888888888.us-east-1.elb.amazonaws.com"
2684
+ assert result.vlan_id == "vpc-88888888"
2685
+ assert result.status == regscale_models.AssetStatus.Active
2686
+ assert result.location == "us-east-1"
2687
+ assert result.external_id == "edge-case-lb"
2688
+ assert (
2689
+ result.aws_identifier
2690
+ == "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/edge-case-lb/8888888888"
2691
+ )
2692
+ assert result.manufacturer == "AWS"
2693
+ assert result.is_public_facing is True
2694
+ assert result.notes is None # Bug: notes field is not being set in parse_load_balancer method
2695
+ assert result.ports_and_protocols == [
2696
+ {"port": 80, "protocol": "HTTP"},
2697
+ {"port": 443, "protocol": "HTTPS"},
2698
+ {"port": 8080, "protocol": "HTTP"},
2699
+ {"port": 8443, "protocol": "HTTPS"},
2700
+ ]
2701
+ assert result.source_data == lb
2702
+
2703
+ def test_parse_ecr_repository_basic(self, mock_aws_integration):
2704
+ """Test parsing a basic ECR repository"""
2705
+
2706
+ repo = {
2707
+ "RepositoryName": "my-app-repo",
2708
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/my-app-repo",
2709
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app-repo",
2710
+ "Region": "us-east-1",
2711
+ "ImageTagMutability": "MUTABLE",
2712
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2713
+ }
2714
+
2715
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2716
+
2717
+ assert result.name == "my-app-repo"
2718
+ assert result.identifier == "arn:aws:ecr:us-east-1:123456789012:repository/my-app-repo"
2719
+ assert result.asset_type == regscale_models.AssetType.Other
2720
+ assert result.asset_category == regscale_models.AssetCategory.Software
2721
+ assert result.component_type == regscale_models.ComponentType.Software
2722
+ assert result.component_names == ["ECR Repositories"]
2723
+ assert result.parent_id == mock_aws_integration.plan_id
2724
+ assert result.parent_module == "securityplans"
2725
+ assert result.status == regscale_models.AssetStatus.Active
2726
+ assert result.location == "us-east-1"
2727
+ assert result.external_id == "my-app-repo"
2728
+ assert result.aws_identifier == "arn:aws:ecr:us-east-1:123456789012:repository/my-app-repo"
2729
+ assert result.uri == "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app-repo"
2730
+ assert result.manufacturer == "AWS"
2731
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2732
+ assert result.source_data == repo
2733
+
2734
+ def test_parse_ecr_repository_immutable_tags(self, mock_aws_integration):
2735
+ """Test parsing ECR repository with immutable tags"""
2736
+
2737
+ repo = {
2738
+ "RepositoryName": "immutable-repo",
2739
+ "RepositoryArn": "arn:aws:ecr:us-west-2:123456789012:repository/immutable-repo",
2740
+ "RepositoryUri": "123456789012.dkr.ecr.us-west-2.amazonaws.com/immutable-repo",
2741
+ "Region": "us-west-2",
2742
+ "ImageTagMutability": "IMMUTABLE",
2743
+ "ImageScanningConfiguration": {"ScanOnPush": False},
2744
+ }
2745
+
2746
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2747
+
2748
+ assert result.name == "immutable-repo"
2749
+ assert result.identifier == "arn:aws:ecr:us-west-2:123456789012:repository/immutable-repo"
2750
+ assert result.status == regscale_models.AssetStatus.Active
2751
+ assert result.location == "us-west-2"
2752
+ assert result.aws_identifier == "arn:aws:ecr:us-west-2:123456789012:repository/immutable-repo"
2753
+ assert result.uri == "123456789012.dkr.ecr.us-west-2.amazonaws.com/immutable-repo"
2754
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2755
+
2756
+ def test_parse_ecr_repository_scan_on_push_enabled(self, mock_aws_integration):
2757
+ """Test parsing ECR repository with scan on push enabled"""
2758
+
2759
+ repo = {
2760
+ "RepositoryName": "scan-enabled-repo",
2761
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/scan-enabled-repo",
2762
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/scan-enabled-repo",
2763
+ "Region": "us-east-1",
2764
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2765
+ }
2766
+
2767
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2768
+
2769
+ assert result.name == "scan-enabled-repo"
2770
+ assert result.identifier == "arn:aws:ecr:us-east-1:123456789012:repository/scan-enabled-repo"
2771
+ assert result.status == regscale_models.AssetStatus.Active
2772
+ assert result.location == "us-east-1"
2773
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2774
+
2775
+ def test_parse_ecr_repository_scan_on_push_disabled(self, mock_aws_integration):
2776
+ """Test parsing ECR repository with scan on push disabled"""
2777
+
2778
+ repo = {
2779
+ "RepositoryName": "scan-disabled-repo",
2780
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/scan-disabled-repo",
2781
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/scan-disabled-repo",
2782
+ "Region": "us-east-1",
2783
+ "ImageScanningConfiguration": {"ScanOnPush": False},
2784
+ }
2785
+
2786
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2787
+
2788
+ assert result.name == "scan-disabled-repo"
2789
+ assert result.identifier == "arn:aws:ecr:us-east-1:123456789012:repository/scan-disabled-repo"
2790
+ assert result.status == regscale_models.AssetStatus.Active
2791
+ assert result.location == "us-east-1"
2792
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2793
+
2794
+ def test_parse_ecr_repository_no_image_tag_mutability(self, mock_aws_integration):
2795
+ """Test parsing ECR repository without image tag mutability"""
2796
+
2797
+ repo = {
2798
+ "RepositoryName": "no-mutability-repo",
2799
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/no-mutability-repo",
2800
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/no-mutability-repo",
2801
+ "Region": "us-east-1",
2802
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2803
+ }
2804
+
2805
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2806
+
2807
+ assert result.name == "no-mutability-repo"
2808
+ assert result.identifier == "arn:aws:ecr:us-east-1:123456789012:repository/no-mutability-repo"
2809
+ assert result.status == regscale_models.AssetStatus.Active
2810
+ assert result.location == "us-east-1"
2811
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2812
+
2813
+ def test_parse_ecr_repository_no_scanning_config(self, mock_aws_integration):
2814
+ """Test parsing ECR repository without scanning configuration"""
2815
+
2816
+ repo = {
2817
+ "RepositoryName": "no-scanning-repo",
2818
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/no-scanning-repo",
2819
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/no-scanning-repo",
2820
+ "Region": "us-east-1",
2821
+ "ImageTagMutability": "MUTABLE",
2822
+ }
2823
+
2824
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2825
+
2826
+ assert result.name == "no-scanning-repo"
2827
+ assert result.identifier == "arn:aws:ecr:us-east-1:123456789012:repository/no-scanning-repo"
2828
+ assert result.status == regscale_models.AssetStatus.Active
2829
+ assert result.location == "us-east-1"
2830
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2831
+
2832
+ def test_parse_ecr_repository_no_name(self, mock_aws_integration):
2833
+ """Test parsing ECR repository without name"""
2834
+
2835
+ repo = {
2836
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/unnamed-repo",
2837
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/unnamed-repo",
2838
+ "Region": "us-east-1",
2839
+ "ImageTagMutability": "MUTABLE",
2840
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2841
+ }
2842
+
2843
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2844
+
2845
+ assert result.name == ""
2846
+ assert result.identifier == "arn:aws:ecr:us-east-1:123456789012:repository/unnamed-repo"
2847
+ assert result.external_id is None
2848
+ assert result.status == regscale_models.AssetStatus.Active
2849
+ assert result.location == "us-east-1"
2850
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2851
+
2852
+ def test_parse_ecr_repository_no_uri(self, mock_aws_integration):
2853
+ """Test parsing ECR repository without URI"""
2854
+
2855
+ repo = {
2856
+ "RepositoryName": "no-uri-repo",
2857
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/no-uri-repo",
2858
+ "Region": "us-east-1",
2859
+ "ImageTagMutability": "MUTABLE",
2860
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2861
+ }
2862
+
2863
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2864
+
2865
+ assert result.name == "no-uri-repo"
2866
+ assert result.uri is None
2867
+ assert result.status == regscale_models.AssetStatus.Active
2868
+ assert result.location == "us-east-1"
2869
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2870
+
2871
+ def test_parse_ecr_repository_no_region(self, mock_aws_integration):
2872
+ """Test parsing ECR repository without region"""
2873
+
2874
+ repo = {
2875
+ "RepositoryName": "no-region-repo",
2876
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/no-region-repo",
2877
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/no-region-repo",
2878
+ "ImageTagMutability": "MUTABLE",
2879
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2880
+ }
2881
+
2882
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2883
+
2884
+ assert result.name == "no-region-repo"
2885
+ assert result.location is None
2886
+ assert result.status == regscale_models.AssetStatus.Active
2887
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2888
+
2889
+ def test_parse_ecr_repository_minimal_data(self, mock_aws_integration):
2890
+ """Test parsing ECR repository with minimal data"""
2891
+
2892
+ repo = {}
2893
+
2894
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2895
+
2896
+ assert result.name == ""
2897
+ assert result.identifier == ""
2898
+ assert result.asset_type == regscale_models.AssetType.Other
2899
+ assert result.asset_category == regscale_models.AssetCategory.Software
2900
+ assert result.component_type == regscale_models.ComponentType.Software
2901
+ assert result.component_names == ["ECR Repositories"]
2902
+ assert result.parent_id == mock_aws_integration.plan_id
2903
+ assert result.parent_module == "securityplans"
2904
+ assert result.status == regscale_models.AssetStatus.Active
2905
+ assert result.location is None
2906
+ assert result.external_id is None
2907
+ assert result.aws_identifier is None
2908
+ assert result.uri is None
2909
+ assert result.manufacturer == "AWS"
2910
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2911
+ assert result.source_data == repo
2912
+
2913
+ def test_parse_ecr_repository_edge_cases(self, mock_aws_integration):
2914
+ """Test parsing ECR repository with edge cases"""
2915
+
2916
+ repo = {
2917
+ "RepositoryName": "edge-case-repo",
2918
+ "RepositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/edge-case-repo",
2919
+ "RepositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/edge-case-repo",
2920
+ "Region": "us-east-1",
2921
+ "ImageTagMutability": "MUTABLE",
2922
+ "ImageScanningConfiguration": {"ScanOnPush": True},
2923
+ }
2924
+
2925
+ result = AWSInventoryIntegration.parse_ecr_repository(mock_aws_integration, repo)
2926
+
2927
+ assert result.name == "edge-case-repo"
2928
+ assert result.identifier == "arn:aws:ecr:us-east-1:123456789012:repository/edge-case-repo"
2929
+ assert result.status == regscale_models.AssetStatus.Active
2930
+ assert result.location == "us-east-1"
2931
+ assert result.external_id == "edge-case-repo"
2932
+ assert result.aws_identifier == "arn:aws:ecr:us-east-1:123456789012:repository/edge-case-repo"
2933
+ assert result.uri == "123456789012.dkr.ecr.us-east-1.amazonaws.com/edge-case-repo"
2934
+ assert result.manufacturer == "AWS"
2935
+ assert result.notes is None # Bug: notes field is not being set in parse_ecr_repository method
2936
+ assert result.source_data == repo
2937
+
2938
+ @pytest.mark.parametrize(
2939
+ "resource_type,expected_baseline",
2940
+ [
2941
+ ("AwsAccount", "AWS Account"),
2942
+ ("AwsS3Bucket", "S3 Bucket"),
2943
+ ("AwsIamRole", "IAM Role"),
2944
+ ("AwsEc2Instance", "EC2 Instance"),
2945
+ ],
2946
+ )
2947
+ def test_maps_known_resource_types(self, resource_type, expected_baseline, mock_aws_integration):
2948
+ """Should map known resource types to correct baselines."""
2949
+ resource = {"Type": resource_type, "Id": f"arn:aws:test::123456789012:resource/{resource_type.lower()}"}
2950
+
2951
+ result = AWSInventoryIntegration.get_baseline(resource)
2952
+
2953
+ assert result == expected_baseline
2954
+
2955
+ @pytest.mark.parametrize(
2956
+ "resource,expected_baseline",
2957
+ [
2958
+ (
2959
+ {"Type": "AwsUnknownResource", "Id": "arn:aws:unknown::123456789012:resource/unknown"},
2960
+ "AwsUnknownResource",
2961
+ ),
2962
+ ({"Id": "arn:aws:unknown::123456789012:resource/missing"}, ""),
2963
+ ({"Type": "", "Id": "arn:aws:unknown::123456789012:resource/empty"}, ""),
2964
+ ({"Type": "Test", "Id": "arn:aws:unknown::123456789012:resource/none"}, "Test"),
2965
+ ],
2966
+ ids=["unknown_resource", "missing_type", "empty_type", "none_type"],
2967
+ )
2968
+ def test_get_baseline_edge_cases(self, resource, expected_baseline, mock_aws_integration):
2969
+ """Should handle various edge cases for get_baseline."""
2970
+ result = AWSInventoryIntegration.get_baseline(resource)
2971
+ assert result == expected_baseline
2972
+
2973
+ @pytest.mark.parametrize(
2974
+ "resource_type,expected_baseline",
2975
+ [
2976
+ ("awsaccount", "awsaccount"), # Should return original since it doesn't match
2977
+ ("AWSACCOUNT", "AWSACCOUNT"), # Should return original since it doesn't match
2978
+ ],
2979
+ ids=["lowercase", "uppercase"],
2980
+ )
2981
+ def test_get_baseline_case_sensitive(self, resource_type, expected_baseline, mock_aws_integration):
2982
+ """Test get_baseline with case variations."""
2983
+ resource = {"Type": resource_type, "Id": "arn:aws:iam::123456789012:root"}
2984
+ result = AWSInventoryIntegration.get_baseline(resource)
2985
+ assert result == expected_baseline
2986
+
2987
+ def test_get_baseline_with_additional_fields(self, mock_aws_integration):
2988
+ """Test get_baseline with resource containing additional fields"""
2989
+ resource = {
2990
+ "Type": "AwsS3Bucket",
2991
+ "Id": "arn:aws:s3:::test-bucket",
2992
+ "Partition": "aws",
2993
+ "Region": "us-east-1",
2994
+ "AdditionalField": "additional_value",
2995
+ }
2996
+ result = AWSInventoryIntegration.get_baseline(resource)
2997
+ assert result == "S3 Bucket"
2998
+
2999
+ @pytest.mark.parametrize(
3000
+ "resource_type,expected_baseline",
3001
+ [
3002
+ ("AwsAccount", "AWS Account"),
3003
+ ("AwsS3Bucket", "S3 Bucket"),
3004
+ ("AwsIamRole", "IAM Role"),
3005
+ ("AwsEc2Instance", "EC2 Instance"),
3006
+ ],
3007
+ ids=["aws_account", "aws_s3_bucket", "aws_iam_role", "aws_ec2_instance"],
3008
+ )
3009
+ def test_get_baseline_all_mapped_types(self, resource_type, expected_baseline, mock_aws_integration):
3010
+ """Test get_baseline with all mapped resource types."""
3011
+ resource = {"Type": resource_type, "Id": f"arn:aws:test::123456789012:resource/{resource_type.lower()}"}
3012
+ result = AWSInventoryIntegration.get_baseline(resource)
3013
+ assert result == expected_baseline
3014
+
3015
+ @pytest.mark.parametrize(
3016
+ "resource_type,expected_baseline",
3017
+ [
3018
+ (" AwsAccount ", " AwsAccount "), # Should return original with whitespace
3019
+ ("AwsAccount@#$%", "AwsAccount@#$%"), # Should return original with special chars
3020
+ ("AwsAccount123", "AwsAccount123"), # Should return original with numbers
3021
+ ],
3022
+ ids=["whitespace", "special_chars", "numbers"],
3023
+ )
3024
+ def test_get_baseline_special_characters(self, resource_type, expected_baseline, mock_aws_integration):
3025
+ """Test get_baseline with various special character cases."""
3026
+ resource = {"Type": resource_type, "Id": "arn:aws:iam::123456789012:root"}
3027
+ result = AWSInventoryIntegration.get_baseline(resource)
3028
+ assert result == expected_baseline
3029
+
3030
+ def test_get_baseline_empty_resource(self, mock_aws_integration):
3031
+ """Test get_baseline with empty resource dictionary"""
3032
+ resource = {}
3033
+ result = AWSInventoryIntegration.get_baseline(resource)
3034
+ assert result == ""
3035
+
3036
+ @pytest.mark.parametrize(
3037
+ "arn,expected_name",
3038
+ [
3039
+ ("arn:aws:iam::123456789012:role/test-role", "test-role"),
3040
+ ("arn:aws:iam::123456789012:role/path/to/test-role", "test-role"),
3041
+ ("arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0", "i-1234567890abcdef0"),
3042
+ ("arn:aws:iam::123456789012:user/test-user", "test-user"),
3043
+ ("arn:aws:iam::123456789012:role/MyRole", "MyRole"),
3044
+ ],
3045
+ )
3046
+ def test_extracts_name_from_arn_with_slash(self, arn, expected_name, mock_aws_integration):
3047
+ """Should extract name from ARN containing slash."""
3048
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3049
+
3050
+ assert result == expected_name
3051
+
3052
+ @pytest.mark.parametrize(
3053
+ "arn",
3054
+ [
3055
+ "arn:aws:s3:::test-bucket",
3056
+ "arn:aws:s3:::my-test-bucket",
3057
+ "arn:aws:lambda:us-east-1:123456789012:function:my-function",
3058
+ "AWS::::Account:123456789012",
3059
+ ],
3060
+ )
3061
+ def test_returns_full_arn_when_no_slash(self, arn, mock_aws_integration):
3062
+ """Should return full ARN when no slash is present."""
3063
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3064
+
3065
+ assert result == arn
3066
+
3067
+ def test_returns_empty_string_for_empty_input(self, mock_aws_integration):
3068
+ """Should return empty string for empty input."""
3069
+ result = AWSInventoryIntegration.extract_name_from_arn("")
3070
+
3071
+ assert result == ""
3072
+
3073
+ @pytest.mark.parametrize(
3074
+ "test_input",
3075
+ [
3076
+ " ", # whitespace only
3077
+ "simple-string", # no slashes or colons
3078
+ "AWS::::Account:123456789012", # AWS account format
3079
+ ],
3080
+ ids=["whitespace", "simple_string", "aws_account_format"],
3081
+ )
3082
+ def test_returns_original_string_for_non_arn_inputs(self, test_input, mock_aws_integration):
3083
+ """Should return original string for non-ARN inputs."""
3084
+ result = AWSInventoryIntegration.extract_name_from_arn(test_input)
3085
+ assert result == test_input
3086
+
3087
+ def test_extract_name_from_arn_complex_path(self, mock_aws_integration):
3088
+ """Test extract_name_from_arn with complex path structure"""
3089
+ arn = "arn:aws:iam::123456789012:role/path/to/subpath/MyComplexRole"
3090
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3091
+ assert result == "MyComplexRole"
3092
+
3093
+ @pytest.mark.parametrize(
3094
+ "arn,expected_name",
3095
+ [
3096
+ ("arn:aws:iam::123456789012:role/test-role@#$%", "test-role@#$%"),
3097
+ ("arn:aws:iam::123456789012:role/role-123-test", "role-123-test"),
3098
+ ("arn:aws:iam::123456789012:role/test_role_name", "test_role_name"),
3099
+ ("arn:aws:iam::123456789012:role/test.role.name", "test.role.name"),
3100
+ ],
3101
+ ids=["special_chars", "numbers", "underscores", "dots"],
3102
+ )
3103
+ def test_extract_name_from_arn_with_characters(self, arn, expected_name, mock_aws_integration):
3104
+ """Test extract_name_from_arn with various character types in the name."""
3105
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3106
+ assert result == expected_name
3107
+
3108
+ def test_extract_name_from_arn_static_method(self, mock_aws_integration):
3109
+ """Test that extract_name_from_arn is a static method and can be called without instance"""
3110
+ arn = "arn:aws:iam::123456789012:role/test-role"
3111
+
3112
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3113
+ assert result == "test-role"
3114
+
3115
+ @pytest.mark.parametrize(
3116
+ "arn,expected_name",
3117
+ [
3118
+ ("arn:aws:iam::123456789012:role/test-role/", ""),
3119
+ ("arn:aws:iam::123456789012:/role/test-role", "test-role"),
3120
+ ("arn:aws:iam::123456789012:role//test-role", "test-role"),
3121
+ ],
3122
+ ids=["trailing_slash", "leading_slash", "multiple_slashes"],
3123
+ )
3124
+ def test_extract_name_from_arn_slash_edge_cases(self, arn, expected_name, mock_aws_integration):
3125
+ """Test extract_name_from_arn with various slash edge cases."""
3126
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3127
+ assert result == expected_name
3128
+
3129
+ @pytest.mark.parametrize(
3130
+ "arn,expected_name",
3131
+ [
3132
+ ("arn:aws:iam::123456789012:root", "arn:aws:iam::123456789012:root"), # No slashes, returns whole string
3133
+ ("arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket"), # No slashes, returns whole string
3134
+ ("arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0", "i-1234567890abcdef0"),
3135
+ ("arn:aws:iam::123456789012:user/JohnDoe", "JohnDoe"),
3136
+ ("arn:aws:iam::123456789012:role/MyRole", "MyRole"),
3137
+ (
3138
+ "arn:aws:lambda:us-east-1:123456789012:function:my-function",
3139
+ "arn:aws:lambda:us-east-1:123456789012:function:my-function",
3140
+ ), # No slashes, returns whole string
3141
+ (
3142
+ "arn:aws:rds:us-east-1:123456789012:db:my-database",
3143
+ "arn:aws:rds:us-east-1:123456789012:db:my-database",
3144
+ ), # No slashes, returns whole string
3145
+ (
3146
+ "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/1234567890abcdef0",
3147
+ "1234567890abcdef0",
3148
+ ),
3149
+ ],
3150
+ ids=[
3151
+ "iam_root",
3152
+ "s3_bucket",
3153
+ "ec2_instance",
3154
+ "iam_user",
3155
+ "iam_role",
3156
+ "lambda_function",
3157
+ "rds_database",
3158
+ "load_balancer",
3159
+ ],
3160
+ )
3161
+ def test_extract_name_from_arn_real_aws_examples(self, arn, expected_name, mock_aws_integration):
3162
+ """Test extract_name_from_arn with real AWS ARN examples."""
3163
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3164
+ assert result == expected_name
3165
+
3166
+ @pytest.mark.parametrize(
3167
+ "arn,expected_name",
3168
+ [
3169
+ ("test:value", "test:value"), # No slashes, so returns whole string
3170
+ ("test/value", "value"),
3171
+ ("a", "a"),
3172
+ ],
3173
+ ids=["colon_only", "slash_only", "single_char"],
3174
+ )
3175
+ def test_extract_name_from_arn_minimal_arns(self, arn, expected_name, mock_aws_integration):
3176
+ """Test extract_name_from_arn with minimal ARN structures."""
3177
+ result = AWSInventoryIntegration.extract_name_from_arn(arn)
3178
+ assert result == expected_name
3179
+
3180
+ def test_extract_name_from_arn_mixed_separators(self, mock_aws_integration):
3181
+ """Test extract_name_from_arn with mixed slash and colon separators"""
3182
+
3183
+ arn_mixed = "arn:aws:iam::123456789012:role/path:to:role"
3184
+ result_mixed = AWSInventoryIntegration.extract_name_from_arn(arn_mixed)
3185
+ assert result_mixed == "path:to:role" # Gets the last part after the last slash
3186
+
3187
+ def test_parse_finding_basic_success(self):
3188
+ """Test parse_finding with basic successful finding"""
3189
+ # Create a real instance of AWSInventoryIntegration
3190
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3191
+
3192
+ finding = {
3193
+ "Title": "Test Security Finding",
3194
+ "Description": "This is a test security finding description",
3195
+ "CreatedAt": "2023-01-01T00:00:00Z",
3196
+ "Types": ["Software and Configuration Checks"],
3197
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3198
+ "Compliance": {"Status": "FAILED"},
3199
+ "Remediation": {"Recommendation": {"Text": "Fix this security issue", "Url": "https://example.com/fix"}},
3200
+ "FindingProviderFields": {"Severity": {"Label": "HIGH"}},
3201
+ }
3202
+
3203
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3204
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3205
+ ) as mock_comments, patch(
3206
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3207
+ ) as mock_severity, patch(
3208
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3209
+ ) as mock_due_date, patch(
3210
+ "regscale.integrations.commercial.aws.scanner.date_str"
3211
+ ) as mock_date_str, patch(
3212
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3213
+ ) as mock_datetime_str:
3214
+
3215
+ mock_status.return_value = ("Fail", "Test results")
3216
+ mock_comments.return_value = "Test comments with Finding Severity: HIGH"
3217
+ mock_severity.return_value = "HIGH"
3218
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3219
+ mock_date_str.return_value = "2023-01-01"
3220
+ mock_datetime_str.return_value = "2023-02-01"
3221
+
3222
+ aws_integration.app = MagicMock()
3223
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3224
+
3225
+ results = aws_integration.parse_finding(finding)
3226
+
3227
+ assert len(results) == 1
3228
+ finding_result = results[0]
3229
+ assert (
3230
+ finding_result.asset_identifier == "arn:aws:iam::123456789012:root"
3231
+ ) # extract_name_from_arn returns full ARN when no slashes
3232
+ assert finding_result.external_id == "" # No finding ID provided in test data
3233
+ assert finding_result.title == "Test Security Finding"
3234
+ assert finding_result.category == "SecurityHub"
3235
+ assert finding_result.issue_title == "Test Security Finding"
3236
+ assert finding_result.severity == regscale_models.IssueSeverity.High
3237
+ assert finding_result.description == "This is a test security finding description"
3238
+ assert finding_result.status == regscale_models.IssueStatus.Open
3239
+ assert finding_result.checklist_status == regscale_models.ChecklistStatus.FAIL
3240
+ assert finding_result.results == "Test results"
3241
+ assert finding_result.recommendation_for_mitigation == "Fix this security issue"
3242
+ assert finding_result.comments == "Test comments with Finding Severity: HIGH"
3243
+ assert finding_result.poam_comments == "Test comments with Finding Severity: HIGH"
3244
+ assert finding_result.date_created == "2023-01-01"
3245
+ assert finding_result.due_date == "2023-02-01"
3246
+ assert finding_result.plugin_name == "Software and Configuration Checks"
3247
+ assert finding_result.baseline == "AWS Account"
3248
+ assert finding_result.observations == "Test comments with Finding Severity: HIGH"
3249
+ assert finding_result.vulnerability_type == "Vulnerability Scan"
3250
+
3251
+ def test_parse_finding_multiple_resources(self):
3252
+ """Test parse_finding with multiple resources"""
3253
+ # Create a real instance of AWSInventoryIntegration
3254
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3255
+
3256
+ finding = {
3257
+ "Title": "Multi-Resource Finding",
3258
+ "Description": "Finding affecting multiple resources",
3259
+ "CreatedAt": "2023-01-01T00:00:00Z",
3260
+ "Types": ["Software and Configuration Checks"],
3261
+ "Resources": [
3262
+ {"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"},
3263
+ {"Type": "AwsS3Bucket", "Id": "arn:aws:s3:::test-bucket"},
3264
+ ],
3265
+ "Compliance": {"Status": "PASSED"},
3266
+ "Remediation": {"Recommendation": {"Text": "No action needed", "Url": "https://example.com/info"}},
3267
+ "FindingProviderFields": {"Severity": {"Label": "LOW"}},
3268
+ }
3269
+
3270
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3271
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3272
+ ) as mock_comments, patch(
3273
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3274
+ ) as mock_severity, patch(
3275
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3276
+ ) as mock_due_date, patch(
3277
+ "regscale.integrations.commercial.aws.scanner.date_str"
3278
+ ) as mock_date_str, patch(
3279
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3280
+ ) as mock_datetime_str:
3281
+
3282
+ mock_status.return_value = ("Pass", "Passed test")
3283
+ mock_comments.return_value = "Test comments with Finding Severity: LOW"
3284
+ mock_severity.return_value = "LOW"
3285
+ mock_due_date.return_value = "2023-04-01T00:00:00Z"
3286
+ mock_date_str.return_value = "2023-01-01"
3287
+ mock_datetime_str.return_value = "2023-04-01"
3288
+
3289
+ aws_integration.app = MagicMock()
3290
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3291
+
3292
+ results = aws_integration.parse_finding(finding)
3293
+
3294
+ # Should create one finding per resource
3295
+ assert len(results) == 2
3296
+
3297
+ # Check first resource (AWS Account)
3298
+ assert (
3299
+ results[0].asset_identifier == "arn:aws:iam::123456789012:root"
3300
+ ) # extract_name_from_arn returns full ARN when no slashes
3301
+ assert results[0].baseline == "AWS Account"
3302
+ assert results[0].status == regscale_models.IssueStatus.Open # Default status when no config
3303
+
3304
+ # Check second resource (S3 Bucket)
3305
+ assert (
3306
+ results[1].asset_identifier == "arn:aws:s3:::test-bucket"
3307
+ ) # extract_name_from_arn returns full ARN when no slashes
3308
+ assert results[1].baseline == "S3 Bucket" # get_baseline maps AwsS3Bucket to "S3 Bucket"
3309
+ assert results[1].status == regscale_models.IssueStatus.Open # Default status when no config
3310
+
3311
+ def test_parse_finding_missing_severity_config(self):
3312
+ """Test parse_finding when severity config is missing"""
3313
+ # Create a real instance of AWSInventoryIntegration
3314
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3315
+
3316
+ finding = {
3317
+ "Title": "Test Finding",
3318
+ "Description": "Test description",
3319
+ "CreatedAt": "2023-01-01T00:00:00Z",
3320
+ "Types": ["Software and Configuration Checks"],
3321
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3322
+ "Compliance": {"Status": "FAILED"},
3323
+ "Remediation": {"Recommendation": {"Text": "Fix this", "Url": "https://example.com/fix"}},
3324
+ "FindingProviderFields": {"Severity": {"Label": "UNKNOWN"}},
3325
+ }
3326
+
3327
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3328
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3329
+ ) as mock_comments, patch(
3330
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3331
+ ) as mock_severity, patch(
3332
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3333
+ ) as mock_due_date, patch(
3334
+ "regscale.integrations.commercial.aws.scanner.date_str"
3335
+ ) as mock_date_str, patch(
3336
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3337
+ ) as mock_datetime_str:
3338
+
3339
+ mock_status.return_value = ("Fail", "Test results")
3340
+ mock_comments.return_value = "Test comments with Finding Severity: UNKNOWN"
3341
+ mock_severity.return_value = "UNKNOWN"
3342
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3343
+ mock_date_str.return_value = "2023-01-01"
3344
+ mock_datetime_str.return_value = "2023-02-01"
3345
+
3346
+ aws_integration.app = MagicMock()
3347
+ aws_integration.app.config = {
3348
+ "issues": {
3349
+ "amazon": {
3350
+ "high": 30,
3351
+ "moderate": 60,
3352
+ # Missing "low" mapping
3353
+ }
3354
+ }
3355
+ }
3356
+
3357
+ results = aws_integration.parse_finding(finding)
3358
+
3359
+ # Should still create a finding with default 30 days
3360
+ assert len(results) == 1
3361
+ assert results[0].due_date == "2023-02-01"
3362
+
3363
+ def test_parse_finding_missing_remediation(self):
3364
+ """Test parse_finding when remediation information is missing"""
3365
+ # Create a real instance of AWSInventoryIntegration
3366
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3367
+
3368
+ finding = {
3369
+ "Title": "Test Finding",
3370
+ "Description": "Test description",
3371
+ "CreatedAt": "2023-01-01T00:00:00Z",
3372
+ "Types": ["Software and Configuration Checks"],
3373
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3374
+ "Compliance": {"Status": "FAILED"},
3375
+ "FindingProviderFields": {"Severity": {"Label": "HIGH"}},
3376
+ # Missing Remediation field
3377
+ }
3378
+
3379
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3380
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3381
+ ) as mock_comments, patch(
3382
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3383
+ ) as mock_severity, patch(
3384
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3385
+ ) as mock_due_date, patch(
3386
+ "regscale.integrations.commercial.aws.scanner.date_str"
3387
+ ) as mock_date_str, patch(
3388
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3389
+ ) as mock_datetime_str:
3390
+
3391
+ mock_status.return_value = ("Fail", "Test results")
3392
+ mock_comments.return_value = "Test comments with Finding Severity: HIGH"
3393
+ mock_severity.return_value = "HIGH"
3394
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3395
+ mock_date_str.return_value = "2023-01-01"
3396
+ mock_datetime_str.return_value = "2023-02-01"
3397
+
3398
+ aws_integration.app = MagicMock()
3399
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3400
+
3401
+ results = aws_integration.parse_finding(finding)
3402
+
3403
+ assert len(results) == 1
3404
+ # Should handle missing remediation gracefully
3405
+ assert results[0].recommendation_for_mitigation == ""
3406
+
3407
+ def test_parse_finding_missing_types(self):
3408
+ """Test parse_finding when Types field is missing"""
3409
+ # Create a real instance of AWSInventoryIntegration
3410
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3411
+
3412
+ finding = {
3413
+ "Title": "Test Finding",
3414
+ "Description": "Test description",
3415
+ "CreatedAt": "2023-01-01T00:00:00Z",
3416
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3417
+ "Compliance": {"Status": "FAILED"},
3418
+ "Remediation": {"Recommendation": {"Text": "Fix this", "Url": "https://example.com/fix"}},
3419
+ "FindingProviderFields": {"Severity": {"Label": "HIGH"}},
3420
+ # Missing Types field
3421
+ }
3422
+
3423
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3424
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3425
+ ) as mock_comments, patch(
3426
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3427
+ ) as mock_severity, patch(
3428
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3429
+ ) as mock_due_date, patch(
3430
+ "regscale.integrations.commercial.aws.scanner.date_str"
3431
+ ) as mock_date_str, patch(
3432
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3433
+ ) as mock_datetime_str:
3434
+
3435
+ mock_status.return_value = ("Fail", "Test results")
3436
+ mock_comments.return_value = "Test comments with Finding Severity: HIGH"
3437
+ mock_severity.return_value = "HIGH"
3438
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3439
+ mock_date_str.return_value = "2023-01-01"
3440
+ mock_datetime_str.return_value = "2023-02-01"
3441
+
3442
+ aws_integration.app = MagicMock()
3443
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3444
+
3445
+ # This should handle the missing Types gracefully and create one finding per resource
3446
+ results = aws_integration.parse_finding(finding)
3447
+ assert len(results) == 1
3448
+
3449
+ def test_parse_finding_empty_types(self):
3450
+ """Test parse_finding when Types field is empty"""
3451
+ # Create a real instance of AWSInventoryIntegration
3452
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3453
+
3454
+ finding = {
3455
+ "Title": "Test Finding",
3456
+ "Description": "Test description",
3457
+ "CreatedAt": "2023-01-01T00:00:00Z",
3458
+ "Types": [], # Empty types list
3459
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3460
+ "Compliance": {"Status": "FAILED"},
3461
+ "Remediation": {"Recommendation": {"Text": "Fix this", "Url": "https://example.com/fix"}},
3462
+ "FindingProviderFields": {"Severity": {"Label": "HIGH"}},
3463
+ }
3464
+
3465
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3466
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3467
+ ) as mock_comments, patch(
3468
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3469
+ ) as mock_severity, patch(
3470
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3471
+ ) as mock_due_date, patch(
3472
+ "regscale.integrations.commercial.aws.scanner.date_str"
3473
+ ) as mock_date_str, patch(
3474
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3475
+ ) as mock_datetime_str:
3476
+
3477
+ mock_status.return_value = ("Fail", "Test results")
3478
+ mock_comments.return_value = "Test comments with Finding Severity: HIGH"
3479
+ mock_severity.return_value = "HIGH"
3480
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3481
+ mock_date_str.return_value = "2023-01-01"
3482
+ mock_datetime_str.return_value = "2023-02-01"
3483
+
3484
+ aws_integration.app = MagicMock()
3485
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3486
+
3487
+ # This should handle the empty Types gracefully and create one finding per resource
3488
+ results = aws_integration.parse_finding(finding)
3489
+ assert len(results) == 1
3490
+
3491
+ def test_parse_finding_exception_handling(self):
3492
+ """Test parse_finding exception handling"""
3493
+ # Create a real instance of AWSInventoryIntegration
3494
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3495
+
3496
+ finding = {
3497
+ "Title": "Test Finding",
3498
+ "Description": "Test description",
3499
+ "CreatedAt": "2023-01-01T00:00:00Z",
3500
+ "Types": ["Software and Configuration Checks"],
3501
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3502
+ "Compliance": {"Status": "FAILED"},
3503
+ "Remediation": {"Recommendation": {"Text": "Fix this", "Url": "https://example.com/fix"}},
3504
+ "FindingProviderFields": {"Severity": {"Label": "HIGH"}},
3505
+ }
3506
+
3507
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status:
3508
+
3509
+ # Make determine_status_and_results raise an exception
3510
+ mock_status.side_effect = Exception("Test exception")
3511
+
3512
+ # Should handle exception gracefully and return empty list
3513
+ results = aws_integration.parse_finding(finding)
3514
+
3515
+ assert len(results) == 0
3516
+
3517
+ @pytest.mark.parametrize(
3518
+ "severity_label,friendly_sev,expected_severity",
3519
+ [
3520
+ ("CRITICAL", "high", regscale_models.IssueSeverity.High),
3521
+ ("HIGH", "high", regscale_models.IssueSeverity.High),
3522
+ ("MEDIUM", "moderate", regscale_models.IssueSeverity.Moderate),
3523
+ ("MODERATE", "moderate", None), # MODERATE is not in the mapping, so it returns None
3524
+ ("LOW", "low", regscale_models.IssueSeverity.Low),
3525
+ ("UNKNOWN", "low", None), # UNKNOWN is not in the mapping, so it returns None
3526
+ ],
3527
+ ids=["critical", "high", "medium", "moderate", "low", "unknown"],
3528
+ )
3529
+ def test_parse_finding_different_severities(self, severity_label, friendly_sev, expected_severity):
3530
+ """Test parse_finding with different severity levels."""
3531
+ # Create a real instance of AWSInventoryIntegration
3532
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3533
+
3534
+ finding = {
3535
+ "Title": f"Test {severity_label} Finding",
3536
+ "Description": "Test description",
3537
+ "CreatedAt": "2023-01-01T00:00:00Z",
3538
+ "Types": ["Software and Configuration Checks"],
3539
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3540
+ "Compliance": {"Status": "FAILED"},
3541
+ "Remediation": {"Recommendation": {"Text": "Fix this", "Url": "https://example.com/fix"}},
3542
+ "FindingProviderFields": {"Severity": {"Label": severity_label}},
3543
+ }
3544
+
3545
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3546
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3547
+ ) as mock_comments, patch(
3548
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3549
+ ) as mock_severity, patch(
3550
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3551
+ ) as mock_due_date, patch(
3552
+ "regscale.integrations.commercial.aws.scanner.date_str"
3553
+ ) as mock_date_str, patch(
3554
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3555
+ ) as mock_datetime_str:
3556
+
3557
+ mock_status.return_value = ("Fail", "Test results")
3558
+ mock_comments.return_value = f"Test comments with Finding Severity: {severity_label}"
3559
+ mock_severity.return_value = severity_label
3560
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3561
+ mock_date_str.return_value = "2023-01-01"
3562
+ mock_datetime_str.return_value = "2023-02-01"
3563
+
3564
+ aws_integration.app = MagicMock()
3565
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3566
+
3567
+ results = aws_integration.parse_finding(finding)
3568
+
3569
+ assert len(results) == 1
3570
+ assert results[0].severity == expected_severity
3571
+
3572
+ @pytest.mark.parametrize(
3573
+ "status,expected_issue_status,expected_checklist_status",
3574
+ [
3575
+ ("Fail", regscale_models.IssueStatus.Open, regscale_models.ChecklistStatus.FAIL),
3576
+ (
3577
+ "Pass",
3578
+ regscale_models.IssueStatus.Open,
3579
+ regscale_models.ChecklistStatus.PASS,
3580
+ ), # Defaults to Open when no config
3581
+ (
3582
+ "Unknown",
3583
+ regscale_models.IssueStatus.Open,
3584
+ regscale_models.ChecklistStatus.NOT_REVIEWED,
3585
+ ), # Defaults to Open when no config
3586
+ ],
3587
+ ids=["fail", "pass", "unknown"],
3588
+ )
3589
+ def test_parse_finding_different_statuses(self, status, expected_issue_status, expected_checklist_status):
3590
+ """Test parse_finding with different status values."""
3591
+ # Create a real instance of AWSInventoryIntegration
3592
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3593
+
3594
+ finding = {
3595
+ "Title": f"Test {status} Finding",
3596
+ "Description": "Test description",
3597
+ "CreatedAt": "2023-01-01T00:00:00Z",
3598
+ "Types": ["Software and Configuration Checks"],
3599
+ "Resources": [{"Type": "AwsAccount", "Id": "arn:aws:iam::123456789012:root"}],
3600
+ "Compliance": {"Status": "FAILED" if status == "Fail" else "PASSED"},
3601
+ "Remediation": {"Recommendation": {"Text": "Fix this", "Url": "https://example.com/fix"}},
3602
+ "FindingProviderFields": {"Severity": {"Label": "HIGH"}},
3603
+ }
3604
+
3605
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3606
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3607
+ ) as mock_comments, patch(
3608
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3609
+ ) as mock_severity, patch(
3610
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3611
+ ) as mock_due_date, patch(
3612
+ "regscale.integrations.commercial.aws.scanner.date_str"
3613
+ ) as mock_date_str, patch(
3614
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3615
+ ) as mock_datetime_str:
3616
+
3617
+ mock_status.return_value = (status, f"{status} results")
3618
+ mock_comments.return_value = "Test comments with Finding Severity: HIGH"
3619
+ mock_severity.return_value = "HIGH"
3620
+ mock_due_date.return_value = "2023-02-01T00:00:00Z"
3621
+ mock_date_str.return_value = "2023-01-01"
3622
+ mock_datetime_str.return_value = "2023-02-01"
3623
+
3624
+ aws_integration.app = MagicMock()
3625
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3626
+
3627
+ results = aws_integration.parse_finding(finding)
3628
+
3629
+ assert len(results) == 1
3630
+
3631
+ assert results[0].status == expected_issue_status
3632
+ assert results[0].checklist_status == expected_checklist_status
3633
+
3634
+ def test_parse_finding_real_aws_finding_structure(self):
3635
+ """Test parse_finding with real AWS Security Hub finding structure"""
3636
+ # Create a real instance of AWSInventoryIntegration
3637
+ aws_integration = AWSInventoryIntegration(plan_id=1)
3638
+
3639
+ finding = {
3640
+ "SchemaVersion": "2018-10-08",
3641
+ "Id": "arn:aws:securityhub:us-east-1:132360893372:security-control/Config.1/finding/6e568eb7-ea14-46ca-87f3-e8a6efbe805f",
3642
+ "ProductArn": "arn:aws:securityhub:us-east-1::product/aws/securityhub",
3643
+ "ProductName": "Security Hub",
3644
+ "CompanyName": "AWS",
3645
+ "Region": "us-east-1",
3646
+ "GeneratorId": "security-control/Config.1",
3647
+ "AwsAccountId": "132360893372",
3648
+ "Types": ["Software and Configuration Checks/Industry and Regulatory Standards"],
3649
+ "FirstObservedAt": "2023-04-26T13:21:29.696Z",
3650
+ "LastObservedAt": "2023-05-02T08:13:56.971Z",
3651
+ "CreatedAt": "2023-04-26T13:21:29.696Z",
3652
+ "UpdatedAt": "2023-05-02T08:13:51.803Z",
3653
+ "Severity": {"Label": "MEDIUM", "Normalized": 40, "Original": "MEDIUM"},
3654
+ "Title": "AWS Config should be enabled",
3655
+ "Description": "This AWS control checks whether the Config service is enabled in the account for the local region and is recording all resources.",
3656
+ "Remediation": {
3657
+ "Recommendation": {
3658
+ "Text": "For information on how to correct this issue, consult the AWS Security Hub controls documentation.",
3659
+ "Url": "https://docs.aws.amazon.com/console/securityhub/Config.1/remediation",
3660
+ }
3661
+ },
3662
+ "ProductFields": {
3663
+ "aws/securityhub/ProductName": "Security Hub",
3664
+ "aws/securityhub/CompanyName": "AWS",
3665
+ "Resources:0/Id": "arn:aws:iam::132360893372:root",
3666
+ "aws/securityhub/FindingId": "arn:aws:securityhub:us-east-1::product/aws/securityhub/arn:aws:securityhub:us-east-1:132360893372:security-control/Config.1/finding/6e568eb7-ea14-46ca-87f3-e8a6efbe805f",
3667
+ },
3668
+ "Resources": [
3669
+ {"Type": "AwsAccount", "Id": "AWS::::Account:132360893372", "Partition": "aws", "Region": "us-east-1"}
3670
+ ],
3671
+ "Compliance": {
3672
+ "Status": "FAILED",
3673
+ "RelatedRequirements": [
3674
+ "NIST.800-53.r5 CM-3",
3675
+ "NIST.800-53.r5 CM-6(1)",
3676
+ "NIST.800-53.r5 CM-8",
3677
+ "NIST.800-53.r5 CM-8(2)",
3678
+ "CIS AWS Foundations Benchmark v1.2.0/2.5",
3679
+ ],
3680
+ "SecurityControlId": "Config.1",
3681
+ "AssociatedStandards": [
3682
+ {"StandardsId": "standards/nist-800-53/v/5.0.0"},
3683
+ {"StandardsId": "ruleset/cis-aws-foundations-benchmark/v/1.2.0"},
3684
+ {"StandardsId": "standards/aws-foundational-security-best-practices/v/1.0.0"},
3685
+ ],
3686
+ },
3687
+ "WorkflowState": "NEW",
3688
+ "Workflow": {"Status": "NEW"},
3689
+ "RecordState": "ACTIVE",
3690
+ "FindingProviderFields": {
3691
+ "Severity": {"Label": "MEDIUM", "Original": "MEDIUM"},
3692
+ "Types": ["Software and Configuration Checks/Industry and Regulatory Standards"],
3693
+ },
3694
+ }
3695
+
3696
+ with patch("regscale.integrations.commercial.aws.scanner.determine_status_and_results") as mock_status, patch(
3697
+ "regscale.integrations.commercial.aws.scanner.get_comments"
3698
+ ) as mock_comments, patch(
3699
+ "regscale.integrations.commercial.aws.scanner.check_finding_severity"
3700
+ ) as mock_severity, patch(
3701
+ "regscale.integrations.commercial.aws.scanner.get_due_date"
3702
+ ) as mock_due_date, patch(
3703
+ "regscale.integrations.commercial.aws.scanner.date_str"
3704
+ ) as mock_date_str, patch(
3705
+ "regscale.integrations.commercial.aws.scanner.datetime_str"
3706
+ ) as mock_datetime_str:
3707
+
3708
+ mock_status.return_value = (
3709
+ "Fail",
3710
+ "NIST.800-53.r5 CM-3, NIST.800-53.r5 CM-6(1), NIST.800-53.r5 CM-8, NIST.800-53.r5 CM-8(2), CIS AWS Foundations Benchmark v1.2.0/2.5",
3711
+ )
3712
+ mock_comments.return_value = "For information on how to correct this issue, consult the AWS Security Hub controls documentation.<br></br>https://docs.aws.amazon.com/console/securityhub/Config.1/remediation<br></br>Finding Severity: MEDIUM"
3713
+ mock_severity.return_value = "MEDIUM"
3714
+ mock_due_date.return_value = "2023-06-25T00:00:00Z"
3715
+ mock_date_str.return_value = "2023-04-26"
3716
+ mock_datetime_str.return_value = "2023-06-25"
3717
+
3718
+ aws_integration.app = MagicMock()
3719
+ aws_integration.app.config = {"issues": {"amazon": {"high": 30, "moderate": 60, "low": 90}}}
3720
+
3721
+ results = aws_integration.parse_finding(finding)
3722
+
3723
+ assert len(results) == 1
3724
+ finding_result = results[0]
3725
+ assert (
3726
+ finding_result.asset_identifier == "AWS::::Account:132360893372"
3727
+ ) # extract_name_from_arn returns full ARN when no slashes
3728
+ assert (
3729
+ finding_result.external_id
3730
+ == "arn:aws:securityhub:us-east-1:132360893372:security-control/Config.1/finding/6e568eb7-ea14-46ca-87f3-e8a6efbe805f"
3731
+ )
3732
+ assert finding_result.title == "AWS Config should be enabled"
3733
+ assert finding_result.category == "SecurityHub"
3734
+ assert finding_result.severity == regscale_models.IssueSeverity.Moderate
3735
+ assert finding_result.status == regscale_models.IssueStatus.Open
3736
+ assert finding_result.checklist_status == regscale_models.ChecklistStatus.FAIL
3737
+ assert finding_result.plugin_name == "Software and Configuration Checks/Industry and Regulatory Standards"
3738
+ assert finding_result.baseline == "AWS Account" # get_baseline maps AwsAccount to "AWS Account"
3739
+ assert (
3740
+ finding_result.results
3741
+ == "NIST.800-53.r5 CM-3, NIST.800-53.r5 CM-6(1), NIST.800-53.r5 CM-8, NIST.800-53.r5 CM-8(2), CIS AWS Foundations Benchmark v1.2.0/2.5"
3742
+ )