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,1421 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Unit and integration tests for control_matcher module
5
+
6
+ This module provides comprehensive test coverage for the ControlMatcher class,
7
+ including control ID parsing, catalog searches, implementation matching, and caching.
8
+ """
9
+
10
+ import logging
11
+ from typing import Dict, List, Optional, Tuple
12
+ from unittest.mock import MagicMock, Mock, patch
13
+
14
+ import pytest
15
+
16
+ from regscale.core.app.api import Api
17
+ from regscale.core.app.application import Application
18
+ from regscale.integrations.control_matcher import ControlMatcher
19
+ from regscale.models.regscale_models.control_implementation import ControlImplementation
20
+ from regscale.models.regscale_models.security_control import SecurityControl
21
+
22
+
23
+ class TestControlMatcherInit:
24
+ """Test cases for ControlMatcher initialization"""
25
+
26
+ @patch("regscale.integrations.control_matcher.Api")
27
+ @patch("regscale.integrations.control_matcher.Application")
28
+ def test_init_with_no_app(self, mock_app_class, mock_api_class):
29
+ """Test ControlMatcher initialization without providing an app"""
30
+ mock_app = MagicMock(spec=Application)
31
+ mock_app_class.return_value = mock_app
32
+ mock_api = MagicMock(spec=Api)
33
+ mock_api_class.return_value = mock_api
34
+
35
+ matcher = ControlMatcher()
36
+
37
+ mock_app_class.assert_called_once()
38
+ mock_api_class.assert_called_once()
39
+ assert matcher.app == mock_app
40
+ assert matcher.api == mock_api
41
+ assert matcher._catalog_cache == {}
42
+ assert matcher._control_impl_cache == {}
43
+
44
+ @patch("regscale.integrations.control_matcher.Api")
45
+ @patch("regscale.integrations.control_matcher.Application")
46
+ def test_init_with_app(self, mock_app_class, mock_api_class):
47
+ """Test ControlMatcher initialization with an app instance"""
48
+ # Create a mock app that is truthy (has a return value)
49
+ mock_app = Mock(spec=Application)
50
+ # Set a non-None return value to make the mock truthy
51
+ mock_app.return_value = MagicMock()
52
+
53
+ mock_api = MagicMock(spec=Api)
54
+ mock_api_class.return_value = mock_api
55
+
56
+ matcher = ControlMatcher(app=mock_app)
57
+
58
+ # When app is provided and is truthy, it should be used
59
+ assert matcher.app == mock_app
60
+ assert matcher.api == mock_api
61
+ assert matcher._catalog_cache == {}
62
+ assert matcher._control_impl_cache == {}
63
+
64
+
65
+ class TestControlMatcherParseControlId:
66
+ """Test cases for parse_control_id method"""
67
+
68
+ @patch("regscale.integrations.control_matcher.Api")
69
+ @patch("regscale.integrations.control_matcher.Application")
70
+ def test_parse_control_id_none(self, mock_app_class, mock_api_class):
71
+ """Test parsing None control ID returns None"""
72
+ matcher = ControlMatcher()
73
+ result = matcher.parse_control_id(None)
74
+ assert result is None
75
+
76
+ @patch("regscale.integrations.control_matcher.Api")
77
+ @patch("regscale.integrations.control_matcher.Application")
78
+ def test_parse_control_id_empty_string(self, mock_app_class, mock_api_class):
79
+ """Test parsing empty string returns None"""
80
+ matcher = ControlMatcher()
81
+ result = matcher.parse_control_id("")
82
+ assert result is None
83
+
84
+ @patch("regscale.integrations.control_matcher.Api")
85
+ @patch("regscale.integrations.control_matcher.Application")
86
+ def test_parse_control_id_whitespace(self, mock_app_class, mock_api_class):
87
+ """Test parsing whitespace-only string returns None"""
88
+ matcher = ControlMatcher()
89
+ result = matcher.parse_control_id(" ")
90
+ assert result is None
91
+
92
+ @patch("regscale.integrations.control_matcher.Api")
93
+ @patch("regscale.integrations.control_matcher.Application")
94
+ def test_parse_control_id_basic_format(self, mock_app_class, mock_api_class):
95
+ """Test parsing basic NIST control ID format"""
96
+ matcher = ControlMatcher()
97
+ test_cases = [
98
+ ("AC-1", "AC-1"),
99
+ ("ac-1", "AC-1"),
100
+ ("AC-10", "AC-10"),
101
+ ("SI-2", "SI-2"),
102
+ ("CM-6", "CM-6"),
103
+ ]
104
+
105
+ for input_id, expected in test_cases:
106
+ result = matcher.parse_control_id(input_id)
107
+ assert result == expected, f"Failed for input {input_id}"
108
+
109
+ @patch("regscale.integrations.control_matcher.Api")
110
+ @patch("regscale.integrations.control_matcher.Application")
111
+ def test_parse_control_id_with_parentheses(self, mock_app_class, mock_api_class):
112
+ """Test parsing control ID with parentheses converts to dots"""
113
+ matcher = ControlMatcher()
114
+ test_cases = [
115
+ ("AC-1(1)", "AC-1.1"),
116
+ ("ac-2(3)", "AC-2.3"),
117
+ ("SI-4(10)", "SI-4.10"),
118
+ ("CM-6(1)", "CM-6.1"),
119
+ ]
120
+
121
+ for input_id, expected in test_cases:
122
+ result = matcher.parse_control_id(input_id)
123
+ assert result == expected, f"Failed for input {input_id}"
124
+
125
+ @patch("regscale.integrations.control_matcher.Api")
126
+ @patch("regscale.integrations.control_matcher.Application")
127
+ def test_parse_control_id_with_dots(self, mock_app_class, mock_api_class):
128
+ """Test parsing control ID already with dot notation"""
129
+ matcher = ControlMatcher()
130
+ test_cases = [
131
+ ("AC-1.1", "AC-1.1"),
132
+ ("ac-2.5", "AC-2.5"),
133
+ ("SI-4.12", "SI-4.12"),
134
+ ]
135
+
136
+ for input_id, expected in test_cases:
137
+ result = matcher.parse_control_id(input_id)
138
+ assert result == expected, f"Failed for input {input_id}"
139
+
140
+ @patch("regscale.integrations.control_matcher.Api")
141
+ @patch("regscale.integrations.control_matcher.Application")
142
+ def test_parse_control_id_with_text(self, mock_app_class, mock_api_class):
143
+ """Test parsing control ID with descriptive text"""
144
+ matcher = ControlMatcher()
145
+ test_cases = [
146
+ ("Access Control AC-1", "AC-1"),
147
+ ("AC-1 Access Control Policy", "AC-1"),
148
+ ("NIST Control AC-2 Account Management", "AC-2"),
149
+ ("System Monitoring SI-4(5)", "SI-4.5"),
150
+ ]
151
+
152
+ for input_id, expected in test_cases:
153
+ result = matcher.parse_control_id(input_id)
154
+ assert result == expected, f"Failed for input {input_id}"
155
+
156
+ @patch("regscale.integrations.control_matcher.Api")
157
+ @patch("regscale.integrations.control_matcher.Application")
158
+ def test_parse_control_id_multiple_controls_returns_first(self, mock_app_class, mock_api_class):
159
+ """Test parsing string with multiple controls returns first one"""
160
+ matcher = ControlMatcher()
161
+ test_cases = [
162
+ ("AC-1, AC-2", "AC-1"),
163
+ ("SI-2, SI-4, CM-6", "SI-2"),
164
+ ("AC-1(1), AC-1(2)", "AC-1.1"),
165
+ ]
166
+
167
+ for input_id, expected in test_cases:
168
+ result = matcher.parse_control_id(input_id)
169
+ assert result == expected, f"Failed for input {input_id}"
170
+
171
+ @patch("regscale.integrations.control_matcher.Api")
172
+ @patch("regscale.integrations.control_matcher.Application")
173
+ def test_parse_control_id_three_letter_family(self, mock_app_class, mock_api_class):
174
+ """Test parsing control IDs with three-letter family codes"""
175
+ matcher = ControlMatcher()
176
+ test_cases = [
177
+ ("PTA-1", "PTA-1"),
178
+ ("SAR-10", "SAR-10"),
179
+ ("PRM-3(2)", "PRM-3.2"),
180
+ ]
181
+
182
+ for input_id, expected in test_cases:
183
+ result = matcher.parse_control_id(input_id)
184
+ assert result == expected, f"Failed for input {input_id}"
185
+
186
+ @patch("regscale.integrations.control_matcher.Api")
187
+ @patch("regscale.integrations.control_matcher.Application")
188
+ def test_parse_control_id_no_match(self, mock_app_class, mock_api_class):
189
+ """Test parsing invalid control ID format returns None"""
190
+ matcher = ControlMatcher()
191
+ test_cases = [
192
+ "No control here",
193
+ "12345",
194
+ "A-1", # Too short family code (single letter)
195
+ "AC", # Missing number
196
+ "Control without ID",
197
+ ]
198
+
199
+ for input_id in test_cases:
200
+ result = matcher.parse_control_id(input_id)
201
+ assert result is None, f"Expected None for input {input_id}, got {result}"
202
+
203
+
204
+ class TestControlMatcherParseControlIdWithSpaces:
205
+ """Test cases for parse_control_id method with spaces in control IDs"""
206
+
207
+ @patch("regscale.integrations.control_matcher.Api")
208
+ @patch("regscale.integrations.control_matcher.Application")
209
+ def test_parse_control_id_with_space_before_parenthesis(self, mock_app_class, mock_api_class):
210
+ """Test parsing control ID with space before parenthesis"""
211
+ matcher = ControlMatcher()
212
+ test_cases = [
213
+ ("AC-1 (1)", "AC-1.1"),
214
+ ("AC-2 (3)", "AC-2.3"),
215
+ ("SI-4 (10)", "SI-4.10"),
216
+ ("CM-6 (1)", "CM-6.1"),
217
+ ]
218
+
219
+ for input_id, expected in test_cases:
220
+ result = matcher.parse_control_id(input_id)
221
+ assert result == expected, f"Failed for input {input_id}"
222
+
223
+ @patch("regscale.integrations.control_matcher.Api")
224
+ @patch("regscale.integrations.control_matcher.Application")
225
+ def test_parse_control_id_with_spaces_inside_parentheses(self, mock_app_class, mock_api_class):
226
+ """Test parsing control ID with spaces inside parentheses"""
227
+ matcher = ControlMatcher()
228
+ test_cases = [
229
+ ("AC-1( 1 )", "AC-1.1"),
230
+ ("AC-2( 3 )", "AC-2.3"),
231
+ ("SI-4( 10)", "SI-4.10"),
232
+ ("CM-6(1 )", "CM-6.1"),
233
+ ]
234
+
235
+ for input_id, expected in test_cases:
236
+ result = matcher.parse_control_id(input_id)
237
+ assert result == expected, f"Failed for input {input_id}"
238
+
239
+ @patch("regscale.integrations.control_matcher.Api")
240
+ @patch("regscale.integrations.control_matcher.Application")
241
+ def test_parse_control_id_with_leading_zeros_and_spaces(self, mock_app_class, mock_api_class):
242
+ """Test parsing control ID with both leading zeros and spaces"""
243
+ matcher = ControlMatcher()
244
+ test_cases = [
245
+ ("AC-01 (01)", "AC-1.1"),
246
+ ("AC-02 (04)", "AC-2.4"),
247
+ ("AC-17 (02)", "AC-17.2"),
248
+ ("SI-04 (05)", "SI-4.5"),
249
+ ]
250
+
251
+ for input_id, expected in test_cases:
252
+ result = matcher.parse_control_id(input_id)
253
+ assert result == expected, f"Failed for input {input_id}"
254
+
255
+ @patch("regscale.integrations.control_matcher.Api")
256
+ @patch("regscale.integrations.control_matcher.Application")
257
+ def test_parse_control_id_with_various_space_combinations(self, mock_app_class, mock_api_class):
258
+ """Test parsing control ID with various space combinations"""
259
+ matcher = ControlMatcher()
260
+ test_cases = [
261
+ ("AC-1 (1)", "AC-1.1"), # Multiple spaces before
262
+ ("AC-1 ( 1 )", "AC-1.1"), # Spaces everywhere
263
+ ("AC-1 ( 1 )", "AC-1.1"), # Multiple spaces everywhere
264
+ ("AC-01 ( 04 )", "AC-1.4"), # Leading zeros and multiple spaces
265
+ ]
266
+
267
+ for input_id, expected in test_cases:
268
+ result = matcher.parse_control_id(input_id)
269
+ assert result == expected, f"Failed for input {input_id}"
270
+
271
+ @patch("regscale.integrations.control_matcher.Api")
272
+ @patch("regscale.integrations.control_matcher.Application")
273
+ def test_parse_control_id_with_spaces_in_text(self, mock_app_class, mock_api_class):
274
+ """Test parsing control ID with spaces in descriptive text"""
275
+ matcher = ControlMatcher()
276
+ test_cases = [
277
+ ("Access Control AC-1 (1)", "AC-1.1"),
278
+ ("AC-2 (3) Account Management", "AC-2.3"),
279
+ ("NIST Control AC-17 (02)", "AC-17.2"),
280
+ ]
281
+
282
+ for input_id, expected in test_cases:
283
+ result = matcher.parse_control_id(input_id)
284
+ assert result == expected, f"Failed for input {input_id}"
285
+
286
+
287
+ class TestControlMatcherFindControlInCatalog:
288
+ """Test cases for find_control_in_catalog method"""
289
+
290
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
291
+ @patch("regscale.integrations.control_matcher.Api")
292
+ @patch("regscale.integrations.control_matcher.Application")
293
+ def test_find_control_exact_match(self, mock_app_class, mock_api_class, mock_get_controls):
294
+ """Test finding control with exact match"""
295
+ matcher = ControlMatcher()
296
+
297
+ # Create mock controls
298
+ mock_control1 = MagicMock(spec=SecurityControl)
299
+ mock_control1.controlId = "AC-1"
300
+ mock_control1.id = 100
301
+
302
+ mock_control2 = MagicMock(spec=SecurityControl)
303
+ mock_control2.controlId = "AC-2"
304
+ mock_control2.id = 101
305
+
306
+ mock_get_controls.return_value = [mock_control1, mock_control2]
307
+
308
+ result = matcher.find_control_in_catalog("AC-1", 1)
309
+
310
+ assert result == mock_control1
311
+ mock_get_controls.assert_called_once_with(1)
312
+
313
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
314
+ @patch("regscale.integrations.control_matcher.Api")
315
+ @patch("regscale.integrations.control_matcher.Application")
316
+ def test_find_control_normalized_match(self, mock_app_class, mock_api_class, mock_get_controls):
317
+ """Test finding control with normalized match when exact match fails"""
318
+ matcher = ControlMatcher()
319
+
320
+ # Create mock controls with parentheses notation
321
+ mock_control1 = MagicMock(spec=SecurityControl)
322
+ mock_control1.controlId = "AC-1(1)"
323
+ mock_control1.id = 100
324
+
325
+ mock_control2 = MagicMock(spec=SecurityControl)
326
+ mock_control2.controlId = "AC-2"
327
+ mock_control2.id = 101
328
+
329
+ mock_get_controls.return_value = [mock_control1, mock_control2]
330
+
331
+ # Search using dot notation
332
+ result = matcher.find_control_in_catalog("AC-1.1", 1)
333
+
334
+ assert result == mock_control1
335
+ mock_get_controls.assert_called_once_with(1)
336
+
337
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
338
+ @patch("regscale.integrations.control_matcher.Api")
339
+ @patch("regscale.integrations.control_matcher.Application")
340
+ def test_find_control_not_found(self, mock_app_class, mock_api_class, mock_get_controls):
341
+ """Test finding control that doesn't exist returns None"""
342
+ matcher = ControlMatcher()
343
+
344
+ mock_control1 = MagicMock(spec=SecurityControl)
345
+ mock_control1.controlId = "AC-1"
346
+ mock_control1.id = 100
347
+
348
+ mock_get_controls.return_value = [mock_control1]
349
+
350
+ result = matcher.find_control_in_catalog("SI-4", 1)
351
+
352
+ assert result is None
353
+ mock_get_controls.assert_called_once_with(1)
354
+
355
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
356
+ @patch("regscale.integrations.control_matcher.Api")
357
+ @patch("regscale.integrations.control_matcher.Application")
358
+ def test_find_control_empty_catalog(self, mock_app_class, mock_api_class, mock_get_controls):
359
+ """Test finding control in empty catalog returns None"""
360
+ matcher = ControlMatcher()
361
+ mock_get_controls.return_value = []
362
+
363
+ result = matcher.find_control_in_catalog("AC-1", 1)
364
+
365
+ assert result is None
366
+ mock_get_controls.assert_called_once_with(1)
367
+
368
+
369
+ class TestControlMatcherFindControlImplementation:
370
+ """Test cases for find_control_implementation method"""
371
+
372
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
373
+ @patch("regscale.integrations.control_matcher.Api")
374
+ @patch("regscale.integrations.control_matcher.Application")
375
+ def test_find_implementation_by_label(self, mock_app_class, mock_api_class, mock_get_impls):
376
+ """Test finding implementation by control label"""
377
+ matcher = ControlMatcher()
378
+
379
+ # Create mock implementations
380
+ mock_impl1 = MagicMock(spec=ControlImplementation)
381
+ mock_impl1.id = 200
382
+ mock_impl1.controlID = 100
383
+
384
+ mock_impl2 = MagicMock(spec=ControlImplementation)
385
+ mock_impl2.id = 201
386
+ mock_impl2.controlID = 101
387
+
388
+ mock_get_impls.return_value = {
389
+ "AC-1": mock_impl1,
390
+ "AC-2": mock_impl2,
391
+ }
392
+
393
+ result = matcher.find_control_implementation("AC-1", 50, "securityplans")
394
+
395
+ assert result == mock_impl1
396
+ mock_get_impls.assert_called_once_with(50, "securityplans")
397
+
398
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
399
+ @patch("regscale.integrations.control_matcher.Api")
400
+ @patch("regscale.integrations.control_matcher.Application")
401
+ def test_find_implementation_case_insensitive(self, mock_app_class, mock_api_class, mock_get_impls):
402
+ """Test finding implementation with case-insensitive matching"""
403
+ matcher = ControlMatcher()
404
+
405
+ mock_impl1 = MagicMock(spec=ControlImplementation)
406
+ mock_impl1.id = 200
407
+ mock_impl1.controlID = 100
408
+
409
+ mock_get_impls.return_value = {
410
+ "ac-1": mock_impl1, # lowercase in dict
411
+ }
412
+
413
+ result = matcher.find_control_implementation("AC-1", 50)
414
+
415
+ assert result == mock_impl1
416
+
417
+ @patch("regscale.integrations.control_matcher.ControlMatcher.find_control_in_catalog")
418
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
419
+ @patch("regscale.integrations.control_matcher.Api")
420
+ @patch("regscale.integrations.control_matcher.Application")
421
+ def test_find_implementation_via_catalog(self, mock_app_class, mock_api_class, mock_get_impls, mock_find_control):
422
+ """Test finding implementation via catalog when label match fails"""
423
+ matcher = ControlMatcher()
424
+
425
+ # No label match
426
+ mock_impl1 = MagicMock(spec=ControlImplementation)
427
+ mock_impl1.id = 200
428
+ mock_impl1.controlID = 100
429
+
430
+ mock_get_impls.return_value = {
431
+ "SI-2": mock_impl1, # Different control
432
+ }
433
+
434
+ # But catalog returns a control
435
+ mock_control = MagicMock(spec=SecurityControl)
436
+ mock_control.id = 100
437
+ mock_control.controlId = "AC-1"
438
+ mock_find_control.return_value = mock_control
439
+
440
+ result = matcher.find_control_implementation("AC-1", 50, "securityplans", catalog_id=1)
441
+
442
+ assert result == mock_impl1
443
+ mock_find_control.assert_called_once_with("AC-1", 1)
444
+
445
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
446
+ @patch("regscale.integrations.control_matcher.Api")
447
+ @patch("regscale.integrations.control_matcher.Application")
448
+ def test_find_implementation_invalid_control_id(self, mock_app_class, mock_api_class, mock_get_impls):
449
+ """Test finding implementation with invalid control ID returns None"""
450
+ matcher = ControlMatcher()
451
+ mock_get_impls.return_value = {}
452
+
453
+ with patch("regscale.integrations.control_matcher.logger") as mock_logger:
454
+ result = matcher.find_control_implementation("Invalid", 50)
455
+
456
+ assert result is None
457
+ mock_logger.warning.assert_called_once()
458
+
459
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
460
+ @patch("regscale.integrations.control_matcher.Api")
461
+ @patch("regscale.integrations.control_matcher.Application")
462
+ def test_find_implementation_not_found(self, mock_app_class, mock_api_class, mock_get_impls):
463
+ """Test finding implementation that doesn't exist returns None"""
464
+ matcher = ControlMatcher()
465
+ mock_get_impls.return_value = {
466
+ "SI-2": MagicMock(spec=ControlImplementation),
467
+ }
468
+
469
+ result = matcher.find_control_implementation("AC-1", 50)
470
+
471
+ assert result is None
472
+
473
+
474
+ class TestControlMatcherMatchControlsToImplementations:
475
+ """Test cases for match_controls_to_implementations method"""
476
+
477
+ @patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
478
+ @patch("regscale.integrations.control_matcher.Api")
479
+ @patch("regscale.integrations.control_matcher.Application")
480
+ def test_match_multiple_controls(self, mock_app_class, mock_api_class, mock_find_impl):
481
+ """Test matching multiple control IDs to implementations"""
482
+ matcher = ControlMatcher()
483
+
484
+ mock_impl1 = MagicMock(spec=ControlImplementation)
485
+ mock_impl1.id = 200
486
+
487
+ mock_impl2 = MagicMock(spec=ControlImplementation)
488
+ mock_impl2.id = 201
489
+
490
+ def find_impl_side_effect(control_id, parent_id, parent_module="securityplans", catalog_id=None):
491
+ if control_id == "AC-1":
492
+ return mock_impl1
493
+ elif control_id == "AC-2":
494
+ return mock_impl2
495
+ return None
496
+
497
+ mock_find_impl.side_effect = find_impl_side_effect
498
+
499
+ control_ids = ["AC-1", "AC-2", "SI-4"]
500
+ result = matcher.match_controls_to_implementations(control_ids, 50)
501
+
502
+ assert len(result) == 3
503
+ assert result["AC-1"] == mock_impl1
504
+ assert result["AC-2"] == mock_impl2
505
+ assert result["SI-4"] is None
506
+ assert mock_find_impl.call_count == 3
507
+
508
+ @patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
509
+ @patch("regscale.integrations.control_matcher.Api")
510
+ @patch("regscale.integrations.control_matcher.Application")
511
+ def test_match_empty_list(self, mock_app_class, mock_api_class, mock_find_impl):
512
+ """Test matching empty list returns empty dict"""
513
+ matcher = ControlMatcher()
514
+ result = matcher.match_controls_to_implementations([], 50)
515
+
516
+ assert result == {}
517
+ mock_find_impl.assert_not_called()
518
+
519
+ @patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
520
+ @patch("regscale.integrations.control_matcher.Api")
521
+ @patch("regscale.integrations.control_matcher.Application")
522
+ def test_match_with_catalog_id(self, mock_app_class, mock_api_class, mock_find_impl):
523
+ """Test matching controls with catalog ID provided"""
524
+ matcher = ControlMatcher()
525
+ mock_impl = MagicMock(spec=ControlImplementation)
526
+ mock_find_impl.return_value = mock_impl
527
+
528
+ control_ids = ["AC-1"]
529
+ result = matcher.match_controls_to_implementations(control_ids, 50, "securityplans", catalog_id=1)
530
+
531
+ assert result["AC-1"] == mock_impl
532
+ mock_find_impl.assert_called_once_with("AC-1", 50, "securityplans", 1)
533
+
534
+
535
+ class TestControlMatcherGetSecurityPlanControls:
536
+ """Test cases for get_security_plan_controls method"""
537
+
538
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
539
+ @patch("regscale.integrations.control_matcher.Api")
540
+ @patch("regscale.integrations.control_matcher.Application")
541
+ def test_get_security_plan_controls(self, mock_app_class, mock_api_class, mock_get_impls):
542
+ """Test getting all control implementations for a security plan"""
543
+ matcher = ControlMatcher()
544
+
545
+ mock_impl1 = MagicMock(spec=ControlImplementation)
546
+ mock_impl2 = MagicMock(spec=ControlImplementation)
547
+
548
+ expected_dict = {
549
+ "AC-1": mock_impl1,
550
+ "AC-2": mock_impl2,
551
+ }
552
+ mock_get_impls.return_value = expected_dict
553
+
554
+ result = matcher.get_security_plan_controls(50)
555
+
556
+ assert result == expected_dict
557
+ mock_get_impls.assert_called_once_with(50, "securityplans")
558
+
559
+
560
+ class TestControlMatcherFindControlsByPattern:
561
+ """Test cases for find_controls_by_pattern method"""
562
+
563
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
564
+ @patch("regscale.integrations.control_matcher.Api")
565
+ @patch("regscale.integrations.control_matcher.Application")
566
+ def test_find_by_control_id_pattern(self, mock_app_class, mock_api_class, mock_get_controls):
567
+ """Test finding controls by control ID pattern"""
568
+ matcher = ControlMatcher()
569
+
570
+ mock_control1 = MagicMock(spec=SecurityControl)
571
+ mock_control1.controlId = "AC-1"
572
+ mock_control1.title = "Access Control Policy"
573
+
574
+ mock_control2 = MagicMock(spec=SecurityControl)
575
+ mock_control2.controlId = "AC-2"
576
+ mock_control2.title = "Account Management"
577
+
578
+ mock_control3 = MagicMock(spec=SecurityControl)
579
+ mock_control3.controlId = "SI-2"
580
+ mock_control3.title = "Flaw Remediation"
581
+
582
+ mock_get_controls.return_value = [mock_control1, mock_control2, mock_control3]
583
+
584
+ result = matcher.find_controls_by_pattern("^AC-", 1)
585
+
586
+ assert len(result) == 2
587
+ assert mock_control1 in result
588
+ assert mock_control2 in result
589
+ assert mock_control3 not in result
590
+
591
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
592
+ @patch("regscale.integrations.control_matcher.Api")
593
+ @patch("regscale.integrations.control_matcher.Application")
594
+ def test_find_by_title_pattern(self, mock_app_class, mock_api_class, mock_get_controls):
595
+ """Test finding controls by title pattern"""
596
+ matcher = ControlMatcher()
597
+
598
+ mock_control1 = MagicMock(spec=SecurityControl)
599
+ mock_control1.controlId = "AC-1"
600
+ mock_control1.title = "Access Control Policy"
601
+
602
+ mock_control2 = MagicMock(spec=SecurityControl)
603
+ mock_control2.controlId = "AC-2"
604
+ mock_control2.title = "Account Management"
605
+
606
+ mock_control3 = MagicMock(spec=SecurityControl)
607
+ mock_control3.controlId = "SI-2"
608
+ mock_control3.title = "Access Review"
609
+
610
+ mock_get_controls.return_value = [mock_control1, mock_control2, mock_control3]
611
+
612
+ result = matcher.find_controls_by_pattern("Access", 1)
613
+
614
+ assert len(result) == 2
615
+ assert mock_control1 in result
616
+ assert mock_control3 in result
617
+ assert mock_control2 not in result
618
+
619
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
620
+ @patch("regscale.integrations.control_matcher.Api")
621
+ @patch("regscale.integrations.control_matcher.Application")
622
+ def test_find_by_pattern_case_insensitive(self, mock_app_class, mock_api_class, mock_get_controls):
623
+ """Test finding controls with case-insensitive pattern"""
624
+ matcher = ControlMatcher()
625
+
626
+ mock_control1 = MagicMock(spec=SecurityControl)
627
+ mock_control1.controlId = "ac-1"
628
+ mock_control1.title = "access control"
629
+
630
+ mock_get_controls.return_value = [mock_control1]
631
+
632
+ result = matcher.find_controls_by_pattern("ACCESS", 1)
633
+
634
+ assert len(result) == 1
635
+ assert mock_control1 in result
636
+
637
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
638
+ @patch("regscale.integrations.control_matcher.Api")
639
+ @patch("regscale.integrations.control_matcher.Application")
640
+ def test_find_by_pattern_no_matches(self, mock_app_class, mock_api_class, mock_get_controls):
641
+ """Test finding controls with pattern that has no matches"""
642
+ matcher = ControlMatcher()
643
+
644
+ mock_control1 = MagicMock(spec=SecurityControl)
645
+ mock_control1.controlId = "AC-1"
646
+ mock_control1.title = "Access Control"
647
+
648
+ mock_get_controls.return_value = [mock_control1]
649
+
650
+ result = matcher.find_controls_by_pattern("NOMATCH", 1)
651
+
652
+ assert len(result) == 0
653
+
654
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
655
+ @patch("regscale.integrations.control_matcher.Api")
656
+ @patch("regscale.integrations.control_matcher.Application")
657
+ def test_find_by_pattern_none_title(self, mock_app_class, mock_api_class, mock_get_controls):
658
+ """Test finding controls when title is None"""
659
+ matcher = ControlMatcher()
660
+
661
+ mock_control1 = MagicMock(spec=SecurityControl)
662
+ mock_control1.controlId = "AC-1"
663
+ mock_control1.title = None
664
+
665
+ mock_control2 = MagicMock(spec=SecurityControl)
666
+ mock_control2.controlId = "AC-2"
667
+ mock_control2.title = "Account Management"
668
+
669
+ mock_get_controls.return_value = [mock_control1, mock_control2]
670
+
671
+ result = matcher.find_controls_by_pattern("AC-1", 1)
672
+
673
+ assert len(result) == 1
674
+ assert mock_control1 in result
675
+
676
+
677
+ class TestControlMatcherBulkMatchControls:
678
+ """Test cases for bulk_match_controls method"""
679
+
680
+ @patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
681
+ @patch("regscale.integrations.control_matcher.Api")
682
+ @patch("regscale.integrations.control_matcher.Application")
683
+ def test_bulk_match_controls(self, mock_app_class, mock_api_class, mock_find_impl):
684
+ """Test bulk matching external IDs to control implementations"""
685
+ matcher = ControlMatcher()
686
+
687
+ mock_impl1 = MagicMock(spec=ControlImplementation)
688
+ mock_impl1.id = 200
689
+
690
+ mock_impl2 = MagicMock(spec=ControlImplementation)
691
+ mock_impl2.id = 201
692
+
693
+ def find_impl_side_effect(control_id, parent_id, parent_module="securityplans", catalog_id=None):
694
+ if control_id == "AC-1":
695
+ return mock_impl1
696
+ elif control_id == "AC-2":
697
+ return mock_impl2
698
+ return None
699
+
700
+ mock_find_impl.side_effect = find_impl_side_effect
701
+
702
+ control_mappings = {
703
+ "ext-001": "AC-1",
704
+ "ext-002": "AC-2",
705
+ "ext-003": "SI-4",
706
+ }
707
+
708
+ result = matcher.bulk_match_controls(control_mappings, 50)
709
+
710
+ assert len(result) == 3
711
+ assert result["ext-001"] == mock_impl1
712
+ assert result["ext-002"] == mock_impl2
713
+ assert result["ext-003"] is None
714
+ assert mock_find_impl.call_count == 3
715
+
716
+ @patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
717
+ @patch("regscale.integrations.control_matcher.Api")
718
+ @patch("regscale.integrations.control_matcher.Application")
719
+ def test_bulk_match_empty_dict(self, mock_app_class, mock_api_class, mock_find_impl):
720
+ """Test bulk matching with empty dict returns empty dict"""
721
+ matcher = ControlMatcher()
722
+ result = matcher.bulk_match_controls({}, 50)
723
+
724
+ assert result == {}
725
+ mock_find_impl.assert_not_called()
726
+
727
+ @patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
728
+ @patch("regscale.integrations.control_matcher.Api")
729
+ @patch("regscale.integrations.control_matcher.Application")
730
+ def test_bulk_match_with_catalog(self, mock_app_class, mock_api_class, mock_find_impl):
731
+ """Test bulk matching with catalog ID"""
732
+ matcher = ControlMatcher()
733
+ mock_impl = MagicMock(spec=ControlImplementation)
734
+ mock_find_impl.return_value = mock_impl
735
+
736
+ control_mappings = {"ext-001": "AC-1"}
737
+ result = matcher.bulk_match_controls(control_mappings, 50, "securityplans", catalog_id=1)
738
+
739
+ assert result["ext-001"] == mock_impl
740
+ mock_find_impl.assert_called_once_with("AC-1", 50, "securityplans", 1)
741
+
742
+
743
+ class TestControlMatcherGetCatalogControls:
744
+ """Test cases for _get_catalog_controls method"""
745
+
746
+ @patch("regscale.models.regscale_models.security_control.SecurityControl.get_list_by_catalog")
747
+ @patch("regscale.integrations.control_matcher.Api")
748
+ @patch("regscale.integrations.control_matcher.Application")
749
+ def test_get_catalog_controls_first_call(self, mock_app_class, mock_api_class, mock_get_list):
750
+ """Test getting catalog controls on first call (not cached)"""
751
+ matcher = ControlMatcher()
752
+
753
+ mock_control1 = MagicMock(spec=SecurityControl)
754
+ mock_control2 = MagicMock(spec=SecurityControl)
755
+ mock_get_list.return_value = [mock_control1, mock_control2]
756
+
757
+ result = matcher._get_catalog_controls(1)
758
+
759
+ assert len(result) == 2
760
+ assert mock_control1 in result
761
+ assert mock_control2 in result
762
+ mock_get_list.assert_called_once_with(1)
763
+ assert 1 in matcher._catalog_cache
764
+
765
+ @patch("regscale.models.regscale_models.security_control.SecurityControl.get_list_by_catalog")
766
+ @patch("regscale.integrations.control_matcher.Api")
767
+ @patch("regscale.integrations.control_matcher.Application")
768
+ def test_get_catalog_controls_cached(self, mock_app_class, mock_api_class, mock_get_list):
769
+ """Test getting catalog controls from cache on subsequent calls"""
770
+ matcher = ControlMatcher()
771
+
772
+ mock_control1 = MagicMock(spec=SecurityControl)
773
+ mock_control2 = MagicMock(spec=SecurityControl)
774
+ cached_controls = [mock_control1, mock_control2]
775
+
776
+ # Pre-populate cache
777
+ matcher._catalog_cache[1] = cached_controls
778
+
779
+ result = matcher._get_catalog_controls(1)
780
+
781
+ assert result == cached_controls
782
+ mock_get_list.assert_not_called()
783
+
784
+ @patch("regscale.models.regscale_models.security_control.SecurityControl.get_list_by_catalog")
785
+ @patch("regscale.integrations.control_matcher.Api")
786
+ @patch("regscale.integrations.control_matcher.Application")
787
+ def test_get_catalog_controls_error(self, mock_app_class, mock_api_class, mock_get_list):
788
+ """Test getting catalog controls handles exception"""
789
+ matcher = ControlMatcher()
790
+ mock_get_list.side_effect = Exception("API Error")
791
+
792
+ with patch("regscale.integrations.control_matcher.logger") as mock_logger:
793
+ result = matcher._get_catalog_controls(1)
794
+
795
+ assert result == []
796
+ mock_logger.error.assert_called_once()
797
+ assert 1 not in matcher._catalog_cache
798
+
799
+
800
+ class TestControlMatcherGetControlImplementations:
801
+ """Test cases for _get_control_implementations method"""
802
+
803
+ @patch("regscale.models.regscale_models.control_implementation.ControlImplementation.get_object")
804
+ @patch(
805
+ "regscale.models.regscale_models.control_implementation.ControlImplementation.get_control_label_map_by_parent"
806
+ )
807
+ @patch("regscale.integrations.control_matcher.Api")
808
+ @patch("regscale.integrations.control_matcher.Application")
809
+ def test_get_control_implementations_first_call(
810
+ self, mock_app_class, mock_api_class, mock_get_label_map, mock_get_object
811
+ ):
812
+ """Test getting control implementations on first call (not cached)"""
813
+ matcher = ControlMatcher()
814
+
815
+ mock_impl1 = MagicMock(spec=ControlImplementation)
816
+ mock_impl1.id = 200
817
+
818
+ mock_impl2 = MagicMock(spec=ControlImplementation)
819
+ mock_impl2.id = 201
820
+
821
+ mock_get_label_map.return_value = {
822
+ "AC-1": 200,
823
+ "AC-2": 201,
824
+ }
825
+
826
+ def get_object_side_effect(impl_id):
827
+ if impl_id == 200:
828
+ return mock_impl1
829
+ elif impl_id == 201:
830
+ return mock_impl2
831
+ return None
832
+
833
+ mock_get_object.side_effect = get_object_side_effect
834
+
835
+ result = matcher._get_control_implementations(50, "securityplans")
836
+
837
+ assert len(result) == 2
838
+ assert result["AC-1"] == mock_impl1
839
+ assert result["AC-2"] == mock_impl2
840
+ mock_get_label_map.assert_called_once_with(50, "securityplans")
841
+ assert (50, "securityplans") in matcher._control_impl_cache
842
+
843
+ @patch(
844
+ "regscale.models.regscale_models.control_implementation.ControlImplementation.get_control_label_map_by_parent"
845
+ )
846
+ @patch("regscale.integrations.control_matcher.Api")
847
+ @patch("regscale.integrations.control_matcher.Application")
848
+ def test_get_control_implementations_cached(self, mock_app_class, mock_api_class, mock_get_label_map):
849
+ """Test getting control implementations from cache"""
850
+ matcher = ControlMatcher()
851
+
852
+ mock_impl = MagicMock(spec=ControlImplementation)
853
+ cached_impls = {"AC-1": mock_impl}
854
+
855
+ # Pre-populate cache
856
+ matcher._control_impl_cache[(50, "securityplans")] = cached_impls
857
+
858
+ result = matcher._get_control_implementations(50, "securityplans")
859
+
860
+ assert result == cached_impls
861
+ mock_get_label_map.assert_not_called()
862
+
863
+ @patch("regscale.models.regscale_models.control_implementation.ControlImplementation.get_object")
864
+ @patch(
865
+ "regscale.models.regscale_models.control_implementation.ControlImplementation.get_control_label_map_by_parent"
866
+ )
867
+ @patch("regscale.integrations.control_matcher.Api")
868
+ @patch("regscale.integrations.control_matcher.Application")
869
+ def test_get_control_implementations_with_none(
870
+ self, mock_app_class, mock_api_class, mock_get_label_map, mock_get_object
871
+ ):
872
+ """Test getting control implementations when some objects return None"""
873
+ matcher = ControlMatcher()
874
+
875
+ mock_impl1 = MagicMock(spec=ControlImplementation)
876
+ mock_impl1.id = 200
877
+
878
+ mock_get_label_map.return_value = {
879
+ "AC-1": 200,
880
+ "AC-2": 201, # This will return None
881
+ }
882
+
883
+ def get_object_side_effect(impl_id):
884
+ if impl_id == 200:
885
+ return mock_impl1
886
+ return None
887
+
888
+ mock_get_object.side_effect = get_object_side_effect
889
+
890
+ result = matcher._get_control_implementations(50, "securityplans")
891
+
892
+ # Should only include the valid implementation
893
+ assert len(result) == 1
894
+ assert result["AC-1"] == mock_impl1
895
+ assert "AC-2" not in result
896
+
897
+ @patch(
898
+ "regscale.models.regscale_models.control_implementation.ControlImplementation.get_control_label_map_by_parent"
899
+ )
900
+ @patch("regscale.integrations.control_matcher.Api")
901
+ @patch("regscale.integrations.control_matcher.Application")
902
+ def test_get_control_implementations_error(self, mock_app_class, mock_api_class, mock_get_label_map):
903
+ """Test getting control implementations handles exception"""
904
+ matcher = ControlMatcher()
905
+ mock_get_label_map.side_effect = Exception("API Error")
906
+
907
+ with patch("regscale.integrations.control_matcher.logger") as mock_logger:
908
+ result = matcher._get_control_implementations(50, "securityplans")
909
+
910
+ assert result == {}
911
+ mock_logger.error.assert_called_once()
912
+ assert (50, "securityplans") not in matcher._control_impl_cache
913
+
914
+
915
+ class TestControlMatcherClearCache:
916
+ """Test cases for clear_cache method"""
917
+
918
+ @patch("regscale.integrations.control_matcher.Api")
919
+ @patch("regscale.integrations.control_matcher.Application")
920
+ def test_clear_cache_empty(self, mock_app_class, mock_api_class):
921
+ """Test clearing cache when already empty"""
922
+ matcher = ControlMatcher()
923
+
924
+ with patch("regscale.integrations.control_matcher.logger") as mock_logger:
925
+ matcher.clear_cache()
926
+
927
+ assert matcher._catalog_cache == {}
928
+ assert matcher._control_impl_cache == {}
929
+ mock_logger.info.assert_called_once_with("Cleared control matcher cache")
930
+
931
+ @patch("regscale.integrations.control_matcher.Api")
932
+ @patch("regscale.integrations.control_matcher.Application")
933
+ def test_clear_cache_with_data(self, mock_app_class, mock_api_class):
934
+ """Test clearing cache with data"""
935
+ matcher = ControlMatcher()
936
+
937
+ # Add data to caches
938
+ matcher._catalog_cache[1] = [MagicMock(spec=SecurityControl)]
939
+ matcher._control_impl_cache[(50, "securityplans")] = {"AC-1": MagicMock(spec=ControlImplementation)}
940
+
941
+ with patch("regscale.integrations.control_matcher.logger") as mock_logger:
942
+ matcher.clear_cache()
943
+
944
+ assert matcher._catalog_cache == {}
945
+ assert matcher._control_impl_cache == {}
946
+ mock_logger.info.assert_called_once_with("Cleared control matcher cache")
947
+
948
+
949
+ class TestControlMatcherEdgeCases:
950
+ """Test cases for edge cases and error scenarios"""
951
+
952
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
953
+ @patch("regscale.integrations.control_matcher.Api")
954
+ @patch("regscale.integrations.control_matcher.Application")
955
+ def test_find_control_with_special_characters(self, mock_app_class, mock_api_class, mock_get_controls):
956
+ """Test finding control with special characters in ID"""
957
+ matcher = ControlMatcher()
958
+
959
+ mock_control = MagicMock(spec=SecurityControl)
960
+ mock_control.controlId = "AC-1(1)"
961
+ mock_get_controls.return_value = [mock_control]
962
+
963
+ # Test with different variations
964
+ result1 = matcher.find_control_in_catalog("AC-1(1)", 1)
965
+ result2 = matcher.find_control_in_catalog("AC-1.1", 1)
966
+
967
+ assert result1 == mock_control
968
+ assert result2 == mock_control
969
+
970
+ @patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
971
+ @patch("regscale.integrations.control_matcher.Api")
972
+ @patch("regscale.integrations.control_matcher.Application")
973
+ def test_match_controls_with_duplicates(self, mock_app_class, mock_api_class, mock_find_impl):
974
+ """Test matching controls when list contains duplicates"""
975
+ matcher = ControlMatcher()
976
+
977
+ mock_impl1 = MagicMock(spec=ControlImplementation)
978
+ mock_impl2 = MagicMock(spec=ControlImplementation)
979
+
980
+ def find_impl_side_effect(control_id, parent_id, parent_module="securityplans", catalog_id=None):
981
+ if control_id == "AC-1":
982
+ return mock_impl1
983
+ elif control_id == "AC-2":
984
+ return mock_impl2
985
+ return None
986
+
987
+ mock_find_impl.side_effect = find_impl_side_effect
988
+
989
+ control_ids = ["AC-1", "AC-1", "AC-2"]
990
+ result = matcher.match_controls_to_implementations(control_ids, 50)
991
+
992
+ # Result is a dict, so duplicates are collapsed - should have 2 unique keys
993
+ assert len(result) == 2
994
+ assert result["AC-1"] == mock_impl1
995
+ assert result["AC-2"] == mock_impl2
996
+ # Should still call find_impl for each entry in the list, including duplicates
997
+ assert mock_find_impl.call_count == 3
998
+
999
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
1000
+ @patch("regscale.integrations.control_matcher.Api")
1001
+ @patch("regscale.integrations.control_matcher.Application")
1002
+ def test_find_by_pattern_with_empty_string(self, mock_app_class, mock_api_class, mock_get_controls):
1003
+ """Test finding controls with empty string pattern"""
1004
+ matcher = ControlMatcher()
1005
+
1006
+ mock_control = MagicMock(spec=SecurityControl)
1007
+ mock_control.controlId = "AC-1"
1008
+ mock_control.title = "Access Control"
1009
+ mock_get_controls.return_value = [mock_control]
1010
+
1011
+ result = matcher.find_controls_by_pattern("", 1)
1012
+
1013
+ # Empty pattern should match everything
1014
+ assert len(result) == 1
1015
+ assert mock_control in result
1016
+
1017
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
1018
+ @patch("regscale.integrations.control_matcher.Api")
1019
+ @patch("regscale.integrations.control_matcher.Application")
1020
+ def test_find_implementation_different_parent_modules(self, mock_app_class, mock_api_class, mock_get_impls):
1021
+ """Test finding implementations with different parent modules"""
1022
+ matcher = ControlMatcher()
1023
+
1024
+ mock_impl1 = MagicMock(spec=ControlImplementation)
1025
+ mock_impl2 = MagicMock(spec=ControlImplementation)
1026
+
1027
+ def get_impls_side_effect(parent_id, parent_module):
1028
+ if parent_module == "securityplans":
1029
+ return {"AC-1": mock_impl1}
1030
+ elif parent_module == "assessments":
1031
+ return {"AC-1": mock_impl2}
1032
+ return {}
1033
+
1034
+ mock_get_impls.side_effect = get_impls_side_effect
1035
+
1036
+ result1 = matcher.find_control_implementation("AC-1", 50, "securityplans")
1037
+ result2 = matcher.find_control_implementation("AC-1", 51, "assessments")
1038
+
1039
+ assert result1 == mock_impl1
1040
+ assert result2 == mock_impl2
1041
+
1042
+
1043
+ class TestControlMatcherLeadingZeros:
1044
+ """Test cases for control IDs with leading zeros"""
1045
+
1046
+ @patch("regscale.integrations.control_matcher.Api")
1047
+ @patch("regscale.integrations.control_matcher.Application")
1048
+ def test_normalize_control_id_with_leading_zeros(self, mock_app_class, mock_api_class):
1049
+ """Test normalizing control IDs with leading zeros"""
1050
+ matcher = ControlMatcher()
1051
+
1052
+ test_cases = [
1053
+ ("AC-01", "AC-1"),
1054
+ ("AC-17", "AC-17"),
1055
+ ("AC-01.02", "AC-1.2"),
1056
+ ("AC-17.02", "AC-17.2"),
1057
+ ("AC-1.1", "AC-1.1"),
1058
+ ]
1059
+
1060
+ for input_id, expected in test_cases:
1061
+ result = matcher._normalize_control_id(input_id)
1062
+ assert result == expected, f"Failed for input {input_id}"
1063
+
1064
+ @patch("regscale.integrations.control_matcher.Api")
1065
+ @patch("regscale.integrations.control_matcher.Application")
1066
+ def test_get_control_id_variations_simple(self, mock_app_class, mock_api_class):
1067
+ """Test generating variations for simple control IDs"""
1068
+ matcher = ControlMatcher()
1069
+
1070
+ result = matcher._get_control_id_variations("AC-1")
1071
+ expected = {"AC-1", "AC-01"}
1072
+ assert result == expected
1073
+
1074
+ @patch("regscale.integrations.control_matcher.Api")
1075
+ @patch("regscale.integrations.control_matcher.Application")
1076
+ def test_get_control_id_variations_with_enhancement(self, mock_app_class, mock_api_class):
1077
+ """Test generating variations for control IDs with enhancements"""
1078
+ matcher = ControlMatcher()
1079
+
1080
+ result = matcher._get_control_id_variations("AC-17.2")
1081
+ expected = {
1082
+ "AC-17.2",
1083
+ "AC-17.02",
1084
+ "AC-17(2)",
1085
+ "AC-17(02)",
1086
+ }
1087
+ assert result == expected
1088
+
1089
+ @patch("regscale.integrations.control_matcher.Api")
1090
+ @patch("regscale.integrations.control_matcher.Application")
1091
+ def test_get_control_id_variations_with_leading_zeros_input(self, mock_app_class, mock_api_class):
1092
+ """Test generating variations when input has leading zeros"""
1093
+ matcher = ControlMatcher()
1094
+
1095
+ result = matcher._get_control_id_variations("AC-01")
1096
+ expected = {"AC-1", "AC-01"}
1097
+ assert result == expected
1098
+
1099
+ @patch("regscale.integrations.control_matcher.Api")
1100
+ @patch("regscale.integrations.control_matcher.Application")
1101
+ def test_get_control_id_variations_with_parentheses_input(self, mock_app_class, mock_api_class):
1102
+ """Test generating variations when input has parentheses"""
1103
+ matcher = ControlMatcher()
1104
+
1105
+ result = matcher._get_control_id_variations("AC-17(02)")
1106
+ expected = {
1107
+ "AC-17.2",
1108
+ "AC-17.02",
1109
+ "AC-17(2)",
1110
+ "AC-17(02)",
1111
+ }
1112
+ assert result == expected
1113
+
1114
+ @patch("regscale.integrations.control_matcher.Api")
1115
+ @patch("regscale.integrations.control_matcher.Application")
1116
+ def test_get_control_id_variations_with_letter_enhancement(self, mock_app_class, mock_api_class):
1117
+ """Test generating variations for control IDs with letter-based enhancements"""
1118
+ matcher = ControlMatcher()
1119
+
1120
+ result = matcher._get_control_id_variations("AC-1.a")
1121
+ expected = {
1122
+ "AC-1.A",
1123
+ "AC-01.A",
1124
+ "AC-1(A)",
1125
+ "AC-01(A)",
1126
+ }
1127
+ assert result == expected
1128
+
1129
+ @patch("regscale.integrations.control_matcher.Api")
1130
+ @patch("regscale.integrations.control_matcher.Application")
1131
+ def test_get_control_id_variations_invalid_input(self, mock_app_class, mock_api_class):
1132
+ """Test generating variations for invalid control ID returns empty set"""
1133
+ matcher = ControlMatcher()
1134
+
1135
+ result = matcher._get_control_id_variations("Invalid")
1136
+ assert result == set()
1137
+
1138
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
1139
+ @patch("regscale.integrations.control_matcher.Api")
1140
+ @patch("regscale.integrations.control_matcher.Application")
1141
+ def test_find_control_in_catalog_with_leading_zeros(self, mock_app_class, mock_api_class, mock_get_controls):
1142
+ """Test finding controls with leading zeros in catalog"""
1143
+ matcher = ControlMatcher()
1144
+
1145
+ # Catalog has control with leading zeros
1146
+ mock_control = MagicMock(spec=SecurityControl)
1147
+ mock_control.controlId = "AC-01"
1148
+ mock_control.id = 100
1149
+
1150
+ mock_get_controls.return_value = [mock_control]
1151
+
1152
+ # Search without leading zero should find it
1153
+ result = matcher.find_control_in_catalog("AC-1", 1)
1154
+ assert result == mock_control
1155
+
1156
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
1157
+ @patch("regscale.integrations.control_matcher.Api")
1158
+ @patch("regscale.integrations.control_matcher.Application")
1159
+ def test_find_control_in_catalog_search_with_leading_zeros(self, mock_app_class, mock_api_class, mock_get_controls):
1160
+ """Test finding controls when search ID has leading zeros"""
1161
+ matcher = ControlMatcher()
1162
+
1163
+ # Catalog has control without leading zeros
1164
+ mock_control = MagicMock(spec=SecurityControl)
1165
+ mock_control.controlId = "AC-1"
1166
+ mock_control.id = 100
1167
+
1168
+ mock_get_controls.return_value = [mock_control]
1169
+
1170
+ # Search with leading zero should find it
1171
+ result = matcher.find_control_in_catalog("AC-01", 1)
1172
+ assert result == mock_control
1173
+
1174
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_catalog_controls")
1175
+ @patch("regscale.integrations.control_matcher.Api")
1176
+ @patch("regscale.integrations.control_matcher.Application")
1177
+ def test_find_control_with_leading_zeros_enhancement(self, mock_app_class, mock_api_class, mock_get_controls):
1178
+ """Test finding controls with leading zeros in enhancement numbers"""
1179
+ matcher = ControlMatcher()
1180
+
1181
+ # Catalog has control with leading zeros in enhancement
1182
+ mock_control = MagicMock(spec=SecurityControl)
1183
+ mock_control.controlId = "AC-17(02)"
1184
+ mock_control.id = 100
1185
+
1186
+ mock_get_controls.return_value = [mock_control]
1187
+
1188
+ # Search with different formats should all find it
1189
+ result1 = matcher.find_control_in_catalog("AC-17.2", 1)
1190
+ result2 = matcher.find_control_in_catalog("AC-17(2)", 1)
1191
+ result3 = matcher.find_control_in_catalog("AC-17.02", 1)
1192
+
1193
+ assert result1 == mock_control
1194
+ assert result2 == mock_control
1195
+ assert result3 == mock_control
1196
+
1197
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
1198
+ @patch("regscale.integrations.control_matcher.Api")
1199
+ @patch("regscale.integrations.control_matcher.Application")
1200
+ def test_find_implementation_with_leading_zeros(self, mock_app_class, mock_api_class, mock_get_impls):
1201
+ """Test finding implementation when control IDs have leading zeros"""
1202
+ matcher = ControlMatcher()
1203
+
1204
+ mock_impl = MagicMock(spec=ControlImplementation)
1205
+ mock_impl.id = 200
1206
+ mock_impl.controlID = 100
1207
+
1208
+ # Implementation key has leading zero
1209
+ mock_get_impls.return_value = {
1210
+ "AC-01": mock_impl,
1211
+ }
1212
+
1213
+ # Search without leading zero should find it
1214
+ result = matcher.find_control_implementation("AC-1", 50)
1215
+ assert result == mock_impl
1216
+
1217
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
1218
+ @patch("regscale.integrations.control_matcher.Api")
1219
+ @patch("regscale.integrations.control_matcher.Application")
1220
+ def test_find_implementation_search_with_leading_zeros(self, mock_app_class, mock_api_class, mock_get_impls):
1221
+ """Test finding implementation when search ID has leading zeros"""
1222
+ matcher = ControlMatcher()
1223
+
1224
+ mock_impl = MagicMock(spec=ControlImplementation)
1225
+ mock_impl.id = 200
1226
+ mock_impl.controlID = 100
1227
+
1228
+ # Implementation key has no leading zero
1229
+ mock_get_impls.return_value = {
1230
+ "AC-1": mock_impl,
1231
+ }
1232
+
1233
+ # Search with leading zero should find it
1234
+ result = matcher.find_control_implementation("AC-01", 50)
1235
+ assert result == mock_impl
1236
+
1237
+ @patch("regscale.integrations.control_matcher.ControlMatcher._get_control_implementations")
1238
+ @patch("regscale.integrations.control_matcher.Api")
1239
+ @patch("regscale.integrations.control_matcher.Application")
1240
+ def test_find_implementation_with_leading_zeros_complex(self, mock_app_class, mock_api_class, mock_get_impls):
1241
+ """Test finding implementation with complex leading zero scenarios"""
1242
+ matcher = ControlMatcher()
1243
+
1244
+ mock_impl = MagicMock(spec=ControlImplementation)
1245
+ mock_impl.id = 200
1246
+
1247
+ # Implementation key has leading zeros in enhancement
1248
+ mock_get_impls.return_value = {
1249
+ "AC-17(02)": mock_impl,
1250
+ }
1251
+
1252
+ # Search with different formats should find it
1253
+ result1 = matcher.find_control_implementation("AC-17.2", 50)
1254
+ result2 = matcher.find_control_implementation("AC-17(2)", 50)
1255
+ result3 = matcher.find_control_implementation("AC-17.02", 50)
1256
+
1257
+ assert result1 == mock_impl
1258
+ assert result2 == mock_impl
1259
+ assert result3 == mock_impl
1260
+
1261
+
1262
+ class TestControlMatcherIntegrationScenarios:
1263
+ """Integration test scenarios for complex workflows"""
1264
+
1265
+ @patch("regscale.models.regscale_models.control_implementation.ControlImplementation.get_object")
1266
+ @patch(
1267
+ "regscale.models.regscale_models.control_implementation.ControlImplementation.get_control_label_map_by_parent"
1268
+ )
1269
+ @patch("regscale.models.regscale_models.security_control.SecurityControl.get_list_by_catalog")
1270
+ @patch("regscale.integrations.control_matcher.Api")
1271
+ @patch("regscale.integrations.control_matcher.Application")
1272
+ def test_full_workflow_with_caching(
1273
+ self, mock_app_class, mock_api_class, mock_get_catalog, mock_get_label_map, mock_get_object
1274
+ ):
1275
+ """Test full workflow with multiple operations and caching"""
1276
+ matcher = ControlMatcher()
1277
+
1278
+ # Setup catalog controls
1279
+ mock_control1 = MagicMock(spec=SecurityControl)
1280
+ mock_control1.controlId = "AC-1"
1281
+ mock_control1.id = 100
1282
+ mock_control1.title = "Access Control Policy"
1283
+
1284
+ mock_control2 = MagicMock(spec=SecurityControl)
1285
+ mock_control2.controlId = "AC-2"
1286
+ mock_control2.id = 101
1287
+ mock_control2.title = "Account Management"
1288
+
1289
+ mock_get_catalog.return_value = [mock_control1, mock_control2]
1290
+
1291
+ # Setup implementations
1292
+ mock_impl1 = MagicMock(spec=ControlImplementation)
1293
+ mock_impl1.id = 200
1294
+ mock_impl1.controlID = 100
1295
+
1296
+ mock_get_label_map.return_value = {"AC-1": 200}
1297
+ mock_get_object.return_value = mock_impl1
1298
+
1299
+ # First operation: find control in catalog
1300
+ control = matcher.find_control_in_catalog("AC-1", 1)
1301
+ assert control == mock_control1
1302
+ assert mock_get_catalog.call_count == 1
1303
+
1304
+ # Second operation: find same control (should use cache)
1305
+ control2 = matcher.find_control_in_catalog("AC-2", 1)
1306
+ assert control2 == mock_control2
1307
+ assert mock_get_catalog.call_count == 1 # Should not increase
1308
+
1309
+ # Third operation: find implementation
1310
+ impl = matcher.find_control_implementation("AC-1", 50)
1311
+ assert impl == mock_impl1
1312
+ assert mock_get_label_map.call_count == 1
1313
+
1314
+ # Fourth operation: find same implementation (should use cache)
1315
+ impl2 = matcher.find_control_implementation("AC-1", 50)
1316
+ assert impl2 == mock_impl1
1317
+ assert mock_get_label_map.call_count == 1 # Should not increase
1318
+
1319
+ # Clear cache
1320
+ matcher.clear_cache()
1321
+
1322
+ # Fifth operation: after cache clear, should fetch again
1323
+ control3 = matcher.find_control_in_catalog("AC-1", 1)
1324
+ assert control3 == mock_control1
1325
+ assert mock_get_catalog.call_count == 2 # Should increase now
1326
+
1327
+ @patch("regscale.integrations.control_matcher.ControlMatcher.find_control_implementation")
1328
+ @patch("regscale.integrations.control_matcher.Api")
1329
+ @patch("regscale.integrations.control_matcher.Application")
1330
+ def test_bulk_operations_with_mixed_results(self, mock_app_class, mock_api_class, mock_find_impl):
1331
+ """Test bulk operations with some successes and some failures"""
1332
+ matcher = ControlMatcher()
1333
+
1334
+ mock_impl1 = MagicMock(spec=ControlImplementation)
1335
+ mock_impl2 = MagicMock(spec=ControlImplementation)
1336
+
1337
+ def find_impl_side_effect(control_id, parent_id, parent_module="securityplans", catalog_id=None):
1338
+ impl_map = {
1339
+ "AC-1": mock_impl1,
1340
+ "AC-2": mock_impl2,
1341
+ }
1342
+ return impl_map.get(control_id)
1343
+
1344
+ mock_find_impl.side_effect = find_impl_side_effect
1345
+
1346
+ # Bulk match with mixed results
1347
+ mappings = {
1348
+ "finding-001": "AC-1", # Will find
1349
+ "finding-002": "AC-2", # Will find
1350
+ "finding-003": "SI-4", # Won't find
1351
+ "finding-004": "CM-6", # Won't find
1352
+ }
1353
+
1354
+ result = matcher.bulk_match_controls(mappings, 50)
1355
+
1356
+ assert result["finding-001"] == mock_impl1
1357
+ assert result["finding-002"] == mock_impl2
1358
+ assert result["finding-003"] is None
1359
+ assert result["finding-004"] is None
1360
+ assert len(result) == 4
1361
+
1362
+ @patch("regscale.models.regscale_models.control_implementation.ControlImplementation.get_object")
1363
+ @patch(
1364
+ "regscale.models.regscale_models.control_implementation.ControlImplementation.get_control_label_map_by_parent"
1365
+ )
1366
+ @patch("regscale.models.regscale_models.security_control.SecurityControl.get_list_by_catalog")
1367
+ @patch("regscale.integrations.control_matcher.Api")
1368
+ @patch("regscale.integrations.control_matcher.Application")
1369
+ def test_workflow_with_leading_zeros_catalog(
1370
+ self, mock_app_class, mock_api_class, mock_get_catalog, mock_get_label_map, mock_get_object
1371
+ ):
1372
+ """Test workflow when catalog has control IDs with leading zeros"""
1373
+ matcher = ControlMatcher()
1374
+
1375
+ # Catalog has controls with leading zeros
1376
+ mock_control1 = MagicMock(spec=SecurityControl)
1377
+ mock_control1.controlId = "AC-01"
1378
+ mock_control1.id = 100
1379
+
1380
+ mock_control2 = MagicMock(spec=SecurityControl)
1381
+ mock_control2.controlId = "AC-17(02)"
1382
+ mock_control2.id = 101
1383
+
1384
+ mock_get_catalog.return_value = [mock_control1, mock_control2]
1385
+
1386
+ # Implementations have standard format
1387
+ mock_impl1 = MagicMock(spec=ControlImplementation)
1388
+ mock_impl1.id = 200
1389
+ mock_impl1.controlID = 100
1390
+
1391
+ mock_impl2 = MagicMock(spec=ControlImplementation)
1392
+ mock_impl2.id = 201
1393
+ mock_impl2.controlID = 101
1394
+
1395
+ mock_get_label_map.return_value = {
1396
+ "AC-1": 200,
1397
+ "AC-17.2": 201,
1398
+ }
1399
+
1400
+ def get_object_side_effect(impl_id):
1401
+ if impl_id == 200:
1402
+ return mock_impl1
1403
+ elif impl_id == 201:
1404
+ return mock_impl2
1405
+ return None
1406
+
1407
+ mock_get_object.side_effect = get_object_side_effect
1408
+
1409
+ # Search with standard format should find controls with leading zeros
1410
+ control1 = matcher.find_control_in_catalog("AC-1", 1)
1411
+ assert control1 == mock_control1
1412
+
1413
+ control2 = matcher.find_control_in_catalog("AC-17.2", 1)
1414
+ assert control2 == mock_control2
1415
+
1416
+ # Find implementations should work with either format
1417
+ impl1 = matcher.find_control_implementation("AC-01", 50)
1418
+ assert impl1 == mock_impl1
1419
+
1420
+ impl2 = matcher.find_control_implementation("AC-17(02)", 50)
1421
+ assert impl2 == mock_impl2