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,1053 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Tests for CCI Importer functionality."""
4
+ import xml.etree.ElementTree as ET
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ import pytest
8
+ from click.testing import CliRunner
9
+
10
+ from regscale.integrations.public.cci_importer import CCIImporter, cci_importer, _load_xml_file
11
+ from tests import CLITestFixture
12
+
13
+
14
+ class TestCCIImporter(CLITestFixture):
15
+ """Test cases for the CCIImporter class."""
16
+
17
+ @pytest.fixture
18
+ def sample_xml_data(self):
19
+ """Create sample XML data for testing."""
20
+ xml_content = """<?xml version="1.0" encoding="utf-8"?>
21
+ <cci_list xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
22
+ xmlns="http://iase.disa.mil/cci">
23
+ <cci_item id="CCI-000001">
24
+ <definition>The organization develops, documents, and disseminates access control policy.</definition>
25
+ <references>
26
+ <reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
27
+ location="AC-1" index="AC-1 a" />
28
+ </references>
29
+ </cci_item>
30
+ <cci_item id="CCI-000002">
31
+ <definition>The organization reviews and updates access control policy.</definition>
32
+ <references>
33
+ <reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
34
+ location="AC-1" index="AC-1 b" />
35
+ <reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
36
+ location="AC-1" index="AC-1 b" />
37
+ </references>
38
+ </cci_item>
39
+ <cci_item id="CCI-000003">
40
+ <definition>The organization develops access control procedures.</definition>
41
+ <references>
42
+ <reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
43
+ location="AC-2" index="AC-2 a 1" />
44
+ </references>
45
+ </cci_item>
46
+ </cci_list>"""
47
+ return ET.fromstring(xml_content)
48
+
49
+ @pytest.fixture
50
+ def cci_importer_instance(self, sample_xml_data):
51
+ """Create a CCIImporter instance for testing."""
52
+ return CCIImporter(sample_xml_data, version="5", verbose=False)
53
+
54
+ def test_init(self, sample_xml_data):
55
+ """Test CCIImporter initialization."""
56
+ importer = CCIImporter(sample_xml_data, version="4", verbose=True)
57
+
58
+ assert importer.xml_data == sample_xml_data
59
+ assert importer.reference_version == "4"
60
+ assert importer.verbose is True
61
+ assert importer.normalized_cci == {}
62
+ assert importer.cci_grouped_by_index == {}
63
+ assert importer._user_context is None
64
+
65
+ def test_parse_control_id(self, cci_importer_instance):
66
+ """Test parsing control IDs from reference indices."""
67
+ assert cci_importer_instance._parse_control_id("AC-1 a 1 (b)") == "AC-1"
68
+ assert cci_importer_instance._parse_control_id("SC-7") == "SC-7"
69
+ assert cci_importer_instance._parse_control_id("") == ""
70
+ assert cci_importer_instance._parse_control_id(" ") == ""
71
+
72
+ def test_extract_cci_data(self, cci_importer_instance, sample_xml_data):
73
+ """Test extracting CCI ID and definition from XML elements."""
74
+ cci_item = sample_xml_data.find(".//{http://iase.disa.mil/cci}cci_item")
75
+ cci_id, definition = cci_importer_instance._extract_cci_data(cci_item)
76
+
77
+ assert cci_id == "CCI-000001"
78
+ assert "organization develops, documents, and disseminates access control policy" in definition
79
+
80
+ def test_is_valid_reference(self, cci_importer_instance, sample_xml_data):
81
+ """Test validation of reference elements based on version."""
82
+ references = sample_xml_data.findall(".//{http://iase.disa.mil/cci}reference")
83
+
84
+ # Should accept version 5 references
85
+ version_5_refs = [ref for ref in references if ref.get("version") == "5"]
86
+ assert len(version_5_refs) > 0
87
+ assert cci_importer_instance._is_valid_reference(version_5_refs[0]) is True
88
+
89
+ # Should reject version 4 references when configured for version 5
90
+ version_4_refs = [ref for ref in references if ref.get("version") == "4"]
91
+ if version_4_refs:
92
+ assert cci_importer_instance._is_valid_reference(version_4_refs[0]) is False
93
+
94
+ def test_parse_cci(self, cci_importer_instance):
95
+ """Test parsing CCI items and normalizing them."""
96
+ cci_importer_instance.parse_cci()
97
+ normalized = cci_importer_instance.get_normalized_cci()
98
+
99
+ # Should have parsed AC-1 and AC-2 controls
100
+ assert "AC-1" in normalized
101
+ assert "AC-2" in normalized
102
+
103
+ # AC-1 should have 2 CCI items (CCI-000001 and CCI-000002)
104
+ assert len(normalized["AC-1"]) == 2
105
+
106
+ # AC-2 should have 1 CCI item (CCI-000003)
107
+ assert len(normalized["AC-2"]) == 1
108
+
109
+ # Check CCI content
110
+ ac1_ccis = normalized["AC-1"]
111
+ cci_ids = [cci["cci_id"] for cci in ac1_ccis]
112
+ assert "CCI-000001" in cci_ids
113
+ assert "CCI-000002" in cci_ids
114
+
115
+ @patch("regscale.integrations.public.cci_importer.Application")
116
+ def test_get_user_context(self, mock_app_class, cci_importer_instance):
117
+ """Test getting user context from application config."""
118
+ mock_app = MagicMock()
119
+ mock_app.config.get.side_effect = lambda key, default=None: {"userId": "123", "tenantId": 456}.get(key, default)
120
+ mock_app_class.return_value = mock_app
121
+
122
+ user_id, tenant_id = cci_importer_instance._get_user_context()
123
+
124
+ assert user_id == "123" # Code keeps user_id as string (UUID)
125
+ assert tenant_id == 456
126
+
127
+ # Should cache the result
128
+ user_id2, tenant_id2 = cci_importer_instance._get_user_context()
129
+ assert user_id2 == "123"
130
+ assert tenant_id2 == 456
131
+
132
+ @patch("regscale.integrations.public.cci_importer.Application")
133
+ def test_get_user_context_none_user_id(self, mock_app_class, cci_importer_instance):
134
+ """Test handling None user ID in config."""
135
+ mock_app = MagicMock()
136
+ mock_app.config.get.side_effect = lambda key, default=None: {"userId": None, "tenantId": "456"}.get(
137
+ key, default
138
+ )
139
+ mock_app_class.return_value = mock_app
140
+
141
+ user_id, tenant_id = cci_importer_instance._get_user_context()
142
+
143
+ assert user_id is None
144
+ assert tenant_id == 456 # tenant_id converted to int
145
+
146
+ @patch("regscale.models.regscale_models.Catalog.get")
147
+ def test_get_catalog_success(self, mock_get, cci_importer_instance):
148
+ """Test successful catalog retrieval."""
149
+ mock_catalog = MagicMock()
150
+ mock_get.return_value = mock_catalog
151
+
152
+ result = cci_importer_instance._get_catalog(1)
153
+
154
+ assert result == mock_catalog
155
+ mock_get.assert_called_once_with(id=1)
156
+
157
+ @patch("regscale.integrations.public.cci_importer.error_and_exit")
158
+ @patch("regscale.models.regscale_models.Catalog.get")
159
+ def test_get_catalog_not_found(self, mock_get, mock_error_exit, cci_importer_instance):
160
+ """Test catalog not found scenario."""
161
+ mock_get.return_value = None
162
+ mock_error_exit.side_effect = SystemExit(1) # Mock the actual exit behavior
163
+
164
+ with pytest.raises(SystemExit):
165
+ cci_importer_instance._get_catalog(999)
166
+
167
+ mock_error_exit.assert_called_once_with("Catalog with id 999 not found. Please ensure the catalog exists.")
168
+
169
+ @patch("regscale.models.regscale_models.CCI.get_all_by_parent")
170
+ def test_find_existing_cci(self, mock_get_all, cci_importer_instance):
171
+ """Test finding existing CCI by ID."""
172
+ mock_cci1 = MagicMock()
173
+ mock_cci1.uuid = "CCI-000001"
174
+ mock_cci2 = MagicMock()
175
+ mock_cci2.uuid = "CCI-000002"
176
+
177
+ mock_get_all.return_value = [mock_cci1, mock_cci2]
178
+
179
+ result = cci_importer_instance._find_existing_cci(123, "CCI-000001")
180
+ assert result == mock_cci1
181
+
182
+ result = cci_importer_instance._find_existing_cci(123, "CCI-999999")
183
+ assert result is None
184
+
185
+ def test_create_cci_data(self, cci_importer_instance):
186
+ """Test creating CCI data structure."""
187
+ current_time = "2023-01-01 12:00:00"
188
+
189
+ result = cci_importer_instance._create_cci_data("CCI-000001", "Test definition", "uuid-123", 456, current_time)
190
+
191
+ expected = {
192
+ "name": "CCI-000001",
193
+ "description": "Test definition",
194
+ "controlType": "policy",
195
+ "publishDate": current_time,
196
+ "dateLastUpdated": current_time,
197
+ "lastUpdatedById": "uuid-123",
198
+ "isPublic": True,
199
+ "tenantsId": 456,
200
+ }
201
+
202
+ assert result == expected
203
+
204
+ def test_create_cci_data_no_user(self, cci_importer_instance):
205
+ """Test creating CCI data with no user ID."""
206
+ current_time = "2023-01-01 12:00:00"
207
+
208
+ result = cci_importer_instance._create_cci_data("CCI-000001", "Test definition", None, 456, current_time)
209
+
210
+ assert result["lastUpdatedById"] is None
211
+
212
+ @patch("regscale.models.regscale_models.CCI")
213
+ def test_update_existing_cci(self, mock_cci_class, cci_importer_instance):
214
+ """Test updating an existing CCI."""
215
+ mock_cci = MagicMock()
216
+ cci_data = {"name": "Updated Name", "description": "Updated Description"}
217
+
218
+ cci_importer_instance._update_existing_cci(mock_cci, cci_data)
219
+
220
+ assert mock_cci.name == "Updated Name"
221
+ assert mock_cci.description == "Updated Description"
222
+ mock_cci.create_or_update.assert_called_once()
223
+
224
+ @patch("regscale.integrations.public.cci_importer.CCI")
225
+ def test_create_new_cci(self, mock_cci_class, cci_importer_instance):
226
+ """Test creating a new CCI."""
227
+ mock_cci = MagicMock()
228
+ mock_cci_class.return_value = mock_cci
229
+ mock_cci.create.return_value = mock_cci # Mock the create method return
230
+
231
+ cci_data = {"name": "CCI-000001", "description": "Test definition"}
232
+ current_time = "2023-01-01 12:00:00"
233
+
234
+ result = cci_importer_instance._create_new_cci("CCI-000001", cci_data, 123, "uuid-456", current_time)
235
+
236
+ mock_cci_class.assert_called_once_with(
237
+ uuid="CCI-000001", securityControlId=123, createdById="uuid-456", dateCreated=current_time, **cci_data
238
+ )
239
+ mock_cci.create.assert_called_once()
240
+ assert result == mock_cci
241
+
242
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._get_user_context")
243
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._find_existing_cci")
244
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._create_new_cci")
245
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._update_existing_cci")
246
+ def test_process_cci_for_control(self, mock_update, mock_create, mock_find, mock_context, cci_importer_instance):
247
+ """Test processing CCI items for a control."""
248
+ mock_context.return_value = ("uuid-123", 456)
249
+ mock_find.side_effect = [None, MagicMock()] # First not found, second found
250
+ mock_create.return_value = MagicMock()
251
+
252
+ cci_list = [
253
+ {"cci_id": "CCI-000001", "definition": "Definition 1"},
254
+ {"cci_id": "CCI-000002", "definition": "Definition 2"},
255
+ ]
256
+
257
+ created, updated = cci_importer_instance._process_cci_for_control(789, cci_list, "uuid-123", 456)
258
+
259
+ assert created == 1
260
+ assert updated == 1
261
+ mock_create.assert_called_once()
262
+ mock_update.assert_called_once()
263
+
264
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._get_catalog")
265
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._get_user_context")
266
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._process_cci_for_control")
267
+ @patch("regscale.models.regscale_models.SecurityControl.get_all_by_parent")
268
+ def test_map_to_security_controls(
269
+ self, mock_get_controls, mock_process, mock_context, mock_catalog, cci_importer_instance
270
+ ):
271
+ """Test mapping CCI data to security controls."""
272
+ # Setup mocks
273
+ mock_catalog_obj = MagicMock()
274
+ mock_catalog_obj.id = 1
275
+ mock_catalog.return_value = mock_catalog_obj
276
+
277
+ mock_control1 = MagicMock()
278
+ mock_control1.controlId = "AC-1"
279
+ mock_control1.id = 101
280
+ mock_control2 = MagicMock()
281
+ mock_control2.controlId = "AC-2"
282
+ mock_control2.id = 102
283
+ mock_get_controls.return_value = [mock_control1, mock_control2]
284
+
285
+ mock_context.return_value = ("uuid-123", 456)
286
+ mock_process.return_value = (2, 1) # 2 created, 1 updated
287
+
288
+ # Setup test data
289
+ cci_importer_instance.normalized_cci = {
290
+ "AC-1": [{"cci_id": "CCI-000001", "definition": "Definition 1"}],
291
+ "AC-2": [{"cci_id": "CCI-000002", "definition": "Definition 2"}],
292
+ "AC-99": [{"cci_id": "CCI-000099", "definition": "Definition 99"}], # Non-existent control
293
+ }
294
+
295
+ result = cci_importer_instance.map_to_security_controls(catalog_id=1)
296
+
297
+ assert result["created"] == 4 # 2 calls * 2 created each
298
+ assert result["updated"] == 2 # 2 calls * 1 updated each
299
+ assert result["skipped"] == 1 # AC-99 control not found
300
+ assert result["total_processed"] == 3
301
+
302
+ mock_catalog.assert_called_once_with(1)
303
+ assert mock_process.call_count == 2 # Called for AC-1 and AC-2
304
+
305
+ def test_get_normalized_cci(self, cci_importer_instance):
306
+ """Test getting normalized CCI data."""
307
+ test_data = {"AC-1": [{"cci_id": "CCI-000001", "definition": "Test"}]}
308
+ cci_importer_instance.normalized_cci = test_data
309
+
310
+ result = cci_importer_instance.get_normalized_cci()
311
+ assert result == test_data
312
+
313
+ def test_format_index(self, cci_importer_instance):
314
+ """Test formatting CCI index strings."""
315
+ # Basic formatting
316
+ assert cci_importer_instance.format_index("AC-1 a 1") == "AC-1(a)(1)"
317
+ assert cci_importer_instance.format_index("AC-2 a") == "AC-2(a)"
318
+
319
+ # Already formatted with parentheses
320
+ assert cci_importer_instance.format_index("IA-13 (03) (a)") == "IA-13(03)(a)"
321
+
322
+ # Mixed format
323
+ assert cci_importer_instance.format_index("AC-1 a 1 (a)") == "AC-1(a)(1)(a)"
324
+
325
+ # Single component (no parts)
326
+ assert cci_importer_instance.format_index("SC-7") == "SC-7"
327
+
328
+ # Empty or whitespace
329
+ assert cci_importer_instance.format_index("") == ""
330
+ assert cci_importer_instance.format_index(" ") == ""
331
+
332
+ def test_parse_objective_id(self, cci_importer_instance):
333
+ """Test parsing objective otherId strings."""
334
+ # Basic control with part
335
+ control_base, part = cci_importer_instance.parse_objective_id("ac-1_smt.a")
336
+ assert control_base == "AC-1"
337
+ assert part == "a"
338
+
339
+ # Enhancement with part
340
+ control_base, part = cci_importer_instance.parse_objective_id("ac-2.3_smt.a")
341
+ assert control_base == "AC-2(3)"
342
+ assert part == "a"
343
+
344
+ # Enhancement with part (different enhancement number)
345
+ control_base, part = cci_importer_instance.parse_objective_id("au-10.1_smt.b")
346
+ assert control_base == "AU-10(1)"
347
+ assert part == "b"
348
+
349
+ # Enhancement without part
350
+ control_base, part = cci_importer_instance.parse_objective_id("ac-2.4_smt")
351
+ assert control_base == "AC-2(4)"
352
+ assert part is None
353
+
354
+ # Invalid format
355
+ control_base, part = cci_importer_instance.parse_objective_id("invalid")
356
+ assert control_base is None
357
+ assert part is None
358
+
359
+ # Invalid format (no _smt)
360
+ control_base, part = cci_importer_instance.parse_objective_id("ac-1.a")
361
+ assert control_base is None
362
+ assert part is None
363
+
364
+ def test_parse_objective_id_revision_4(self, cci_importer_instance):
365
+ """Test parsing objective otherId strings in NIST 800-53 Revision 4 format."""
366
+ # Rev 4 format: control_smt.part.subpart
367
+ control_base, part = cci_importer_instance.parse_objective_id("ac-1_smt.a.1")
368
+ assert control_base == "AC-1"
369
+ assert part == "a"
370
+
371
+ control_base, part = cci_importer_instance.parse_objective_id("ac-1_smt.b.2")
372
+ assert control_base == "AC-1"
373
+ assert part == "b"
374
+
375
+ # Rev 4 with enhancement
376
+ control_base, part = cci_importer_instance.parse_objective_id("ac-2.3_smt.d.1")
377
+ assert control_base == "AC-2(3)"
378
+ assert part == "d"
379
+
380
+ # Rev 4 with multiple digit subpart
381
+ control_base, part = cci_importer_instance.parse_objective_id("au-10.1_smt.c.15")
382
+ assert control_base == "AU-10(1)"
383
+ assert part == "c"
384
+
385
+ def test_find_matching_ccis(self, cci_importer_instance):
386
+ """Test finding matching CCIs by control base and part."""
387
+ cci_map = {
388
+ "AC-1(a)": "CCI-000001",
389
+ "AC-1(a)(1)": "CCI-000002",
390
+ "AC-1(a)(2)": "CCI-000003",
391
+ "AC-1(b)": "CCI-000004",
392
+ "AC-2(3)": "CCI-000005",
393
+ "AC-2(3)(a)": "CCI-000006",
394
+ }
395
+
396
+ # Match with part 'a'
397
+ matches = cci_importer_instance.find_matching_ccis("AC-1", "a", cci_map)
398
+ assert len(matches) == 3
399
+ assert "CCI-000001" in matches
400
+ assert "CCI-000002" in matches
401
+ assert "CCI-000003" in matches
402
+
403
+ # Match with part 'b'
404
+ matches = cci_importer_instance.find_matching_ccis("AC-1", "b", cci_map)
405
+ assert len(matches) == 1
406
+ assert "CCI-000004" in matches
407
+
408
+ # Enhancement without part (exact match only)
409
+ matches = cci_importer_instance.find_matching_ccis("AC-2(3)", None, cci_map)
410
+ assert len(matches) == 1
411
+ assert "CCI-000005" in matches
412
+
413
+ # No matches
414
+ matches = cci_importer_instance.find_matching_ccis("AC-99", "a", cci_map)
415
+ assert len(matches) == 0
416
+
417
+ def test_find_matching_ccis_by_name(self, cci_importer_instance):
418
+ """Test finding CCIs by name fallback method."""
419
+ cci_map = {
420
+ "AC-1(a)": "CCI-000001",
421
+ "AC-1(a)(1)": "CCI-000002",
422
+ "AC-2(4)": "CCI-000003",
423
+ }
424
+
425
+ # Exact match by name
426
+ matches = cci_importer_instance.find_matching_ccis_by_name("AC-2(4)", "AC-2(4)", cci_map)
427
+ assert len(matches) == 1
428
+ assert "CCI-000003" in matches
429
+
430
+ # Single letter match
431
+ matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "a.", cci_map)
432
+ assert len(matches) == 2
433
+ assert "CCI-000001" in matches
434
+ assert "CCI-000002" in matches
435
+
436
+ # Single letter without period
437
+ matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "a", cci_map)
438
+ assert len(matches) == 2
439
+
440
+ # No match
441
+ matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "xyz", cci_map)
442
+ assert len(matches) == 0
443
+
444
+ def test_find_matching_ccis_by_name_revision_4(self, cci_importer_instance):
445
+ """Test finding CCIs by name using NIST 800-53 Revision 4 label formats."""
446
+ cci_map = {
447
+ "AC-1(a)": "CCI-000001",
448
+ "AC-1(a)(1)": "CCI-000002",
449
+ "AC-1(b)": "CCI-000003",
450
+ "AC-1(b)(2)": "CCI-000004",
451
+ }
452
+
453
+ # Rev 4 label format: "a.1."
454
+ matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "a.1.", cci_map)
455
+ assert len(matches) == 2
456
+ assert "CCI-000001" in matches
457
+ assert "CCI-000002" in matches
458
+
459
+ # Rev 4 label format without trailing period: "a.1"
460
+ matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "a.1", cci_map)
461
+ assert len(matches) == 2
462
+
463
+ # Rev 4 label format: "b.2."
464
+ matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "b.2.", cci_map)
465
+ assert len(matches) == 2
466
+ assert "CCI-000003" in matches
467
+ assert "CCI-000004" in matches
468
+
469
+ # Invalid rev 4 format (no digit)
470
+ matches = cci_importer_instance.find_matching_ccis_by_name("AC-1", "a.", cci_map)
471
+ assert len(matches) == 2 # Should still match as single letter
472
+
473
+ def test_ccis_already_present(self, cci_importer_instance):
474
+ """Test checking for duplicate CCI IDs."""
475
+ # CCIs already present
476
+ current = "ac-1_smt.a, CCI-000001, CCI-000002"
477
+ new = "CCI-000002, CCI-000003"
478
+ assert cci_importer_instance.ccis_already_present(current, new) is True
479
+
480
+ # No overlap
481
+ current = "ac-1_smt.a, CCI-000001, CCI-000002"
482
+ new = "CCI-000003, CCI-000004"
483
+ assert cci_importer_instance.ccis_already_present(current, new) is False
484
+
485
+ # Empty current
486
+ current = ""
487
+ new = "CCI-000001"
488
+ assert cci_importer_instance.ccis_already_present(current, new) is False
489
+
490
+ # No CCI IDs in current
491
+ current = "ac-1_smt.a"
492
+ new = "CCI-000001"
493
+ assert cci_importer_instance.ccis_already_present(current, new) is False
494
+
495
+ def test_parse_cci_creates_grouped_index(self, cci_importer_instance):
496
+ """Test that parse_cci creates both normalized_cci and cci_grouped_by_index."""
497
+ cci_importer_instance.parse_cci()
498
+
499
+ # Check normalized_cci was created
500
+ assert len(cci_importer_instance.normalized_cci) > 0
501
+
502
+ # Check cci_grouped_by_index was created
503
+ assert len(cci_importer_instance.cci_grouped_by_index) > 0
504
+
505
+ # Verify formatted indices
506
+ # The sample data has "AC-1 a" and "AC-1 b" and "AC-2 a 1"
507
+ assert "AC-1(a)" in cci_importer_instance.cci_grouped_by_index
508
+ assert "AC-1(b)" in cci_importer_instance.cci_grouped_by_index
509
+ assert "AC-2(a)(1)" in cci_importer_instance.cci_grouped_by_index
510
+
511
+ @patch("regscale.integrations.public.cci_importer.ControlObjective")
512
+ def test_map_to_control_objectives(self, mock_objective_class, cci_importer_instance):
513
+ """Test mapping CCIs to control objectives."""
514
+ # Setup sample grouped index data
515
+ cci_importer_instance.cci_grouped_by_index = {
516
+ "AC-1(a)": "CCI-000001, CCI-000002",
517
+ "AC-1(b)": "CCI-000003",
518
+ "AC-2(3)": "CCI-000004",
519
+ }
520
+
521
+ # Create mock objectives
522
+ mock_obj1 = MagicMock()
523
+ mock_obj1.otherId = "ac-1_smt.a"
524
+ mock_obj1.name = "a."
525
+
526
+ mock_obj2 = MagicMock()
527
+ mock_obj2.otherId = "ac-1_smt.b"
528
+ mock_obj2.name = "b."
529
+
530
+ mock_obj3 = MagicMock()
531
+ mock_obj3.otherId = "ac-2.3_smt"
532
+ mock_obj3.name = "AC-2(3)"
533
+
534
+ mock_obj4 = MagicMock()
535
+ mock_obj4.otherId = "invalid"
536
+ mock_obj4.name = "invalid"
537
+
538
+ mock_objective_class.get_by_catalog.return_value = [mock_obj1, mock_obj2, mock_obj3, mock_obj4]
539
+
540
+ # Run the mapping
541
+ result = cci_importer_instance.map_to_control_objectives(catalog_id=1)
542
+
543
+ # Verify results
544
+ assert result["updated"] == 3
545
+ assert result["not_found"] == 1
546
+ assert result["skipped"] == 0
547
+ assert result["total_processed"] == 4
548
+
549
+ # Verify CCIs were added to otherId
550
+ assert "CCI-000001, CCI-000002" in mock_obj1.otherId
551
+ assert "CCI-000003" in mock_obj2.otherId
552
+ assert "CCI-000004" in mock_obj3.otherId
553
+
554
+ # Verify save was called
555
+ assert mock_obj1.save.call_count == 1
556
+ assert mock_obj2.save.call_count == 1
557
+ assert mock_obj3.save.call_count == 1
558
+
559
+ @patch("regscale.integrations.public.cci_importer.ControlObjective")
560
+ def test_map_to_control_objectives_with_duplicates(self, mock_objective_class, cci_importer_instance):
561
+ """Test that duplicate CCIs are not added again."""
562
+ cci_importer_instance.cci_grouped_by_index = {"AC-1(a)": "CCI-000001, CCI-000002"}
563
+
564
+ # Create mock objective with existing CCIs
565
+ mock_obj = MagicMock()
566
+ mock_obj.otherId = "ac-1_smt.a, CCI-000001"
567
+ mock_obj.name = "a."
568
+
569
+ mock_objective_class.get_by_catalog.return_value = [mock_obj]
570
+
571
+ # Run the mapping
572
+ result = cci_importer_instance.map_to_control_objectives(catalog_id=1)
573
+
574
+ # Should be skipped due to duplicate
575
+ assert result["skipped"] == 1
576
+ assert result["updated"] == 0
577
+
578
+ # Verify save was NOT called
579
+ mock_obj.save.assert_not_called()
580
+
581
+ @patch("regscale.integrations.public.cci_importer.ControlObjective")
582
+ def test_map_to_control_objectives_fallback_by_name(self, mock_objective_class, cci_importer_instance):
583
+ """Test fallback matching by name when otherId doesn't match."""
584
+ cci_importer_instance.cci_grouped_by_index = {"AC-1(a)": "CCI-000001"}
585
+
586
+ # Create mock objective with non-standard otherId but valid name
587
+ mock_obj = MagicMock()
588
+ mock_obj.otherId = "custom_id"
589
+ mock_obj.name = "a."
590
+
591
+ # This would normally fail otherId parsing, but should work with name fallback
592
+ # However, the parse_objective_id will return None, None for "custom_id"
593
+ # So it won't match. Let me adjust the test.
594
+
595
+ # Actually, looking at the code, if parse_objective_id returns None, it skips
596
+ # So the fallback only works if control_base is valid
597
+ # Let me create a better test
598
+
599
+ mock_obj.otherId = "ac-1_smt" # Valid but no part
600
+ mock_obj.name = "a" # Single letter should match as part
601
+
602
+ mock_objective_class.get_by_catalog.return_value = [mock_obj]
603
+
604
+ # This should use fallback matching
605
+ result = cci_importer_instance.map_to_control_objectives(catalog_id=1)
606
+
607
+ # With the current logic, ac-1_smt will parse to ("AC-1", None)
608
+ # Then it tries to match with find_matching_ccis("AC-1", None, cci_map)
609
+ # which will only match exact "AC-1" with no remainder
610
+ # So it won't find "AC-1(a)"
611
+ # Then it falls back to find_matching_ccis_by_name("AC-1", "a", cci_map)
612
+ # which should find "AC-1(a)"
613
+
614
+ assert result["updated"] == 1
615
+
616
+ @patch("regscale.integrations.public.cci_importer.ControlObjective")
617
+ def test_map_to_control_objectives_revision_4_format(self, mock_objective_class):
618
+ """Test mapping CCIs to control objectives using NIST 800-53 Revision 4 formats."""
619
+ # Create XML with rev 4 references
620
+ xml_content = """<?xml version="1.0" encoding="utf-8"?>
621
+ <cci_list xmlns="http://iase.disa.mil/cci">
622
+ <cci_item id="CCI-000001">
623
+ <definition>Rev 4 definition for AC-1 a 1</definition>
624
+ <references>
625
+ <reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
626
+ location="AC-1" index="AC-1 a 1" />
627
+ </references>
628
+ </cci_item>
629
+ <cci_item id="CCI-000002">
630
+ <definition>Rev 4 definition for AC-1 a 2</definition>
631
+ <references>
632
+ <reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
633
+ location="AC-1" index="AC-1 a 2" />
634
+ </references>
635
+ </cci_item>
636
+ <cci_item id="CCI-000003">
637
+ <definition>Rev 4 definition for AC-1 b 1</definition>
638
+ <references>
639
+ <reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
640
+ location="AC-1" index="AC-1 b 1" />
641
+ </references>
642
+ </cci_item>
643
+ </cci_list>"""
644
+
645
+ root = ET.fromstring(xml_content)
646
+ importer = CCIImporter(root, version="4", verbose=False)
647
+ importer.parse_cci()
648
+
649
+ # Create mock objectives with rev 4 format (otherId with subparts)
650
+ mock_obj1 = MagicMock()
651
+ mock_obj1.otherId = "ac-1_smt.a.1" # Rev 4 format: part.subpart
652
+ mock_obj1.name = "a.1."
653
+
654
+ mock_obj2 = MagicMock()
655
+ mock_obj2.otherId = "ac-1_smt.a.2"
656
+ mock_obj2.name = "a.2."
657
+
658
+ mock_obj3 = MagicMock()
659
+ mock_obj3.otherId = "ac-1_smt.b.1"
660
+ mock_obj3.name = "b.1."
661
+
662
+ mock_objective_class.get_by_catalog.return_value = [mock_obj1, mock_obj2, mock_obj3]
663
+
664
+ # Run the mapping
665
+ result = importer.map_to_control_objectives(catalog_id=1)
666
+
667
+ # Verify all rev 4 objectives were successfully mapped
668
+ assert result["updated"] == 3
669
+ assert result["not_found"] == 0
670
+ assert result["skipped"] == 0
671
+ assert result["total_processed"] == 3
672
+
673
+ # Verify CCIs were added to otherId
674
+ assert "CCI-000001" in mock_obj1.otherId
675
+ assert "CCI-000002" in mock_obj2.otherId
676
+ assert "CCI-000003" in mock_obj3.otherId
677
+
678
+ # Verify save was called for all
679
+ assert mock_obj1.save.call_count == 1
680
+ assert mock_obj2.save.call_count == 1
681
+ assert mock_obj3.save.call_count == 1
682
+
683
+ @patch("regscale.integrations.public.cci_importer.ControlObjective")
684
+ def test_map_to_control_objectives_revision_5_format(self, mock_objective_class):
685
+ """Test mapping CCIs to control objectives using NIST 800-53 Revision 5 formats."""
686
+ # Create XML with rev 5 references
687
+ xml_content = """<?xml version="1.0" encoding="utf-8"?>
688
+ <cci_list xmlns="http://iase.disa.mil/cci">
689
+ <cci_item id="CCI-000001">
690
+ <definition>Rev 5 definition for AC-1 a 1 (a)</definition>
691
+ <references>
692
+ <reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
693
+ location="AC-1" index="AC-1 a 1 (a)" />
694
+ </references>
695
+ </cci_item>
696
+ <cci_item id="CCI-000002">
697
+ <definition>Rev 5 definition for AC-1 a 2</definition>
698
+ <references>
699
+ <reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
700
+ location="AC-1" index="AC-1 a 2" />
701
+ </references>
702
+ </cci_item>
703
+ <cci_item id="CCI-000003">
704
+ <definition>Rev 5 definition for AC-1 c 1</definition>
705
+ <references>
706
+ <reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
707
+ location="AC-1" index="AC-1 c 1" />
708
+ </references>
709
+ </cci_item>
710
+ </cci_list>"""
711
+
712
+ root = ET.fromstring(xml_content)
713
+ importer = CCIImporter(root, version="5", verbose=False)
714
+ importer.parse_cci()
715
+
716
+ # Create mock objectives with rev 5 format (no subparts)
717
+ mock_obj1 = MagicMock()
718
+ mock_obj1.otherId = "ac-1_smt.a" # Rev 5 format: just part letter
719
+ mock_obj1.name = "a"
720
+
721
+ mock_obj2 = MagicMock()
722
+ mock_obj2.otherId = "ac-1_smt.c"
723
+ mock_obj2.name = "c"
724
+
725
+ mock_objective_class.get_by_catalog.return_value = [mock_obj1, mock_obj2]
726
+
727
+ # Run the mapping
728
+ result = importer.map_to_control_objectives(catalog_id=1)
729
+
730
+ # Verify rev 5 objectives were successfully mapped
731
+ # obj1 should get both CCI-000001 (AC-1(a)(1)(a)) and CCI-000002 (AC-1(a)(2))
732
+ # obj2 should get CCI-000003 (AC-1(c)(1))
733
+ assert result["updated"] == 2
734
+ assert result["not_found"] == 0
735
+ assert result["skipped"] == 0
736
+ assert result["total_processed"] == 2
737
+
738
+ # Verify CCIs were added to otherId
739
+ assert "CCI-000001" in mock_obj1.otherId or "CCI-000002" in mock_obj1.otherId
740
+ assert "CCI-000003" in mock_obj2.otherId
741
+
742
+ # Verify save was called for both
743
+ assert mock_obj1.save.call_count == 1
744
+ assert mock_obj2.save.call_count == 1
745
+
746
+ @patch("regscale.integrations.public.cci_importer.ControlObjective")
747
+ def test_map_to_control_objectives_mixed_revisions(self, mock_objective_class):
748
+ """Test mapping CCIs with mixed revision 4 and 5 control objectives."""
749
+ # Create XML with both rev 4 and rev 5 references
750
+ xml_content = """<?xml version="1.0" encoding="utf-8"?>
751
+ <cci_list xmlns="http://iase.disa.mil/cci">
752
+ <cci_item id="CCI-000001">
753
+ <definition>AC-1 part a definition</definition>
754
+ <references>
755
+ <reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
756
+ location="AC-1" index="AC-1 a 1" />
757
+ <reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
758
+ location="AC-1" index="AC-1 a 1 (a)" />
759
+ </references>
760
+ </cci_item>
761
+ </cci_list>"""
762
+
763
+ root = ET.fromstring(xml_content)
764
+
765
+ # Test with rev 4 importer
766
+ importer_v4 = CCIImporter(root, version="4", verbose=False)
767
+ importer_v4.parse_cci()
768
+
769
+ mock_obj_v4 = MagicMock()
770
+ mock_obj_v4.otherId = "ac-1_smt.a.1"
771
+ mock_obj_v4.name = "a.1."
772
+
773
+ mock_objective_class.get_by_catalog.return_value = [mock_obj_v4]
774
+
775
+ result_v4 = importer_v4.map_to_control_objectives(catalog_id=1)
776
+ assert result_v4["updated"] == 1
777
+ assert "CCI-000001" in mock_obj_v4.otherId
778
+
779
+ # Test with rev 5 importer
780
+ importer_v5 = CCIImporter(root, version="5", verbose=False)
781
+ importer_v5.parse_cci()
782
+
783
+ mock_obj_v5 = MagicMock()
784
+ mock_obj_v5.otherId = "ac-1_smt.a"
785
+ mock_obj_v5.name = "a"
786
+
787
+ mock_objective_class.get_by_catalog.return_value = [mock_obj_v5]
788
+
789
+ result_v5 = importer_v5.map_to_control_objectives(catalog_id=1)
790
+ assert result_v5["updated"] == 1
791
+ assert "CCI-000001" in mock_obj_v5.otherId
792
+
793
+ @patch("regscale.integrations.public.cci_importer.ControlObjective")
794
+ def test_map_to_control_objectives_revision_4_with_enhancements(self, mock_objective_class):
795
+ """Test mapping CCIs with revision 4 control enhancements."""
796
+ xml_content = """<?xml version="1.0" encoding="utf-8"?>
797
+ <cci_list xmlns="http://iase.disa.mil/cci">
798
+ <cci_item id="CCI-000001">
799
+ <definition>AC-2(3) enhancement definition</definition>
800
+ <references>
801
+ <reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
802
+ location="AC-2(3)" index="AC-2 (3)" />
803
+ </references>
804
+ </cci_item>
805
+ <cci_item id="CCI-000002">
806
+ <definition>AC-2(3) part d definition</definition>
807
+ <references>
808
+ <reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
809
+ location="AC-2(3)" index="AC-2 (3) d" />
810
+ </references>
811
+ </cci_item>
812
+ </cci_list>"""
813
+
814
+ root = ET.fromstring(xml_content)
815
+ importer = CCIImporter(root, version="4", verbose=False)
816
+ importer.parse_cci()
817
+
818
+ # Create mock objectives with rev 4 enhancement format
819
+ mock_obj1 = MagicMock()
820
+ mock_obj1.otherId = "ac-2.3_smt" # Enhancement without part
821
+ mock_obj1.name = "AC-2(3)"
822
+
823
+ mock_obj2 = MagicMock()
824
+ mock_obj2.otherId = "ac-2.3_smt.d.1" # Enhancement with part and subpart
825
+ mock_obj2.name = "d.1."
826
+
827
+ mock_objective_class.get_by_catalog.return_value = [mock_obj1, mock_obj2]
828
+
829
+ result = importer.map_to_control_objectives(catalog_id=1)
830
+
831
+ assert result["updated"] == 2
832
+ assert result["not_found"] == 0
833
+
834
+ # Verify the enhancement without part got the base CCI
835
+ assert "CCI-000001" in mock_obj1.otherId
836
+ # Verify the enhancement with part got the part-specific CCI
837
+ assert "CCI-000002" in mock_obj2.otherId
838
+
839
+ @patch("regscale.integrations.public.cci_importer.ControlObjective")
840
+ def test_map_to_control_objectives_revision_4_label_fallback(self, mock_objective_class):
841
+ """Test that revision 4 label format fallback matching works correctly."""
842
+ xml_content = """<?xml version="1.0" encoding="utf-8"?>
843
+ <cci_list xmlns="http://iase.disa.mil/cci">
844
+ <cci_item id="CCI-000001">
845
+ <definition>AC-1 part a definition</definition>
846
+ <references>
847
+ <reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
848
+ location="AC-1" index="AC-1 a" />
849
+ </references>
850
+ </cci_item>
851
+ </cci_list>"""
852
+
853
+ root = ET.fromstring(xml_content)
854
+ importer = CCIImporter(root, version="4", verbose=False)
855
+ importer.parse_cci()
856
+
857
+ # Create objective with rev 4 format where otherId parsing should work
858
+ mock_obj = MagicMock()
859
+ mock_obj.otherId = "ac-1_smt" # No part specified
860
+ mock_obj.name = "a.1." # Rev 4 label format should trigger fallback
861
+
862
+ mock_objective_class.get_by_catalog.return_value = [mock_obj]
863
+
864
+ result = importer.map_to_control_objectives(catalog_id=1)
865
+
866
+ # Should successfully map using fallback name matching
867
+ assert result["updated"] == 1
868
+ assert "CCI-000001" in mock_obj.otherId
869
+
870
+
871
+ class TestCCIImporterCLI:
872
+ """Test cases for the CCI importer CLI command."""
873
+
874
+ @patch("regscale.integrations.public.cci_importer._load_xml_file")
875
+ @patch("regscale.integrations.public.cci_importer.CCIImporter")
876
+ def test_cci_importer_command_dry_run(self, mock_importer_class, mock_load_xml):
877
+ """Test CLI command with dry run flag."""
878
+ runner = CliRunner()
879
+
880
+ # Setup mocks
881
+ mock_root = MagicMock()
882
+ mock_load_xml.return_value = mock_root
883
+
884
+ mock_importer = MagicMock()
885
+ mock_importer.get_normalized_cci.return_value = {"AC-1": []}
886
+ mock_importer_class.return_value = mock_importer
887
+
888
+ result = runner.invoke(cci_importer, ["--dry-run", "--verbose"])
889
+
890
+ # More detailed assertion with error context
891
+ if result.exit_code != 0:
892
+ print(f"Command output: {result.output}")
893
+ print(f"Exception: {result.exception}")
894
+ assert result.exit_code == 0, f"Expected exit code 0, got {result.exit_code}. Output: {result.output}"
895
+ mock_load_xml.assert_called_once()
896
+ mock_importer_class.assert_called_once_with(mock_root, version="5", verbose=True)
897
+ mock_importer.parse_cci.assert_called_once()
898
+ mock_importer.map_to_security_controls.assert_not_called()
899
+
900
+ @patch("regscale.integrations.public.cci_importer._load_xml_file")
901
+ @patch("regscale.integrations.public.cci_importer.CCIImporter")
902
+ def test_cci_importer_command_with_database(self, mock_importer_class, mock_load_xml):
903
+ """Test CLI command with database operations (default: maps to objectives)."""
904
+ runner = CliRunner()
905
+
906
+ # Setup mocks
907
+ mock_root = MagicMock()
908
+ mock_load_xml.return_value = mock_root
909
+
910
+ mock_importer = MagicMock()
911
+ mock_importer.get_normalized_cci.return_value = {"AC-1": []}
912
+ mock_importer.map_to_security_controls.return_value = {
913
+ "created": 5,
914
+ "updated": 3,
915
+ "skipped": 1,
916
+ "total_processed": 2,
917
+ }
918
+ mock_importer.map_to_control_objectives.return_value = {
919
+ "updated": 10,
920
+ "skipped": 2,
921
+ "not_found": 1,
922
+ "total_processed": 13,
923
+ }
924
+ mock_importer_class.return_value = mock_importer
925
+
926
+ result = runner.invoke(cci_importer, ["-n", "4", "-c", "2"])
927
+
928
+ # More detailed assertion with error context
929
+ if result.exit_code != 0:
930
+ print(f"Command output: {result.output}")
931
+ print(f"Exception: {result.exception}")
932
+ assert result.exit_code == 0, f"Expected exit code 0, got {result.exit_code}. Output: {result.output}"
933
+ mock_importer_class.assert_called_once_with(mock_root, version="4", verbose=False)
934
+ mock_importer.map_to_security_controls.assert_called_with(2)
935
+ # By default, should also map to objectives
936
+ mock_importer.map_to_control_objectives.assert_called_with(2)
937
+
938
+ @patch("regscale.integrations.public.cci_importer._load_xml_file")
939
+ @patch("regscale.integrations.public.cci_importer.CCIImporter")
940
+ def test_cci_importer_command_with_disable_objectives(self, mock_importer_class, mock_load_xml):
941
+ """Test CLI command with --disable-objectives flag."""
942
+ runner = CliRunner()
943
+
944
+ # Setup mocks
945
+ mock_root = MagicMock()
946
+ mock_load_xml.return_value = mock_root
947
+
948
+ mock_importer = MagicMock()
949
+ mock_importer.get_normalized_cci.return_value = {"AC-1": []}
950
+ mock_importer.map_to_security_controls.return_value = {
951
+ "created": 5,
952
+ "updated": 3,
953
+ "skipped": 1,
954
+ "total_processed": 2,
955
+ }
956
+ mock_importer_class.return_value = mock_importer
957
+
958
+ result = runner.invoke(cci_importer, ["-n", "4", "-c", "2", "--disable-objectives"])
959
+
960
+ # Should succeed
961
+ assert result.exit_code == 0
962
+ mock_importer_class.assert_called_once_with(mock_root, version="4", verbose=False)
963
+ mock_importer.map_to_security_controls.assert_called_with(2)
964
+ # Should NOT map to objectives when flag is set
965
+ mock_importer.map_to_control_objectives.assert_not_called()
966
+
967
+ @patch("regscale.integrations.public.cci_importer._load_xml_file")
968
+ def test_cci_importer_command_xml_error(self, mock_load_xml):
969
+ """Test CLI command with XML loading error."""
970
+ runner = CliRunner()
971
+
972
+ mock_load_xml.side_effect = ET.ParseError("Invalid XML")
973
+
974
+ result = runner.invoke(cci_importer)
975
+
976
+ assert result.exit_code != 0
977
+
978
+ def test_load_xml_file_success(self, tmp_path):
979
+ """Test successful XML file loading."""
980
+ xml_content = """<?xml version="1.0" encoding="utf-8"?>
981
+ <cci_list xmlns="http://iase.disa.mil/cci">
982
+ <cci_item id="CCI-000001">
983
+ <definition>Test definition</definition>
984
+ </cci_item>
985
+ </cci_list>"""
986
+
987
+ xml_file = tmp_path / "test.xml"
988
+ xml_file.write_text(xml_content)
989
+
990
+ root = _load_xml_file(str(xml_file))
991
+
992
+ assert root is not None
993
+ assert root.tag.endswith("cci_list")
994
+
995
+ def test_load_xml_file_parse_error(self, tmp_path):
996
+ """Test XML file loading with parse error."""
997
+ invalid_xml = "This is not valid XML"
998
+
999
+ xml_file = tmp_path / "invalid.xml"
1000
+ xml_file.write_text(invalid_xml)
1001
+
1002
+ with pytest.raises(SystemExit):
1003
+ _load_xml_file(str(xml_file))
1004
+
1005
+
1006
+ class TestCCIImporterIntegration:
1007
+ """Integration tests for CCI importer."""
1008
+
1009
+ def test_full_workflow_dry_run(self):
1010
+ """Test full workflow in dry run mode."""
1011
+ xml_content = """<?xml version="1.0" encoding="utf-8"?>
1012
+ <cci_list xmlns="http://iase.disa.mil/cci">
1013
+ <cci_item id="CCI-000001">
1014
+ <definition>Test definition for AC-1</definition>
1015
+ <references>
1016
+ <reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
1017
+ location="AC-1" index="AC-1 a" />
1018
+ </references>
1019
+ </cci_item>
1020
+ </cci_list>"""
1021
+
1022
+ root = ET.fromstring(xml_content)
1023
+ importer = CCIImporter(root, version="5", verbose=False)
1024
+
1025
+ # Parse CCI data
1026
+ importer.parse_cci()
1027
+ normalized = importer.get_normalized_cci()
1028
+
1029
+ # Verify parsing worked correctly
1030
+ assert "AC-1" in normalized
1031
+ assert len(normalized["AC-1"]) == 1
1032
+ assert normalized["AC-1"][0]["cci_id"] == "CCI-000001"
1033
+ assert "Test definition for AC-1" in normalized["AC-1"][0]["definition"]
1034
+
1035
+ @patch("regscale.integrations.public.cci_importer.Application")
1036
+ def test_user_context_caching(self, mock_app_class):
1037
+ """Test that user context is properly cached."""
1038
+ mock_app = MagicMock()
1039
+ mock_app.config.get.side_effect = lambda key, default=None: {"userId": "uuid-123", "tenantId": "456"}.get(
1040
+ key, default
1041
+ )
1042
+ mock_app_class.return_value = mock_app
1043
+
1044
+ root = ET.fromstring("<root></root>")
1045
+ importer = CCIImporter(root)
1046
+
1047
+ # First call should create the context
1048
+ context1 = importer._get_user_context()
1049
+ # Second call should use cached context
1050
+ context2 = importer._get_user_context()
1051
+
1052
+ assert context1 == context2
1053
+ assert mock_app_class.call_count == 1 # Should only create Application once